Руководство по языку (proto 2)

Охватывает, как использовать редакцию proto2 языка Protocol Buffers в вашем проекте.

Это руководство описывает, как использовать язык protocol buffer для структурирования ваших данных protocol buffer, включая синтаксис файлов .proto и как генерировать классы доступа к данных из ваших файлов .proto. Оно охватывает редакцию proto2 языка protocol buffers.

Для информации о синтаксисе editions см. Руководство по языку Protobuf Editions.

Для информации о синтаксисе proto3 см. Руководство по языку Proto3.

Это справочное руководство – для пошагового примера, который использует многие из функций, описанных в этом документе, см. обучение для выбранного вами языка.

Определение типа сообщения

Сначала давайте рассмотрим очень простой пример. Допустим, вы хотите определить формат сообщения поискового запроса, где каждый поисковый запрос имеет строку запроса, конкретную страницу результатов, которая вас интересует, и количество результатов на страницу. Вот файл .proto, который вы используете для определения типа сообщения.

syntax = "proto2";

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
}
  • Первая строка файла указывает, что вы используете редакцию proto2 спецификации языка protobuf.

    • syntax должна быть первой непустой, не-комментарием строкой файла.
    • Если syntax не указана, компилятор protocol buffer предположит, что вы используете proto2.
  • Определение сообщения SearchRequest задает три поля (пары имя/значение), по одному для каждой части данных, которые вы хотите включить в этот тип сообщения. Каждое поле имеет имя и тип.

Указание типов полей

В предыдущем примере все поля являются скалярными типами: два целых числа (page_number и results_per_page) и строка (query). Вы также можете указать перечисления и составные типы, такие как другие типы сообщений, для вашего поля.

Назначение номеров полей

Вы должны дать каждому полю в определении вашего сообщения число между 1 и 536,870,911 со следующими ограничениями:

  • Данный номер должен быть уникальным среди всех полей этого сообщения.
  • Номера полей 19,000 до 19,999 зарезервированы для реализации Protocol Buffers. Компилятор protocol buffer будет жаловаться, если вы используете один из этих зарезервированных номеров полей в вашем сообщении.
  • Вы не можете использовать любые ранее зарезервированные номера полей или любые номера полей, которые были выделены для расширений.

Этот номер не может быть изменен после того, как ваш тип сообщения используется, потому что он идентифицирует поле в формате передачи сообщения. «Изменение» номера поля эквивалентно удалению этого поля и созданию нового поля с тем же типом, но новым номером. См. Удаление полей для того, как сделать это правильно.

Номера полей никогда не должны использоваться повторно. Никогда не извлекайте номер поля из зарезервированного списка для повторного использования с новым определением поля. См. Последствия повторного использования номеров полей.

Вам следует использовать номера полей от 1 до 15 для наиболее часто устанавливаемых полей. Меньшие значения номеров полей занимают меньше места в формате передачи. Например, номера полей в диапазоне от 1 до 15 занимают один байт для кодирования. Номера полей в диапазоне от 16 до 2047 занимают два байта. Вы можете узнать больше об этом в Кодирование Protocol Buffer.

Последствия повторного использования номеров полей

Повторное использование номера поля делает декодирование сообщений в формате передачи неоднозначным.

Формат передачи protobuf является «легким» и не предоставляет способа обнаружения полей, закодированных с использованием одного определения и декодированных с использованием другого.

Кодирование поля с использованием одного определения и последующее декодирование этого же поля с другим определением может привести к:

  • Потере времени разработчика на отладку
  • Ошибке разбора/слияния (лучший сценарий)
  • Утечке PII/SPII
  • Повреждению данных

Распространенные причины повторного использования номеров полей:

  • перенумерация полей (иногда делается для достижения более эстетически приятного порядка номеров для полей). Перенумерация фактически удаляет и заново добавляет все поля, вовлеченные в перенумерацию, что приводит к несовместимым изменениям формата передачи.

  • удаление поля и не резервирование номера для предотвращения будущего повторного использования.

Номер поля ограничен 29 битами вместо 32 битов, потому что три бита используются для указания формата передачи поля. Для получения дополнительной информации см. Тема Кодирование.

Указание мощности полей

Поля сообщения могут быть одним из следующих:

  • Одиночное (Singular):

    В proto2 есть два типа одиночных полей:

    • optional: (рекомендуется) Поле optional находится в одном из двух возможных состояний:

      • поле установлено и содержит значение, которое было явно установлено или разобрано из передачи. Оно будет сериализовано в передачу.
      • поле не установлено и будет возвращать значение по умолчанию. Оно не будет сериализовано в передачу.

      Вы можете проверить, было ли значение явно установлено.

    • required: Не используйте. Обязательные поля настолько проблематичны, что были удалены из proto3 и editions. Семантика обязательных полей должна быть реализована на уровне приложения. Когда оно используется, правильно сформированное сообщение должно иметь ровно одно такое поле.

  • repeated: этот тип поля может повторяться ноль или более раз в правильно сформированном сообщении. Порядок повторяющихся значений будет сохранен.

  • map: это поле типа пар ключ/значение. См. Карты для получения дополнительной информации об этом типе поля.

Используйте упакованное кодирование для новых повторяющихся полей

По историческим причинам, repeated поля скалярных числовых типов (например, int32, int64, enum) не кодируются так эффективно, как могли бы. Новый код должен использовать специальную опцию [packed = true] для получения более эффективного кодирования. Например:

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

Вы можете узнать больше об packed кодировании в Кодирование Protocol Buffer.

Required настоятельно устарел

{{% alert title="Важно" color="warning" %}} Required это навсегда Как упоминалось ранее, required не должен использоваться для новых полей. Семантика обязательных полей должна быть реализована на уровне приложения вместо этого. Существующие required поля должны рассматриваться как постоянные, неизменяемые элементы определения сообщения. Почти невозможно безопасно изменить поле с required на optional. Если существует хоть малейшая вероятность, что существует устаревший читатель, он будет считать сообщения без этого поля неполными и может отклонить или отбросить их. {{% /alert %}}

Вторая проблема с обязательными полями появляется, когда кто-то добавляет значение в enum. В этом случае нераспознанное значение enum рассматривается как если бы оно было отсутствующим, что также вызывает сбой проверки обязательного значения.

Правильно сформированные сообщения

Термин «правильно сформированный» (well-formed), когда применяется к сообщениям protobuf, относится к байтам, сериализованным/десериализованным. Парсер protoc проверяет, что данное proto определение файла разбираемо.

Одиночные поля могут появляться более одного раза в байтах формата передачи. Парсер примет ввод, но только последний экземпляр этого поля будет доступен через сгенерированные привязки. См. Последний побеждает для получения дополнительной информации по этой теме.

Добавление дополнительных типов сообщений

Несколько типов сообщений могут быть определены в одном файле .proto. Это полезно, если вы определяете несколько связанных сообщений – так, например, если вы хотите определить формат сообщения ответа, который соответствует вашему типу сообщения SearchResponse, вы можете добавить его в тот же .proto:

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

Комбинирование сообщений приводит к раздуванию Хотя несколько типов сообщений (таких как message, enum и service) могут быть определены в одном файле .proto, это может также привести к раздуванию зависимостей, когда большое количество сообщений с различными зависимостями определяется в одном файле. Рекомендуется включать как можно меньше типов сообщений в каждый файл .proto.

Добавление комментариев

Чтобы добавить комментарии в ваши файлы .proto:

  • Предпочитайте комментарии в стиле C/C++/Java в конце строки '//' в строке перед элементом кода .proto

  • Встроенные/многострочные комментарии в стиле C /* ... */ также принимаются.

    • При использовании многострочных комментариев предпочтительна строка отступа '*'.
/**
 * SearchRequest представляет поисковый запрос с параметрами pagination,
 * чтобы указать, какие результаты включать в ответ.
 */
message SearchRequest {
  optional string query = 1;

  // Какой номер страницы нам нужен?
  optional int32 page_number = 2;

  // Количество результатов, возвращаемых на страницу.
  optional int32 results_per_page = 3;
}

Удаление полей

Удаление полей может вызвать серьезные проблемы, если не сделано правильно.

Не удаляйте required поля. Это почти невозможно сделать безопасно. Если вы должны удалить required поле, вы должны сначала пометить поле optional и deprecated и убедиться, что все системы, которые так или иначе наблюдают за сообщением, были развернуты с новой схемой. Затем вы можете рассмотреть удаление поля (но обратите внимание, что это все еще процесс, подверженный ошибкам).

Когда вам больше не нужно поле, которое не required, сначала удалите все ссылки на поле из клиентского кода, а затем удалите определение поля из сообщения. Однако, вы должны зарезервировать удаленный номер поля. Если вы не зарезервируете номер поля, возможно, что разработчик повторно использует этот номер в будущем и вызовет поломку.

Вам также следует зарезервировать имя поля, чтобы позволить JSON и TextFormat кодировкам вашего сообщения продолжать разбираться.

Зарезервированные номера полей

Если вы обновляете тип сообщения, полностью удаляя поле, или комментируя его, будущие разработчики могут повторно использовать номер поля при внесении своих собственных обновлений в тип. Это может вызвать серьезные проблемы, как описано в Последствия повторного использования номеров полей. Чтобы убедиться, что этого не происходит, добавьте ваш удаленный номер поля в список reserved.

Компилятор protoc будет генерировать сообщения об ошибках, если любые будущие разработчики попытаются использовать эти зарезервированные номера полей.

message Foo {
  reserved 2, 15, 9 to 11;
}

Диапазоны зарезервированных номеров полей включительны (9 to 11 это то же самое, что 9, 10, 11).

Зарезервированные имена полей

Повторное использование старого имени поля позже обычно безопасно, за исключением случаев использования TextProto или JSON кодировок, где имя поля сериализуется. Чтобы избежать этого риска, вы можете добавить удаленное имя поля в список reserved.

Зарезервированные имена влияют только на поведение компилятора protoc, а не на поведение во время выполнения, за одним исключением: реализации TextProto могут отбрасывать неизвестные поля (без вызова ошибки, как с другими неизвестными полями) с зарезервированными именами во время разбора (только реализации C++ и Go делают это сегодня). JSON парсинг во время выполнения не затрагивается зарезервированными именами.

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

Обратите внимание, что вы не можете смешивать имена полей и номера полей в одном операторе reserved.

Что генерируется из вашего .proto?

Когда вы запускаете компилятор protocol buffer на .proto файле, компилятор генерирует код на выбранном вами языке, который вам понадобится для работы с типами сообщений, которые вы описали в файле, включая получение и установку значений полей, сериализацию ваших сообщений в выходной поток и разбор ваших сообщений из входного потока.

  • Для C++ компилятор генерирует файлы .h и .cc из каждого .proto, с классом для каждого типа сообщения, описанного в вашем файле.
  • Для Java компилятор генерирует файл .java с классом для каждого типа сообщения, а также специальный класс Builder для создания экземпляров классов сообщений.
  • Для Kotlin, в дополнение к сгенерированному Java коду, компилятор генерирует файл .kt для каждого типа сообщения с улучшенным Kotlin API. Это включает DSL, который упрощает создание экземпляров сообщений, метод доступа к nullable полю и функцию копирования.
  • Python немного отличается — компилятор Python генерирует модуль со статическим дескриптором каждого типа сообщения в вашем .proto, который затем используется с метаклассом для создания необходимого класса доступа к данным Python во время выполнения.
  • Для Go компилятор генерирует файл .pb.go с типом для каждого типа сообщения в вашем файле.
  • Для Ruby компилятор генерирует файл .rb с Ruby модулем, содержащим ваши типы сообщений.
  • Для Objective-C компилятор генерирует файлы pbobjc.h и pbobjc.m из каждого .proto, с классом для каждого типа сообщения, описанного в вашем файле.
  • Для C# компилятор генерирует файл .cs из каждого .proto, с классом для каждого типа сообщения, описанного в вашем файле.
  • Для PHP компилятор генерирует файл сообщения .php для каждого типа сообщения, описанного в вашем файле, и файл метаданных .php для каждого .proto файла, который вы компилируете. Файл метаданных используется для загрузки допустимых типов сообщений в пул дескрипторов.
  • Для Dart компилятор генерирует файл .pb.dart с классом для каждого типа сообщения в вашем файле.

Вы можете узнать больше об использовании API для каждого языка, следуя обучению для выбранного языка. Для получения еще более подробной информации об API см. соответствующий справочник по API.

Скалярные типы значений

Скалярное поле сообщения может иметь один из следующих типов – таблица показывает тип, указанный в файле .proto, и соответствующий тип в автоматически сгенерированном классе:

Тип в Proto Примечания
double
float
int32 Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint32 вместо этого.
int64 Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint64 вместо этого.
uint32 Использует кодирование переменной длины.
uint64 Использует кодирование переменной длины.
sint32 Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int32.
sint64 Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int64.
fixed32 Всегда четыре байта. Более эффективно, чем uint32, если значения часто больше чем 228.
fixed64 Всегда восемь байта. Более эффективно, чем uint64, если значения часто больше чем 256.
sfixed32 Всегда четыре байта.
sfixed64 Всегда восемь байта.
bool
string Строка должна всегда содержать текст в кодировке UTF-8 или 7-битный ASCII и не может быть длиннее 232.
bytes Может содержать любую произвольную последовательность байт не длиннее 232.
Тип в Proto Тип в C++ Тип в Java/Kotlin[1] Тип в Python[3] Тип в Go Тип в Ruby Тип в C# Тип в PHP Тип в Dart Тип в Rust
double double double float *float64 Float double float double f64
float float float float *float32 Float float float double f32
int32 int32_t int int int32 Fixnum или Bignum (по необходимости) int integer *int32 i32
int64 int64_t long int/long[4] *int64 Bignum long integer/string[6] Int64 i64
uint32 uint32_t int[2] int/long[4] *uint32 Fixnum или Bignum (по необходимости) uint integer int u32
uint64 uint64_t long[2] int/long[4] *uint64 Bignum ulong integer/string[6] Int64 u64
sint32 int32_t int int int32 Fixnum или Bignum (по необходимости) int integer *int32 i32
sint64 int64_t long int/long[4] *int64 Bignum long integer/string[6] Int64 i64
fixed32 uint32_t int[2] int/long[4] *uint32 Fixnum или Bignum (по необходимости) uint integer int u32
fixed64 uint64_t long[2] int/long[4] *uint64 Bignum ulong integer/string[6] Int64 u64
sfixed32 int32_t int int *int32 Fixnum или Bignum (по необходимости) int integer int i32
sfixed64 int64_t long int/long[4] *int64 Bignum long integer/string[6] Int64 i64
bool bool boolean bool *bool TrueClass/FalseClass bool boolean bool bool
string string String unicode (Python 2), str (Python 3) *string String (UTF-8) string string String ProtoString
bytes string ByteString bytes []byte String (ASCII-8BIT) ByteString string List ProtoBytes

[1] Kotlin использует соответствующие типы из Java, даже для беззнаковых типов, чтобы обеспечить совместимость в смешанных Java/Kotlin кодовых базах.

[2] В Java беззнаковые 32-битные и 64-битные целые числа представляются с использованием их знаковых аналогов, при этом старший бит просто хранится в бите знака.

[3] Во всех случаях установка значений в поле будет выполнять проверку типа чтобы убедиться, что оно допустимо.

[4] 64-битные или беззнаковые 32-битные целые числа всегда представляются как long при декодировании, но могут быть int, если при установке поля задан int. Во всех случаях значение должно помещаться в тип, представленный при установке. См. [2].

[5] Proto2 обычно никогда не проверяет валидность UTF-8 строковых полей. Однако поведение варьируется между языками, и невалидные данные UTF-8 не должны храниться в строковых полях.

[6] Integer используется на 64-битных машинах, а string используется на 32-битных машинах.

Вы можете узнать больше о том, как эти типы кодируются при сериализации вашего сообщения в Кодирование Protocol Buffer.

Значения полей по умолчанию

Когда сообщение разбирается, если закодированные байты сообщения не содержат конкретное поле, доступ к этому полю в разобранном объекте возвращает значение по умолчанию для этого поля. Значения по умолчанию зависят от типа:

  • Для строк значение по умолчанию — пустая строка.
  • Для bytes значение по умолчанию — пустые байты.
  • Для bools значение по умолчанию — false.
  • Для числовых типов значение по умолчанию — ноль.
  • Для полей сообщения поле не установлено. Его точное значение зависит от языка. Подробности см. в руководстве по сгенерированному коду для вашего языка.
  • Для перечислений значение по умолчанию — первое определенное значение перечисления, которое должно быть 0 (рекомендуется для совместимости с открытыми enums). См. Значение перечисления по умолчанию.

Значение по умолчанию для повторяющихся полей — пустое (обычно пустой список в соответствующем языке).

Значение по умолчанию для полей map — пустое (обычно пустая карта в соответствующем языке).

Переопределение скалярных значений по умолчанию

В proto2 вы можете указать явные значения по умолчанию для одиночных полей, не являющихся сообщениями. Например, допустим, вы хотите предоставить значение по умолчанию 10 для поля SearchRequest.results_per_page:

optional int32 results_per_page = 3 [default = 10];

Если отправитель не указывает results_per_page, получатель будет наблюдать следующее состояние:

  • Поле results_per_page отсутствует. То есть метод has_results_per_page() (hazzer метод) вернет false.
  • Значение results_per_page (возвращаемое из «геттера») равно 10.

Если отправитель отправляет значение для results_per_page, значение по умолчанию 10 игнорируется, и значение отправителя возвращается из «геттера».

См. руководство по сгенерированному коду для вашего выбранного языка для получения более подробной информации о том, как работают значения по умолчанию в сгенерированном коде.

Поскольку значение по умолчанию для enums является первым определенным значением enum, будьте осторожны при добавлении значения в начало списка значений enum. См. раздел Обновление типа сообщения для рекомендаций о том, как безопасно изменять определения.

Перечисления

Когда вы определяете тип сообщения, вы можете захотеть, чтобы одно из его полей имело только одно значение из предопределенного списка. Например, допустим, вы хотите добавить поле corpus для каждого SearchRequest, где corpus может быть UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS или VIDEO. Вы можете сделать это очень просто, добавив enum в определение вашего сообщения с константой для каждого возможного значения.

В следующем примере мы добавили enum с именем Corpus со всеми возможными значениями и поле типа Corpus:

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
  optional Corpus corpus = 4;
}

Значение перечисления по умолчанию

Значение по умолчанию для поля SearchRequest.corpusCORPUS_UNSPECIFIED, потому что это первое значение, определенное в перечислении.

Настоятельно рекомендуется определять первое значение каждого enum как ENUM_TYPE_NAME_UNSPECIFIED = 0; или ENUM_TYPE_NAME_UNKNOWN = 0;. Это из-за способа, которым proto2 обрабатывает неизвестные значения для полей enum.

Также рекомендуется, чтобы это первое, значение по умолчанию не имело семантического значения, кроме «это значение не было указано».

Значение по умолчанию для поля перечисления, такого как поле SearchRequest.corpus, может быть явно переопределено следующим образом:

  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

Псевдонимы значений перечисления

Вы можете определить псевдонимы, назначая одно и то же значение разным константам перечисления. Чтобы сделать это, вам нужно установить опцию allow_alias в true. В противном случае компилятор protocol buffer генерирует предупреждение, когда псевдонимы найдены. Хотя все значения псевдонимов действительны для сериализации, только первое значение используется при десериализации.

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Раскомментирование этой строки вызовет предупреждение.
  ENAA_FINISHED = 2;
}

Константы перечислителя должны находиться в диапазоне 32-битного целого числа. Поскольку значения enum используют varint кодирование в передаче, отрицательные значения неэффективны и поэтому не рекомендуются. Вы можете определять enum внутри определения сообщения, как в предыдущем примере, или снаружи – эти enum могут быть повторно использованы в любом определении сообщения в вашем файле .proto. Вы также можете использовать тип enum, объявленный в одном сообщении, в качестве типа поля в другом сообщении, используя синтаксис _MessageType_._EnumType_.

Когда вы запускаете компилятор protocol buffer на .proto, который использует enum, сгенерированный код будет иметь соответствующий enum для Java, Kotlin или C++, или специальный класс EnumDescriptor для Python, который используется для создания набора символических констант с целочисленными значениями в классе, сгенерированном во время выполнения.

{{% alert title="Важно" color="warning" %}} Сгенерированный код может быть подвержен языко-специфичным ограничениям на количество перечислителей (несколько тысяч для одного языка). Ознакомьтесь с ограничениями для языков, которые вы планируете использовать. {{% /alert %}}

{{% alert title="Важно" color="warning" %}} Для информации о том, как перечисления должны работать, в противовес тому, как они сейчас работают в разных языках, см. Поведение Enum. {{% /alert %}}

Удаление значений enum является критическим изменением для сохраненных protos. Вместо удаления значения пометьте значение ключевым словом reserved, чтобы предотвратить генерацию кода для значения enum, или сохраните значение, но укажите, что оно будет удалено позже, используя опцию поля deprecated:

enum PhoneType {
  PHONE_TYPE_UNSPECIFIED = 0;
  PHONE_TYPE_MOBILE = 1;
  PHONE_TYPE_HOME = 2;
  PHONE_TYPE_WORK = 3 [deprecated = true];
  reserved 4,5;
}

Для получения дополнительной информации о том, как работать с enum сообщений в ваших приложениях, см. руководство по сгенерированному коду для выбранного вами языка.

Зарезервированные значения

Если вы обновляете тип перечисления, полностью удаляя запись перечисления, или комментируя ее, будущие пользователи могут повторно использовать числовое значение при внесении своих собственных обновлений в тип. Это может вызвать серьезные проблемы, если они позже загрузят старые экземпляры того же .proto, включая повреждение данных, ошибки конфиденциальности и т.д. Один из способов убедиться, что этого не произойдет, — указать, что числовые значения (и/или имена, которые также могут вызывать проблемы для JSON сериализации) ваших удаленных записей reserved. Компилятор protocol buffer будет жаловаться, если любые будущие пользователи попытаются использовать эти идентификаторы. Вы можете указать, что ваш зарезервированный диапазон числовых значений доходит до максимально возможного значения, используя ключевое слово max.

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

Обратите внимание, что вы не можете смешивать имена полей и числовые значения в одном операторе reserved.

Использование других типов сообщений

Вы можете использовать другие типы сообщений в качестве типов полей. Например, допустим, вы хотели включить сообщения Result в каждое сообщение SearchResponse – чтобы сделать это, вы можете определить тип сообщения Result в том же .proto и затем указать поле типа Result в SearchResponse:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  optional string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

Импорт определений

В предыдущем примере тип сообщения Result определен в том же файле, что и SearchResponse – что, если тип сообщения, который вы хотите использовать в качестве типа поля, уже определен в другом файле .proto?

Вы можете использовать определения из других файлов .proto, импортируя их. Чтобы импортировать определения другого .proto, вы добавляете оператор импорта в начало вашего файла:

import "myproject/other_protos.proto";

По умолчанию вы можете использовать определения только из непосредственно импортированных файлов .proto. Однако иногда вам может потребоваться переместить файл .proto в новое место. Вместо прямого перемещения файла .proto и обновления всех мест вызова в одном изменении, вы можете поместить файл-заполнитель .proto в старое местоположение для перенаправления всех импортов в новое местоположение с использованием понятия import public.

Обратите внимание, что функциональность публичного импорта недоступна в Java, Kotlin, TypeScript, JavaScript, GCL, а также в целях C++, которые используют статическую рефлексию protobuf.

Зависимости import public могут быть транзитивно использованы любым кодом, импортирующим proto, содержащий оператор import public. Например:

// new.proto
// Все определения перемещены сюда
// old.proto
// Это proto, которое импортируют все клиенты.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// Вы используете определения из old.proto и new.proto, но не other.proto

Компилятор протокола ищет импортированные файлы в наборе каталогов, указанных в командной строке компилятора протокола с использованием флага -I/--proto_path. Если флаг не был задан, он ищет в каталоге, из которого был вызван компилятор. В общем, вам следует установить флаг --proto_path в корень вашего проекта и использовать полностью квалифицированные имена для всех импортов.

Использование типов сообщений proto3

Возможно импортировать proto3 и edition 2023 типы сообщений и использовать их в ваших proto2 сообщениях, и наоборот. Однако proto2 enums не могут использоваться напрямую в синтаксисе proto3 (это нормально, если импортированное proto2 сообщение использует их).

Вложенные типы

Вы можете определять и использовать типы сообщений внутри других типов сообщений, как в следующем примере – здесь сообщение Result определено внутри сообщения SearchResponse:

message SearchResponse {
  message Result {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

Если вы хотите повторно использовать этот тип сообщения вне его родительского типа сообщения, вы ссылаетесь на него как _Parent_._Type_:

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

Вы можете вкладывать сообщения настолько глубоко, насколько хотите. В примере ниже обратите внимание, что два вложенных типа с именем Inner полностью независимы, поскольку они определены внутри разных сообщений:

message Outer {       // Уровень 0
  message MiddleAA {  // Уровень 1
    message Inner {   // Уровень 2
      optional int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Уровень 1
    message Inner {   // Уровень 2
      optional int32  ival = 1;
      optional bool   booly = 2;
    }
  }
}

Группы

Обратите внимание, что функция групп устарела и не должна использоваться при создании новых типов сообщений. Используйте вложенные типы сообщений вместо этого.

Группы — это другой способ вложения информации в ваши определения сообщений. Например, другой способ указать SearchResponse, содержащий ряд Result, выглядит следующим образом:

message SearchResponse {
  repeated group Result = 1 {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
}

Группа просто объединяет вложенный тип сообщения и поле в одно объявление. В вашем коде вы можете обращаться с этим сообщением так, как если бы оно имело поле типа Result с именем result (последнее имя преобразуется в нижний регистр чтобы оно не конфликтовало с первым). Следовательно, этот пример точно эквивалентен предыдущему SearchResponse, за исключением того, что сообщение имеет другой формат передачи.

Обновление типа сообщения

Если существующий тип сообщения больше не удовлетворяет всем вашим потребностям – например, вы хотите, чтобы формат сообщения имел дополнительное поле – но вы все еще хотите использовать код, созданный со старым форматом, не волнуйтесь! Это очень просто обновить типы сообщений, не нарушая любой из вашего существующего кода, когда вы используете бинарный формат передачи.

{{% alert title="Примечание" color="note" %}} Если вы используете ProtoJSON или текстовый формат proto для хранения ваших сообщений protocol buffer, изменения, которые вы можете внести в ваше proto определение, отличаются. Безопасные изменения формата передачи ProtoJSON описаны здесь. {{% /alert %}}

Проверьте Лучшие практики Proto и следующие правила:

Бинарно небезопасные изменения формата передачи

Небезопасные изменения формата передачи – это изменения схемы, которые сломаются, если вы используете разбор данных, которые были сериализованы с использованием старой схемы, с парсером, который использует новую схему (или наоборот). Делайте небезопасные изменения формата передачи только если вы знаете, что все сериализаторы и десериализаторы данных используют новую схему.

  • Изменение номеров полей для любого существующего поля небезопасно.
    • Изменение номера поля эквивалентно удалению поля и добавлению нового поля с тем же типом. Если вы хотите перенумеровать поле, см. инструкции для удаления поля.
  • Перемещение полей в существующий oneof небезопасно.

Бинарно безопасные изменения формата передачи

Безопасные изменения формата передачи – это те, при которых полностью безопасно развивать схему таким образом без риска потери данных или новых ошибок разбора.

Обратите внимание, что любые безопасные изменения формата передачи могут быть критическим изменением для кода приложения на данном языке. Например, добавление значения в предсуществующее enum будет критическим изменением компиляции для любого кода с исчерпывающим switch по этому enum. По этой причине Google может избегать внесения некоторых из этих типов изменений в публичные сообщения: AIPs содержат рекомендации о том, какие из этих изменений безопасно делать там.

  • Добавление новых полей безопасно.
    • Если вы добавляете новые поля, любые сообщения, сериализованные кодом, использующим ваш «старый» формат сообщения, все еще могут быть разобраны вашим новым сгенерированным кодом. Вам следует иметь в виду значения по умолчанию для этих элементов, чтобы новый код мог правильно взаимодействовать с сообщениями, сгенерированными старым кодом. Аналогично, сообщения, созданные вашим новым кодом, могут быть разобраны вашим старым кодом: старые бинарные файлы просто игнорируют новое поле при разборе. См. раздел Неизвестные поля для деталей.
  • Удаление полей безопасно.
    • Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения. Вы можете захотеть переименовать поле вместо этого, возможно, добавив префикс "OBSOLETE_", или сделать номер поля зарезервированным, чтобы будущие пользователи вашего .proto не могли случайно повторно использовать номер.
  • Добавление дополнительных значений в enum безопасно.
  • Изменение одного поля с явным присутствием или расширения на член нового oneof безопасно.
  • Изменение oneof, который содержит только одно поле, на поле с явным присутствием безопасно.
  • Изменение поля на расширение с тем же номером и типом безопасно.

Бинарно совместимые изменения формата передачи (Условно Безопасные)

В отличие от безопасных изменений формата передачи, совместимые означают, что одни и те же данные могут быть разобраны как до, так и после данного изменения. Однако разбор данных может быть потерейным при такой форме изменения. Например, изменение int32 на int64 является совместимым изменением, но если значение больше INT32_MAX записано, клиент, который читает его как int32, отбросит старшие биты числа.

Вы можете вносить совместимые изменения в вашу схему только если вы тщательно управляете развертыванием в вашей системе. Например, вы можете изменить int32 на int64, но обеспечить, чтобы вы продолжали записывать только допустимые значения int32 до тех пор, пока новая схема не будет развернута на всех конечных точках, и затем начать записывать большие значения после этого.

Если ваша схема опубликована за пределами вашей организации, вам обычно не следует вносить совместимые изменения формата передачи, так как вы не можете управлять развертыванием новой схемы, чтобы знать, когда различные диапазоны значений могут быть безопасны для использования.

  • int32, uint32, int64, uint64 и bool все совместимы.
    • Если число разбирается из передачи, которое не помещается в соответствующий тип, вы получите тот же эффект, как если бы вы привели число к этому типу в C++ (например, если 64-битное число читается как int32, оно будет усечено до 32 бит).
  • sint32 и sint64 совместимы друг с другом, но не совместимы с другими целочисленными типами.
    • Если записанное значение было между INT_MIN и INT_MAX включительно, оно будет разобрано как то же значение с любым типом. Если значение sint64 было записано вне этого диапазона и разобрано как sint32, varint усекается до 32 бит, а затем происходит zigzag декодирование (что вызовет наблюдение другого значения).
  • string и bytes совместимы, пока байты являются допустимым UTF-8.
  • Встроенные сообщения совместимы с bytes, если байты содержат закодированный экземпляр сообщения.
  • fixed32 совместим с sfixed32, и fixed64 с sfixed64.
  • Для string, bytes и полей сообщений, одиночное совместимо с repeated.
    • При получении сериализованных данных повторяющегося поля в качестве входных данных, клиенты, которые ожидают это поле одиночным, возьмут последнее входное значение, если это поле примитивного типа, или объединят все входные элементы, если это тип сообщения. Обратите внимание, что это не вообще безопасно для числовых типов, включая bools и enums. Повторяющиеся поля числовых типов сериализуются в упакованном формате по умолчанию, который не будет правильно разобран, когда ожидается одиночное поле.
  • enum совместим с int32, uint32, int64 и uint64
    • Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение десериализуется: например, нераспознанные значения proto3 enum будут сохранены в сообщении, но то, как это представлено, когда сообщение десериализуется, зависит от языка.
  • Изменение поля между map<K, V> и соответствующим repeated полем сообщения бинарно совместимо (см. Карты ниже, для макета сообщения и других ограничений).
    • Однако безопасность изменения зависит от приложения: при десериализации и повторной сериализации сообщения клиенты, использующие определение repeated поля, произведут семантически идентичный результат; однако, клиенты, использующие определение map поля, могут переупорядочить записи и отбросить записи с дублирующимися ключами.

Неизвестные поля

Неизвестные поля – это правильно сформированные сериализованные данные protocol buffer, представляющие поля, которые парсер не распознает. Например, когда старый бинарный файл разбирает данные, отправленные новым бинарным файлом с новыми полями, эти новые поля становятся неизвестными полями в старом бинарном файле.

Первоначально, proto3 сообщения всегда отбрасывали неизвестные поля во время разбора, но в версии 3.5 мы reintroduced сохранение неизвестных полей, чтобы соответствовать поведению proto2. В версиях 3.5 и позже, неизвестные поля сохраняются во время разбора и включаются в сериализованный вывод.

Сохранение неизвестных полей

Некоторые действия могут привести к потере неизвестных полей. Например, если вы сделаете одно из следующего, неизвестные поля теряются:

  • Сериализуете proto в JSON.
  • Итерируетесь по всем полям в сообщении, чтобы заполнить новое сообщение.

Чтобы избежать потери неизвестных полей, делайте следующее:

  • Используйте бинарный формат; избегайте использования текстовых форматов для обмена данными.
  • Используйте API, ориентированные на сообщения, такие как CopyFrom() и MergeFrom(), для копирования данных, а не копирования поле за полем.

TextFormat – это особый случай. Сериализация в TextFormat печатает неизвестные поля, используя их номера полей. Но разбор данных TextFormat обратно в бинарный proto завершается неудачей, если есть записи, которые используют номера полей.

Расширения

Расширение – это поле, определенное вне своего контейнерного сообщения; обычно в файле .proto, отдельном от файла .proto контейнерного сообщения.

Зачем использовать расширения?

Есть две основные причины использовать расширения:

  • Файл .proto контейнерного сообщения будет иметь меньше импортов/зависимостей. Это может улучшить время сборки, разорвать циклические зависимости и иным образом способствовать слабой связанности. Расширения очень хороши для этого.
  • Позволяют системам прикреплять данные к контейнерному сообщению с минимальной зависимостью и координацией. Расширения не являются отличным решением для этого из-за ограниченного пространства номеров полей и Последствий повторного использования номеров полей. Если ваш случай использования требует очень низкой координации для большого количества расширений, рассмотрите использование типа сообщения Any вместо этого.

Пример расширения

Давайте рассмотрим пример расширения:

// файл kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// Это расширение позволяет видео с котятами в сообщении media.UserContent.
extend media.UserContent {
  // Video - это сообщение, импортированное из kittens/video.proto
  repeated Video kitten_videos = 126;
}

Обратите внимание, что файл, определяющий расширение (kittens/video_ext.proto), импортирует файл контейнерного сообщения (media/user_content.proto).

Контейнерное сообщение должно зарезервировать подмножество своих номеров полей для расширений.

// файл media/user_content.proto

package media;

// Контейнерное сообщение для хранения вещей, созданных пользователем.
message UserContent {
  // Установите verification в `DECLARATION`, чтобы требовать объявления расширений для всех
  // расширений в этом диапазоне.
  extensions 100 to 199 [verification = DECLARATION];
}

Файл контейнерного сообщения (media/user_content.proto) определяет сообщение UserContent, которое резервирует номера полей [100 до 199] для расширений. Рекомендуется установить verification = DECLARATION для диапазона, чтобы требовать объявления для всех его расширений.

Когда новое расширение (kittens/video_ext.proto) добавляется, соответствующее объявление должно быть добавлено в UserContent и verification должно быть удалено.

// Контейнерное сообщение для хранения вещей, созданных пользователем.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    }
  ];
}

UserContent объявляет, что номер поля 126 будет использоваться repeated полем расширения с полностью квалифицированным именем .kittens.kitten_videos и fully-qualified типом .kittens.Video. Чтобы узнать больше об объявлениях расширений, см. Объявления расширений.

Обратите внимание, что файл контейнерного сообщения (media/user_content.proto) не импортирует определение расширения kitten_video (kittens/video_ext.proto)

Нет разницы в кодировании формата передачи полей расширений по сравнению со стандартным полем с тем же номером поля, типом и мощностью. Следовательно, безопасно перемещать стандартное поле из его контейнера, чтобы быть расширением, или перемещать поле расширения в его контейнерное сообщение как стандартное поле, при условии, что номер поля, тип и мощность остаются постоянными.

Однако, поскольку расширения определены вне контейнерного сообщения, никакие специализированные методы доступа не генерируются для получения и установки конкретных полей расширений. Для нашего примера компилятор protobuf не сгенерирует методы доступа AddKittenVideos() или GetKittenVideos(). Вместо этого доступ к расширениям осуществляется через параметризованные функции, такие как: HasExtension(), ClearExtension(), GetExtension(), MutableExtension() и AddExtension().

В C++ это будет выглядеть примерно так:

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

Определение диапазонов расширений

Если вы являетесь владельцем контейнерного сообщения, вам нужно будет определить диапазон расширений для расширений вашего сообщения.

Номера полей, выделенные для полей расширений, не могут быть повторно использованы для стандартных полей.

Безопасно расширять диапазон расширений после его определения. Хорошим значением по умолчанию является выделение 1000 относительно маленьких чисел и плотное заполнение этого пространства с использованием объявлений расширений:

message ModernExtendableMessage {
  // Все расширения в этом диапазоне должны использовать объявления расширений.
  extensions 1000 to 2000 [verification = DECLARATION];
}

При добавлении диапазона для объявлений расширений до фактических расширений, вы должны добавить verification = DECLARATION, чтобы обеспечить использование объявлений для этого нового диапазона. Этот заполнитель может быть удален, как только будет добавлено фактическое объявление.

Безопасно разделить существующий диапазон расширений на отдельные диапазоны, которые покрывают тот же общий диапазон. Это может быть необходимо для миграции унаследованного типа сообщения на Объявления расширений. Например, до миграции диапазон может быть определен как:

message LegacyMessage {
  extensions 1000 to max;
}

И после миграции (разделения диапазона) это может быть:

message LegacyMessage {
  // Унаследованный диапазон, который использовал непроверенную схему выделения.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Текущий диапазон, который использует объявления расширений.
  extensions 525000000 to max  [verification = DECLARATION];
}

Небезопасно увеличивать начальный номер поля или уменьшать конечный номер поля для перемещения или сокращения диапазона расширений. Эти изменения могут сделать недействительным существующее расширение.

Предпочитайте использовать номера полей от 1 до 15 для стандартных полей, которые заполняются в большинстве экземпляров вашего proto. Не рекомендуется использовать эти номера для расширений.

Если ваше соглашение о нумерации может включать расширения с очень большими номерами полей, вы можете указать, что ваш диапазон расширений доходит до максимально возможного номера поля, используя ключевое слово max:

message Foo {
  extensions 1000 to max;
}

max это 229 - 1, или 536,870,911.

Выбор номеров расширений

Расширения – это просто поля, которые могут быть указаны вне их контейнерных сообщений. Все те же правила для Назначения номеров полей применяются к номерам полей расширений. Те же Последствия повторного использования номеров полей также применяются к повторному использованию номеров полей расширений.

Выбор уникальных номеров полей расширений прост, если контейнерное сообщение использует объявления расширений. При определении нового расширения выберите наименьший номер поля выше всех других объявлений из самого высокого диапазона расширений, определенного в контейнерном сообщении. Например, если контейнерное сообщение определено так:

message Container {
  // Унаследованный диапазон, который использовал непроверенную схему выделения
  extensions 1000 to 524999999;
  // Текущий диапазон, который использует объявления расширений. (самый высокий диапазон расширений)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 - это наименьший номер поля выше всех других объявлений
  ];
}

Следующее расширение Container должно добавить новое объявление с номером 525000002.

Непроверенное выделение номеров расширений (не рекомендуется)

Владелец контейнерного сообщения может отказаться от объявлений расширений в пользу своей собственной непроверенной стратегии выделения номеров расширений.

Непроверенная схема выделения использует механизм, внешний по отношению к экосистеме protobuf, для выделения номеров полей расширений в выбранном диапазоне расширений. Одним из примеров может быть использование номера коммита монорепозитория. Эта система «непроверена» с точки зрения компилятора protobuf, поскольку нет способа проверить, что расширение использует правильно полученный номер поля расширения.

Преимущество непроверенной системы над проверенной системой, такой как объявления расширений, – это возможность определить расширение без координации с владельцем контейнерного сообщения.

Недостатком непроверенной системы является то, что компилятор protobuf не может защитить участников от повторного использования номеров полей расширений.

Непроверенные стратегии выделения номеров полей расширений не рекомендуются, потому что Последствия повторного использования номеров полей ложатся на всех расширителей сообщения (а не только на разработчика, который не следовал рекомендациям). Если ваш случай использования требует очень низкой координации, рассмотрите использование сообщения Any вместо этого.

Непроверенные стратегии выделения номеров полей расширений ограничены диапазоном от 1 до 524,999,999. Номера полей 525,000,000 и выше могут использоваться только с объявлениями расширений.

Указание типов расширений

Расширения могут быть любого типа поля, кроме oneof и map.

Вложенные расширения (не рекомендуется)

Вы можете объявлять расширения в области видимости другого сообщения:

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    optional int32 likes_count = 111;
  }
  ...
}

В этом случае код C++ для доступа к этому расширению:

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

Другими словами, единственный эффект заключается в том, что likes_count определен в области видимости puppies.Photo.

Это является частым источником путаницы: Объявление блока extend, вложенного внутрь типа сообщения, не подразумевает никаких отношений между внешним типом и расширяемым типом. В частности, предыдущий пример не означает, что Photo является каким-либо подклассом UserProfile. Все это означает, что символ likes_count объявлен внутри области видимости Photo; это просто статический член.

Распространенным шаблоном является определение расширений внутри области видимости типа поля расширения – например, вот расширение для media.UserContent типа puppies.Photo, где расширение определено как часть Photo:

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    optional Photo puppy_photo = 127;
  }
  ...
}

Однако нет требования, чтобы расширение с типом сообщения было определено внутри этого типа. Вы также можете использовать стандартный шаблон определения:

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// Это может быть даже в другом файле.
extend media.UserContent {
  optional Photo puppy_photo = 127;
}

Этот стандартный (файловый уровень) синтаксис предпочтительнее, чтобы избежать путаницы. Вложенный синтаксис часто ошибочно принимается за наследование пользователями, которые еще не знакомы с расширениями.

Any

Тип сообщения Any позволяет вам использовать сообщения как встроенные типы без наличия их определения .proto. Any содержит произвольное сериализованное сообщение как bytes, вместе с URL, который действует как глобально уникальный идентификатор и разрешается к типу этого сообщения. Чтобы использовать тип Any, вам нужно импортировать google/protobuf/any.proto.

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

URL типа по умолчанию для данного типа сообщения: type.googleapis.com/_packagename_._messagename_.

Различные языковые реализации будут поддерживать вспомогательные библиотеки времени выполнения для упаковки и распаковки значений Any типобезопасным образом – например, в Java тип Any будет иметь специальные методы доступа pack() и unpack(), в то время как в C++ есть методы PackFrom() и UnpackTo():

// Хранение произвольного типа сообщения в Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Чтение произвольного сообщения из Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... обработка network_error ...
  }
}

Если вы хотите ограничить содержащиеся сообщения небольшим количеством типов и требовать разрешение перед добавлением новых типов в список, рассмотрите использование расширений с объявлениями расширений вместо типов сообщений Any.

Oneof

Если у вас есть сообщение со многими опциональными полями и где не более одного поля будет установлено одновременно, вы можете обеспечить это поведение и сэкономить память, используя функцию oneof.

Поля oneof похожи на опциональные поля, за исключением того, что все поля в oneof разделяют память, и не более одного поля может быть установлено одновременно. Установка любого члена oneof автоматически очищает всех других членов. Вы можете проверить, какое значение в oneof установлено (если есть), используя специальный метод case() или WhichOneof(), в зависимости от выбранного вами языка.

Обратите внимание, что если установлено несколько значений, последнее установленное значение, определенное порядком в proto, перезапишет все предыдущие.

Номера полей для полей oneof должны быть уникальными в пределах заключающего сообщения.

Использование Oneof

Чтобы определить oneof в вашем .proto, вы используете ключевое слово oneof, за которым следует ваше имя oneof, в этом случае test_oneof:

message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

Затем вы добавляете ваши поля oneof в определение oneof. Вы можете добавлять поля любого типа, кроме полей map, но вы не можете использовать ключевые слова required, optional или repeated. Если вам нужно добавить повторяющееся поле в oneof, вы можете использовать сообщение, содержащее повторяющееся поле.

В вашем сгенерированном коде поля oneof имеют те же геттеры и сеттеры, что и обычные optional поля. Вы также получаете специальный метод для проверки, какое значение (если есть) в oneof установлено. Вы можете узнать больше об API oneof для вашего выбранного языка в соответствующем справочнике по API.

Особенности Oneof

  • Установка поля oneof автоматически очистит всех других членов oneof. Так что если вы установите несколько полей oneof, только последнее поле, которое вы установили, все еще будет иметь значение.

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    // Вызов mutable_sub_message() очистит поле name и установит
    // sub_message в новый экземпляр SubMessage без установленных полей.
    message.mutable_sub_message();
    CHECK(!message.has_name());
    
  • Если парсер встречает несколько членов одного и того же oneof в передаче, только последний увиденный член используется в разобранном сообщении. При разборе данных в передаче, начиная с начала байтов, оцените следующее значение и примените следующие правила разбора:

    • Сначала проверьте, установлено ли другое поле в том же oneof в настоящее время, и если да, очистите его.

    • Затем примените содержимое, как если бы поле не было в oneof:

      • Примитив перезапишет любое уже установленное значение
      • Сообщение объединится с любым уже установленным значением
  • Расширения не поддерживаются для oneof.

  • Oneof не может быть repeated.

  • Reflection API работают для полей oneof.

  • Если вы установите поле oneof в значение по умолчанию (например, установите поле oneof int32 в 0), «случай» этого поля oneof будет установлен, и значение будет сериализовано в передаче.

  • Если вы используете C++, убедитесь, что ваш код не вызывает сбоев памяти. Следующий пример кода приведет к сбою, потому что sub_message уже был удален вызовом метода set_name().

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Удалит sub_message
    sub_message->set_...            // Сбой здесь
    
  • Снова в C++, если вы Swap() два сообщения с oneof, каждое сообщение окажется со случаем oneof другого: в примере ниже msg1 будет иметь sub_message и msg2 будет иметь name.

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

Проблемы обратной совместимости

Будьте осторожны при добавлении или удалении полей oneof. Если проверка значения oneof возвращает None/NOT_SET, это может означать, что oneof не был установлен или он был установлен в поле в другой версии oneof. Нет способа отличить разницу, поскольку нет способа узнать, является ли неизвестное поле в передаче членом oneof.

Проблемы повторного использования тегов

  • Перемещение опциональных полей в oneof или из oneof: Вы можете потерять некоторую вашу информацию (некоторые поля будут очищены) после того, как сообщение будет сериализовано и разобрано. Однако, вы можете безопасно переместить одно поле в новый oneof и, возможно, сможете переместить несколько полей, если известно, что только одно когда-либо установлено. См. Обновление типа сообщения для дальнейших деталей.
  • Удаление поля oneof и его возвращение: Это может очистить ваше текуще установленное поле oneof после сериализации и разбора сообщения.
  • Разделение или объединение oneof: Это имеет проблемы, похожие на перемещение optional полей.

Карты

Если вы хотите создать ассоциативную карту как часть вашего определения данных, protocol buffers предоставляет удобный сокращенный синтаксис:

map<key_type, value_type> map_field = N;

...где key_type может быть любым целочисленным или строковым типом (так, любой скалярный тип, кроме типов с плавающей точкой и bytes). Обратите внимание, что ни enum, ни proto сообщения не допустимы для key_type. value_type может быть любым типом, кроме другой карты.

Итак, например, если вы хотите создать карту проектов, где каждое сообщение Project связано со строковым ключом, вы можете определить это так:

map<string, Project> projects = 3;

Особенности карт

  • Расширения не поддерживаются для карт.
  • Карты не могут быть repeated, optional или required.
  • Порядок в формате передачи и порядок итерации карты значений не определен, поэтому вы не можете полагаться на то, что элементы вашей карты находятся в определенном порядке.
  • При генерации текстового формата для .proto карты сортируются по ключу. Числовые ключи сортируются численно.
  • При разборе из передачи или при слиянии, если есть дублирующиеся ключи карты, используется последний увиденный ключ. При разборе карты из текстового формата разбор может завершиться неудачей, если есть дублирующиеся ключи.
  • Если вы предоставляете ключ, но не значение для поля карты, поведение при сериализации поля зависит от языка. В C++, Java, Kotlin и Python сериализуется значение по умолчанию для типа, в то время как в других языках ничего не сериализуется.
  • Никакой символ FooEntry не может существовать в той же области видимости, что и карта foo, потому что FooEntry уже используется реализацией карты.

Сгенерированный API карт в настоящее время доступен для всех поддерживаемых языков. Вы можете узнать больше об API карт для вашего выбранного языка в соответствующем справочнике по API.

Обратная совместимость

Синтаксис карт эквивалентен следующему в передаче, поэтому реализации protocol buffers, которые не поддерживают карты, все еще могут обрабатывать ваши данные:

message MapFieldEntry {
  optional key_type key = 1;
  optional value_type value = 2;
}

repeated MapFieldEntry map_field = N;

Любая реализация protocol buffers, которая поддерживает карты, должна и производить, и принимать данные, которые могут быть приняты предыдущим определением.

Пакеты

Вы можете добавить необязательный спецификатор package в файл .proto, чтобы предотвратить конфликты имен между типами сообщений протокола.

package foo.bar;
message Open { ... }

Затем вы можете использовать спецификатор пакета при определении полей вашего типа сообщения:

message Foo {
  ...
  optional foo.bar.Open open = 1;
  ...
}

Способ, которым спецификатор пакета влияет на сгенерированный код, зависит от вашего выбранного языка:

  • В C++ сгенерированные классы обернуты внутри пространства имен C++. Например, Open будет в пространстве имен foo::bar.
  • В Java и Kotlin пакет используется как Java пакет, если только вы явно не предоставите option java_package в вашем файле .proto.
  • В Python директива package игнорируется, поскольку модули Python организованы в соответствии с их расположением в файловой системе.
  • В Go директива package игнорируется, и сгенерированный файл .pb.go находится в пакете, названном в соответствии с соответствующим правилом Bazel go_proto_library. Для проектов с открытым исходным кодом вы должны предоставить либо опцию go_package, либо установить флаг Bazel -M.
  • В Ruby сгенерированные классы обернуты внутри вложенных пространств имен Ruby, преобразованных в требуемый стиль капитализации Ruby (первая буква заглавная; если первый символ не буква, добавляется PB_). Например, Open будет в пространстве имен Foo::Bar.
  • В PHP пакет используется как пространство имен после преобразования в PascalCase, если вы явно не предоставите option php_namespace в вашем файле .proto. Например, Open будет в пространстве имен Foo\Bar.
  • В C# пакет используется как пространство имен после преобразования в PascalCase, если вы явно не предоставите option csharp_namespace в вашем файле .proto. Например, Open будет в пространстве имен Foo.Bar.

Обратите внимание, что даже когда директива package не влияет напрямую на сгенерированный код, например в Python, все равно настоятельно рекомендуется указывать пакет для файла .proto, так как в противном случае это может привести к конфликтам имен в дескрипторах и сделать proto непереносимым для других языков.

Пакеты и разрешение имен

Разрешение имен типов в языке protocol buffer работает как в C++: сначала ищется самая внутренняя область, затем следующая по внутренности и так далее, причем каждый пакет считается «внутренним» по отношению к своему родительскому пакету. Начальная '.' (например, .foo.bar.Baz) означает начать с самой внешней области вместо этого.

Компилятор protocol buffer разрешает все имена типов, разбирая импортированные файлы .proto. Генератор кода для каждого языка знает, как ссылаться на каждый тип на этом языке, даже если он имеет разные правила области видимости.

Определение сервисов

Если вы хотите использовать ваши типы сообщений с системой RPC (Удаленный вызов процедур), вы можете определить интерфейс RPC сервиса в файле .proto, и компилятор protocol buffer сгенерирует код интерфейса сервиса и заглушки на вашем выбранном языке. Итак, например, если вы хотите определить RPC сервис с методом, который принимает ваш SearchRequest и возвращает SearchResponse, вы можете определить это в вашем файле .proto следующим образом:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

По умолчанию компилятор протокола затем сгенерирует абстрактный интерфейс called SearchService и соответствующую «заглушку» (stub) реализацию. Заглушка пересылает все вызовы на RpcChannel, который, в свою очередь, является абстрактным интерфейсом который вы должны определить сами в терминах вашей собственной RPC системы. Например, вы можете реализовать RpcChannel, который сериализует сообщение и отправляет его на сервер через HTTP. Другими словами, сгенерированная заглушка предоставляет типобезопасный интерфейс для выполнения RPC вызовов на основе protocol buffer, не привязывая вас к какой-либо конкретной RPC реализации. Так, в C++, вы можете получить код, похожий на этот:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // Вы предоставляете классы MyRpcChannel и MyRpcController, которые реализуют
  // абстрактные интерфейсы protobuf::RpcChannel и protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // Компилятор протокола генерирует класс SearchService на основе
  // определения, данного ранее.
  service = new SearchService::Stub(channel);

  // Настройка запроса.
  request.set_query("protocol buffers");

  // Выполнение RPC.
  service->Search(controller, &request, &response,
                  protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

Все сервисные классы также реализуют интерфейс Service, который предоставляет способ вызывать конкретные методы, не зная имени метода или его входных и выходных типов во время компиляции. На стороне сервера это может быть использовано для реализации RPC сервера, в который вы могли бы регистрировать сервисы.

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // Вы предоставляете класс MyRpcServer. Он не должен реализовывать какой-либо
  // конкретный интерфейс; это просто пример.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

Если вы не хотите подключать вашу собственную существующую RPC систему, вы можете использовать gRPC: независимую от языка и платформы открытую RPC систему, разработанную в Google. gRPC особенно хорошо работает с protocol buffers и позволяет вам генерировать соответствующий RPC код непосредственно из ваших файлов .proto с помощью специального плагина компилятора protocol buffer. Однако, поскольку существуют потенциальные проблемы совместимости между клиентами и серверами, сгенерированными с proto2 и proto3, мы рекомендуем вам использовать proto3 или edition 2023 для определения gRPC сервисов. Вы можете узнать больше о синтаксисе proto3 в Руководстве по языку Proto3 и о edition 2023 в Руководстве по языку Edition 2023.

В дополнение к gRPC, также существует ряд сторонних проектов по разработке RPC реализаций для Protocol Buffers. Для списка ссылок на проекты, о которых мы знаем, см. вики-страницу сторонних дополнений.

Отображение JSON

Стандартный бинарный формат передачи protobuf является предпочтительным форматом сериализации для общения между двумя системами, которые используют protobuf. Для общения с системами, которые используют JSON, а не формат передачи protobuf, Protobuf поддерживает каноническое кодирование в JSON.

Опции

Отдельные объявления в файле .proto могут быть аннотированы рядом опций. Опции не меняют общее значение объявления, но могут влиять на способ его обработки в определенном контексте. Полный список доступных опций определен в /google/protobuf/descriptor.proto.

Некоторые опции являются опциями уровня файла, что означает, что они должны быть записаны на верхнем уровне, не внутри любого сообщения, enum или определения сервиса. Некоторые опции являются опциями уровня сообщения, что означает, что они должны быть записаны внутри определений сообщений. Некоторые опции являются опциями уровня поля, что означает, что они должны быть записаны внутри определений полей. Опции также могут быть записаны на типах перечислений, значениях перечислений, полях oneof, типах сервисов и методах сервисов; однако, в настоящее время нет полезных опций для любого из этих.

Вот несколько наиболее часто используемых опций:

  • java_package (опция файла): Пакет, который вы хотите использовать для ваших сгенерированных Java/Kotlin классов. Если явная опция java_package не задана в файле .proto, то по умолчанию будет использоваться proto пакет (указанный с помощью ключевого слова "package" в файле .proto). Однако proto пакеты обычно не являются хорошими Java пакетами, поскольку proto пакеты не ожидаются начинающимися с обратных доменных имен. Если не генерируется Java или Kotlin код, эта опция не имеет эффекта.

    option java_package = "com.example.foo";
    
  • java_outer_classname (опция файла): Имя класса (и, следовательно, имя файла) для класса-обертки Java, который вы хотите сгенерировать. Если явный java_outer_classname не указан в файле .proto, имя класса будет сконструировано путем преобразования имени файла .proto в camel-case (так foo_bar.proto становится FooBar.java). Если опция java_multiple_files отключена, то все другие классы/enums/etc., сгенерированные для файла .proto, будут сгенерированы внутри этого внешнего класса-обертки Java как вложенные классы/enums/etc. Если не генерируется Java код, эта опция не имеет эффекта.

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files (опция файла): Если false, будет сгенерирован только один файл .java для этого файла .proto, и все Java классы/enums/etc., сгенерированные для сообщений верхнего уровня, сервисов и перечислений, будут вложены внутри внешнего класса (см. java_outer_classname). Если true, отдельные файлы .java будут сгенерированы для каждого из Java классов/enums/etc., сгенерированных для сообщений верхнего уровня, сервисов и перечислений, и класс-обертка Java, сгенерированный для этого файла .proto, не будет содержать никаких вложенных классов/enums/etc. Это логическая опция, которая по умолчанию false. Если не генерируется Java код, эта опция не имеет эффекта.

    option java_multiple_files = true;
    
  • optimize_for (опция файла): Может быть установлена в SPEED, CODE_SIZE или LITE_RUNTIME. Это влияет на генераторы кода C++ и Java (и возможно, сторонние генераторы) следующим образом:

    • SPEED (по умолчанию): Компилятор protocol buffer сгенерирует код для сериализации, разбора и выполнения других общих операций над вашими типами сообщений. Этот код высоко оптимизирован.
    • CODE_SIZE: Компилятор protocol buffer сгенерирует минимальные классы и будет полагаться на общий, основанный на рефлексии код для реализации сериализации, разбора и различных других операций. Сгенерированный код будет таким образом намного меньше, чем с SPEED, но операции будут медленнее. Классы все еще будут реализовывать точно такой же публичный API, как они делают в режиме SPEED. Этот режим наиболее полезен в приложениях, которые содержат очень большое количество файлов .proto и не нуждаются в том, чтобы все они были ослепительно быстрыми.
    • LITE_RUNTIME: Компилятор protocol buffer сгенерирует классы, которые зависят только от «облегченной» библиотеки времени выполнения (libprotobuf-lite вместо libprotobuf). Облегченная среда выполнения намного меньше, чем полная библиотека (примерно на порядок меньше), но опускает определенные функции, такие как дескрипторы и рефлексия. Это особенно полезно для приложений, работающих на ограниченных платформах, таких как мобильные телефоны. Компилятор все еще будет генерировать быстрые реализации всех методов, как он делает в режиме SPEED. Сгенерированные классы будут реализовывать только интерфейс MessageLite в каждом языке, который предоставляет только подмножество методов полного интерфейса Message.
    option optimize_for = CODE_SIZE;
    
  • cc_generic_services, java_generic_services, py_generic_services (опции файла): Универсальные сервисы устарели. Должен ли компилятор protocol buffer генерировать абстрактный сервисный код на основе определений сервисов в C++, Java и Python, соответственно. По legacy причинам, они по умолчанию true. Однако, начиная с версии 2.3.0 (январь 2010), считается предпочтительным, чтобы RPC реализации предоставляли плагины генератора кода для генерации кода, более специфичного для каждой системы, а не полагаться на «абстрактные» сервисы.

    // Этот файл полагается на плагины для генерации сервисного кода.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas (опция файла): Включает выделение арены для C++ сгенерированного кода.

  • objc_class_prefix (опция файла): Устанавливает префикс класса Objective-C, который добавляется ко всем сгенерированным классам и enum Objective-C из этого .proto. Значения по умолчания нет. Вы должны использовать префиксы, которые состоят из 3-5 заглавных символов, как рекомендовано Apple. Обратите внимание, что все 2-буквенные префиксы зарезервированы Apple.

  • message_set_wire_format (опция сообщения): Если установлено в true, сообщение использует другой бинарный формат, предназначенный для совместимости со старым форматом, используемым внутри Google, под названием MessageSet. Пользователи вне Google, вероятно, никогда не будут нуждаться в этой опции. Сообщение должно быть объявлено точно следующим образом:

    message Foo {
      option message_set_wire_format = true;
      extensions 4 to max;
    }
    
  • packed (опция поля): Если установлено в true на повторяющемся поле базового числового типа, это вызывает использование более компактного кодирования. Единственная причина не использовать эту опцию — если вам нужна совместимость с парсерами до версии 2.3.0. Эти старые парсеры игнорировали упакованные данные, когда это не ожидалось. Следовательно, было невозможно изменить существующее поле на упакованный формат без нарушения совместимости передачи. В 2.3.0 и позже, это изменение безопасно, так как парсеры для упаковываемых полей всегда принимают оба формата, но будьте осторожны, если вам приходится иметь дело со старыми программами, использующими старые версии protobuf.

    repeated int32 samples = 4 [packed = true];
    
  • deprecated (опция поля): Если установлено в true, указывает, что поле устарело и не должно использоваться новым кодом. В большинстве языков это не имеет фактического эффекта. В Java это становится аннотацией @Deprecated. Для C++, clang-tidy будет генерировать предупреждения всякий раз, когда используются устаревшие поля. В будущем другие языко-специфичные генераторы кода могут генерировать аннотации устаревания на методах доступа поля, что, в свою очередь, вызовет предупреждение при компиляции кода, который пытается использовать поле. Если поле не используется никем и вы хотите предотвратить использование его новыми пользователями, рассмотрите замену объявления поля на зарезервированное утверждение.

    optional int32 old_field = 6 [deprecated = true];
    

Опции значений перечисления

Опции значений перечисления поддерживаются. Вы можете использовать опцию deprecated для указания, что значение больше не должно использоваться. Вы также можете создавать пользовательские опции, используя расширения.

Следующий пример показывает синтаксис для добавления этих опций:

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

Код C++ для чтения опции string_name может выглядеть примерно так:

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

См. Пользовательские опции, чтобы узнать, как применять пользовательские опции к значениям перечисления и к полям.

Пользовательские опции

Protocol Buffers также позволяет вам определять и использовать ваши собственные опции. Обратите внимание, что это продвинутая функция, которая большинству людей не нужна. Поскольку опции определены сообщениями, определенными в google/protobuf/descriptor.proto (как FileOptions или FieldOptions), определение ваших собственных опций — это просто вопрос расширения этих сообщений. Например:

import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

Здесь мы определили новую опцию уровня сообщения, расширив MessageOptions. Когда мы затем используем опцию, имя опции должно быть заключено в круглые скобки, чтобы указать, что это расширение. Теперь мы можем прочитать значение my_option в C++ примерно так:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

Здесь MyMessage::descriptor()->options() возвращает протокольное сообщение MessageOptions для MyMessage. Чтение пользовательских опций из него — это так же, как чтение любого другого расширения.

Аналогично, в Java мы бы написали:

String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
  .getExtension(MyProtoFile.myOption);

В Python это было бы:

value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
  .Extensions[my_proto_file_pb2.my_option]

Пользовательские опции могут быть определены для каждого вида конструкций в языке Protocol Buffers. Вот пример, который использует каждый вид опции:

import "google/protobuf/descriptor.proto";

extend google.protobuf.FileOptions {
  optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
  optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
  optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
  optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

option (my_file_option) = "Hello world!";

message MyMessage {
  option (my_message_option) = 1234;

  optional int32 foo = 1 [(my_field_option) = 4.5];
  optional string bar = 2;
  oneof qux {
    option (my_oneof_option) = 42;

    string quux = 3;
  }
}

enum MyEnum {
  option (my_enum_option) = true;

  FOO = 1 [(my_enum_value_option) = 321];
  BAR = 2;
}

message RequestType {}
message ResponseType {}

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Примечание: my_method_option имеет тип MyMessage. Мы можем установить каждое поле
    // внутри него, используя отдельную строку "option".
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}

Обратите внимание, что если вы хотите использовать пользовательскую опцию в пакете, отличном от того, в котором она была определена, вы должны добавить префикс имени опции с именем пакета, точно так же, как вы делаете для имен типов. Например:

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
  option (foo.my_option) = "Hello world!";
}

Последнее: Поскольку пользовательские опции являются расширениями, они должны быть назначены номера полей как любое другое поле или расширение. В примерах ранее мы использовали номера полей в диапазоне 50000-99999. Этот диапазон зарезервирован для внутреннего использования внутри отдельных организаций, поэтому вы можете свободно использовать числа в этом диапазоне для внутренних приложений. Однако, если вы планируете использовать пользовательские опции в публичных приложениях, тогда важно, чтобы вы убедились, что ваши номера полей глобально уникальны. Чтобы получить глобально уникальные номера полей, отправьте запрос на добавление записи в глобальный реестр расширений protobuf. Обычно вам нужно только одно число расширения. Вы можете объявить несколько опций с только одним номером расширения, поместив их в под-сообщение:

message FooOptions {
  optional int32 opt1 = 1;
  optional string opt2 = 2;
}

extend google.protobuf.FieldOptions {
  optional FooOptions foo_options = 1234;
}

// использование:
message Bar {
  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
  // альтернативный агрегатный синтаксис (использует TextFormat):
  optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}

Также обратите внимание, что каждый тип опции (уровень файла, уровень сообщения, уровень поля и т.д.) имеет свое собственное пространство номеров, так что, например, вы могли бы объявить расширения FieldOptions и MessageOptions с одним и тем же номером.

Сохранение опций

Опции имеют понятие сохранения (retention), которое контролирует, сохраняется ли опция в сгенерированном коде. Опции имеют сохранение времени выполнения по умолчанию, что означает, что они сохраняются в сгенерированном коде и, таким образом, видны во время выполнения в сгенерированном пуле дескрипторов. Однако вы можете установить retention = RETENTION_SOURCE, чтобы указать, что опция (или поле внутри опции) не должна сохраняться во время выполнения. Это называется сохранением исходного кода.

Сохранение опций – это продвинутая функция, о которой большинству пользователей не нужно беспокоиться, но она может быть полезна, если вы хотите использовать определенные опции без оплаты стоимости размера кода за их сохранение в ваших бинарных файлах. Опции с сохранением исходного кода все еще видны protoc и плагинам protoc, поэтому генераторы кода могут использовать их для настройки своего поведения.

Сохранение может быть установлено непосредственно на опции, вот так:

extend google.protobuf.FileOptions {
  optional int32 source_retention_option = 1234
      [retention = RETENTION_SOURCE];
}

Оно также может быть установлено на простом поле, в этом случае оно вступает в силу только когда это поле появляется внутри опции:

message OptionsMessage {
  optional int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}

Вы можете установить retention = RETENTION_RUNTIME, если хотите, но это не имеет эффекта, поскольку это поведение по умолчанию. Когда поле сообщения помечено RETENTION_SOURCE, все его содержимое отбрасывается; поля внутри него не могут переопределить это, пытаясь установить RETENTION_RUNTIME.

{{% alert title="Примечание" color="note" %}} По состоянию на Protocol Buffers 22.0, поддержка сохранения опций все еще находится в разработке, и только C++ и Java поддерживаются. Go имеет поддержку, начиная с 1.29.0. Поддержка Python завершена, но еще не попала в релиз. {{% /alert %}}

Цели опций

Поля имеют опцию targets, которая контролирует типы сущностей, к которым поле может применяться при использовании в качестве опции. Например, если поле имеет targets = TARGET_TYPE_MESSAGE, то это поле не может быть установлено в пользовательской опции на enum (или любой другой не-сообщенной сущности). Protoc обеспечивает это и будет выдавать ошибку, если есть нарушение ограничений цели.

На первый взгляд, эта функция может показаться ненужной, учитывая, что каждая пользовательская опция является расширением сообщения опций для конкретной сущности, что уже ограничивает опцию этой одной сущностью. Однако цели опций полезны в случае, когда у вас есть общее сообщение опций, применяемое к нескольким типам сущностей, и вы хотите контролировать использование отдельных полей в этом сообщении. Например:

message MyOptions {
  optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                              targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  optional MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  optional MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  optional MyOptions enum_options = 50000;
}

// OK: это поле разрешено в опциях файла
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: это поле разрешено в опциях сообщения и enum
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Ошибка: file_only_option не может быть установлен на enum.
  option (enum_options).file_only_option = "xyz";
}

Генерация ваших классов

Чтобы сгенерировать Java, Kotlin, Python, C++, Go, Ruby, Objective-C или C# код, который вам нужен для работы с типами сообщений, определенными в файле .proto, вам нужно запустить компилятор protocol buffer protoc на файле .proto. Если вы не установили компилятор, загрузите пакет и следуйте инструкциям в README. Для Go вам также нужно установить специальный плагин генератора кода для компилятора; вы можете найти его и инструкции по установке в репозитории golang/protobuf на GitHub.

Компилятор протокола вызывается следующим образом:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH указывает каталог, в котором нужно искать файлы .proto при разрешении директив import. Если опущено, используется текущий каталог. Несколько каталогов импорта могут быть указаны путем передачи опции --proto_path несколько раз; они будут искаться в порядке. -I=_IMPORT_PATH_ может быть использована как короткая форма --proto_path.

Примечание: Пути к файлам относительно их proto_path должны быть глобально уникальными в данном бинарном файле. Например, если у вас есть proto/lib1/data.proto и proto/lib2/data.proto, эти два файла не могут быть использованы вместе с -I=proto/lib1 -I=proto/lib2, потому что будет неоднозначно, какой файл import "data.proto" будет означать. Вместо этого следует использовать -Iproto/ и глобальные имена будут lib1/data.proto и lib2/data.proto.

Если вы публикуете библиотеку и другие пользователи могут использовать ваши сообщения напрямую, вы должны включить уникальное имя библиотеки в путь, под которым они ожидаются использоваться, чтобы избежать конфликтов имен файлов. Если у вас несколько каталогов в одном проекте, лучшей практикой является предпочтение установки одного -I в каталог верхнего уровня проекта.

Расположение файлов

Предпочитайте не помещать файлы .proto в тот же каталог, что и другие языковые исходники. Рассмотрите создание подпакета proto для файлов .proto под корневым пакетом для вашего проекта.

Расположение должно быть независимым от языка

При работе с Java кодом удобно помещать связанные файлы .proto в тот же каталог, что и Java исходники. Однако, если какой-либо не-Java код когда-либо использует те же protos, префикс пути больше не будет иметь смысла. Поэтому в общем, помещайте protos в связанный независимый от языка каталог, такой как //myteam/mypackage.

Исключением из этого правила является случай, когда ясно, что protos будут использоваться только в Java контексте, например, для тестирования.

Поддерживаемые платформы

Для информации о: