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

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

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

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

Для получения информации о синтаксисе proto2, см. Руководство по языку Proto2.

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

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

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

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • Первая строка файла указывает, что вы используете редакцию proto3 спецификации языка protobuf.
    • edition (или syntax для proto2/proto3) должна быть первой непустой, не комментарием строкой файла.
    • Если edition или syntax не указана, компилятор буферов протокола предположит, что вы используете proto2.
  • Определение сообщения SearchRequest задает три поля (пары имя/значение), по одному для каждой части данных, которые вы хотите включить в этот тип сообщения. Каждое поле имеет имя и тип.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      • поле установлено и содержит значение, которое было явно задано или разобрано из провода. Оно будет сериализовано в провод.
      • поле не установлено и будет возвращать значение по умолчанию. Оно не будет сериализовано в провод. Вы можете проверить, было ли значение явно установлено. optional рекомендуется вместо неявных полей для максимальной совместимости с редакциями protobuf и proto2.
    • неявные (implicit): (не рекомендуется) Неявное поле не имеет явной метки мощности и ведет себя следующим образом:

      • если поле является типом сообщения, оно ведет себя так же, как поле optional.
      • если поле не является сообщением, оно имеет два состояния:
        • поле установлено в не-умолчательное (ненулевое) значение, которое было явно задано или разобрано из провода. Оно будет сериализовано в провод.
        • поле установлено в значение по умолчанию (ноль). Оно не будет сериализовано в провод. Фактически, вы не можете определить, было ли значение по умолчанию (ноль) установлено, разобрано из провода или не предоставлено вовсе. Для получения дополнительной информации по этой теме см. Присутствие поля.
  • repeated: этот тип поля может повторяться ноль или более раз в правильно сформированном сообщении. Порядок повторяющихся значений сохраняется.

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

Поля Repeated упаковываются по умолчанию

В proto3 поля repeated скалярных числовых типов используют packed кодирование по умолчанию.

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

Поля типа сообщения всегда имеют присутствие поля

В proto3 поля типа сообщения уже имеют присутствие поля. Из-за этого добавление модификатора optional не меняет присутствие поля для этого поля.

Определения для Message2 и Message3 в следующем примере кода генерируют одинаковый код для всех языков, и нет разницы в представлении в бинарном, JSON и TextFormat:

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1;
}

message Message3 {
  optional Message1 bar = 1;
}

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

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

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

Добавление большего количества типов сообщений

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

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

message SearchResponse {
 ...
}

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

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

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

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

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

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

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

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

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

Вам также следует зарезервировать имя поля, чтобы кодировки 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?

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

  • Для C++ компилятор генерирует файлы .h и .cc из каждого .proto, с классом для каждого типа сообщения, описанного в вашем файле.
  • Для Java компилятор генерирует файл .java с классом для каждого типа сообщения, а также специальный класс Builder для создания экземпляров классов сообщений.
  • Для Kotlin в дополнение к сгенерированному Java коду компилятор генерирует файл .kt для каждого типа сообщения с улучшенным Kotlin API. Это включает DSL, который упрощает создание экземпляров сообщений, accessor 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 Использует формат двойной точности IEEE 754.
float Использует формат одинарной точности IEEE 754.
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 int 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 int 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 std::string String str/unicode[5] string String (UTF-8) string string String ProtoString
bytes std::string ByteString str (Python 2), bytes (Python 3) []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] Строки Python представлены как unicode при декодировании, но могут быть str, если задана строка ASCII (это может измениться).

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

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

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

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

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

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

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

Обратите внимание, что для скалярных полей с неявным присутствием после разбора сообщения нет возможности определить, было ли это поле явно установлено в значение по умолчанию (например, было ли логическое значение установлено в false) или просто не установлено вовсе: вы должны иметь это в виду при определении типов ваших сообщений. Например, не используйте логическое значение, которое включает какое-либо поведение при установке в false, если вы не хотите, чтобы это поведение также происходило по умолчанию. Также обратите внимание, что если скалярное поле сообщения установлено в значение по умолчанию, значение не будет сериализовано в провод. Если значение float или double установлено в +0, оно не будет сериализовано, но -0 считается отличным и будет сериализовано.

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

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

Когда вы определяете тип сообщения, вам может понадобиться, чтобы одно из его полей имело только одно значение из предопределенного списка. Например, допустим, вы хотите добавить поле 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 {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

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

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

В proto3 первое значение, определенное в определении перечисления, должно иметь значение ноль и должно иметь имя ENUM_TYPE_NAME_UNSPECIFIED или ENUM_TYPE_NAME_UNKNOWN. Это потому что:

  • Должно быть нулевое значение, чтобы мы могли использовать 0 как числовое значение по умолчанию.
  • Нулевое значение должно быть первым элементом для совместимости с семантикой proto2, где первое значение перечисления является значением по умолчанию, если явно не указано другое значение.

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

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

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

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_.

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

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

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

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

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

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

Если вы обновляете тип enum, полностью удаляя запись enum или комментируя ее, будущие пользователи могут повторно использовать числовое значение при внесении своих собственных обновлений в тип. Это может вызвать серьезные проблемы, если они позже загрузят старые экземпляры того же .proto, включая повреждение данных, ошибки конфиденциальности и т. д. Один из способов убедиться, что этого не произойдет, – указать, что числовые значения (и/или имена, которые также могут вызывать проблемы для сериализации JSON) ваших удаленных записей reserved. Компилятор буферов протокола будет жаловаться, если какие-либо будущие пользователи попытаются использовать эти идентификаторы. Вы можете указать, что ваш зарезервированный числовой диапазон значений доходит до максимально возможного значения, используя ключевое слово 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 {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

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

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

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

import "myproject/other_protos.proto";

Компилятор protobuf ищет импортированные файлы в наборе каталогов, указанных с помощью флага -I/--proto_path. Путь, указанный в операторе import, разрешается относительно этих каталогов. Для получения дополнительной информации об использовании компилятора см. Генерация ваших классов.

Например, рассмотрим следующую структуру каталогов:

my_project/
├── protos/
│   ├── main.proto
│   └── common/
│       └── timestamp.proto

Чтобы использовать определения из timestamp.proto внутри main.proto, вы должны запустить компилятор из каталога my_project и установить --proto_path=protos. Тогда оператор import в main.proto будет:

// Находится в my_project/protos/main.proto
import "common/timestamp.proto";

Как правило, вы должны установить флаг --proto_path в каталог самого высокого уровня, который содержит proto. Часто это корень проекта, но в этом примере он находится в отдельном каталоге /protos.

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

Примечание: Функциональность публичного импорта, доступная в Java, наиболее эффективна при перемещении всего файла .proto или при использовании java_multiple_files = true. В этих случаях сгенерированные имена остаются стабильными, что позволяет избежать необходимости обновлять ссылки в вашем коде. Хотя технически функциональна при перемещении подмножества файла .proto без java_multiple_files = true, это требует одновременного обновления многих ссылок, поэтому может не значительно облегчить миграцию. Функциональность недоступна в 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

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

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

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

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

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

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

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 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, могут переупорядочивать записи и отбрасывать записи с дублирующимися ключами.

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

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

Сообщения Proto3 сохраняют неизвестные поля и включают их во время разбора и в сериализованном выводе, что соответствует поведению proto2.

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

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

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

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

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

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

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 ...
  }
}

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 полей и repeated полей. Если вам нужно добавить повторяющееся поле в oneof, вы можете использовать сообщение, содержащее повторяющееся поле.

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

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

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

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

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

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

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

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

  • Если вы установите поле oneof в значение по умолчанию (например, установите поле oneof int32 в 0), «case» этого поля 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, каждое сообщение окажется с case 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_EQ(msg2.name(), "name");
    

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

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

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

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

Карты

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

map<key_type, value_type> map_field = N;

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

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

map<string, Project> projects = 3;

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

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

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

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

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

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

repeated MapFieldEntry map_field = N;

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

Пакеты

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

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

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

message Foo {
  ...
  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 непереносимым для других языков.

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

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

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

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

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

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

Наиболее straightforward RPC система для использования с буферами протокола – это gRPC: нейтральная к языку и платформе система RPC с открытым исходным кодом, разработанная в Google. gRPC особенно хорошо работает с буферами протокола и позволяет вам генерировать соответствующий RPC код непосредственно из ваших файлов .proto с использованием специального плагина компилятора буфера протокола.

Если вы не хотите использовать gRPC, также возможно использовать буферы протокола с вашей собственной реализацией RPC. Вы можете узнать больше об этом в Руководстве по языку Proto2.

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

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

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

Опции

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

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

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

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

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

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

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

    • SPEED (по умолчанию): Компилятор буфера протокола сгенерирует код для сериализации, разбора и выполнения других общих операций над вашими типами сообщений. Этот код высоко оптимизирован.
    • CODE_SIZE: Компилятор буфера протокола сгенерирует минимальные классы и будет полагаться на общий, основанный на отражении код для реализации сериализации, разбора и различных других операций. Сгенерированный код будет thus намного меньше, чем с SPEED, но операции будут медленнее. Классы все равно будут реализовывать точно такой же публичный API, как и в режиме SPEED. Этот режим наиболее полезен в приложениях, которые содержат очень большое количество файлов .proto и не нуждаются в том, чтобы все они были blindingly быстрыми.
    • LITE_RUNTIME: Компилятор буфера протокола сгенерирует классы, которые зависят только от «облегченной» библиотеки времени выполнения (libprotobuf-lite вместо libprotobuf). Облегченная среда выполнения намного меньше полной библиотеки (примерно на порядок меньше), но опускает определенные функции, такие как дескрипторы и отражение. Это особенно полезно для приложений, работающих на ограниченных платформах, таких как мобильные телефоны. Компилятор все равно будет генерировать быстрые реализации всех методов, как в режиме SPEED. Сгенерированные классы будут реализовывать только интерфейс MessageLite на каждом языке, который предоставляет только подмножество методов полного интерфейса Message.
    option optimize_for = CODE_SIZE;
    
  • cc_generic_services, java_generic_services, py_generic_services (опции файла): Общие сервисы устарели. Должен ли компилятор буфера протокола генерировать абстрактный сервисный код на основе определений сервисов в 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, который добавляется ко всем сгенерированным классам и перечислениям Objective-C из этого .proto. Значения по умолчанию нет. Вы должны использовать префиксы длиной от 3 до 5 заглавных символов, как рекомендовано Apple. Обратите внимание, что все 2-буквенные префиксы зарезервированы Apple.

  • packed (опция поля): По умолчанию true для повторяющегося поля базового числового типа, вызывая использование более компактного кодирования. Чтобы использовать неупакованный wireformat, можно установить false. Это обеспечивает совместимость с парсерами до версии 2.3.0 (редко нужно), как показано в следующем примере:

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

    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 также позволяет вам определять и использовать ваши собственные опции. Обратите внимание, что это продвинутая функция, которая большинству людей не нужна. Если вы все же думаете, что вам нужно создать свои собственные опции, см. Руководство по языку Proto2 для подробностей. Обратите внимание, что создание пользовательских опций использует расширения, которые разрешены только для пользовательских опций в proto3.

Удержание опций

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

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

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

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

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

message OptionsMessage {
  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, то это поле не может быть установлено в пользовательской опции на перечислении (или любой другой не-сообщенной сущности). Protoc обеспечивает это и будет выдавать ошибку, если есть нарушение ограничений целей.

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

message MyOptions {
  string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  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: это поле разрешено в опциях сообщения и перечисления
  option (message_options).message_and_enum_option = 42;
}

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

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

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

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

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, например, для тестирования.

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

Для получения информации о: