Руководство по сгенерированному коду на Rust

Описывает API объектов сообщений, которые компилятор protocol buffer генерирует для любого данного определения протокола.

На этой странице описано, какой именно код Rust компилятор protocol buffer генерирует для любого данного определения протокола.

Этот документ охватывает, как компилятор protocol buffer генерирует код Rust для proto2, proto3 и редакций protobuf. Любые различия между сгенерированным кодом для proto2, proto3 и редакций выделены. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по редакциям перед чтением этого документа.

Protobuf Rust

Protobuf Rust — это реализация protocol buffers, разработанная для возможности находиться поверх других существующих реализаций protocol buffer, которые мы называем 'ядрами' (kernels).

Решение поддерживать несколько не-Rust ядер значительно повлияло на наш публичный API, включая выбор использования пользовательских типов, таких как ProtoStr, вместо стандартных типов Rust, таких как str. См. Проектные решения Rust Proto для получения дополнительной информации по этой теме.

Генерируемые имена файлов

Каждый rust_proto_library будет скомпилирован как один crate (пакет). Что наиболее важно, для каждого файла .proto в srcs соответствующего proto_library создается один файл Rust, и все эти файлы образуют единый crate.

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

Генерируемые файлы:

  • Ядро C++:
    • .c.pb.rs - сгенерированный код Rust
    • .pb.thunks.cc - сгенерированные thunks C++ (код-клей, который вызывает код Rust, и который делегирует вызовы C++ Protobuf API).
  • Ядро C++ Lite:
    • <то же, что и для ядра C++>
  • Ядро UPB
    • .u.pb.rs - сгенерированный код Rust.

Каждый proto_library также будет иметь файл generated.rs, который рассматривается как точка входа для crate. Этот файл будет реэкспортировать символы из всех остальных файлов Rust в crate.

Пакеты

В отличие от большинства других языков, объявления package в файлах .proto не используются в кодогенерации Rust. Вместо этого, каждый целевой объект rust_proto_library(name = "some_rust_proto") выпускает crate с именем some_rust_proto, который содержит сгенерированный код для всех файлов .proto в целевом объекте.

Сообщения

Для объявления сообщения:

message Foo {}

Компилятор генерирует структуру с именем Foo. Структура Foo определяет следующие ассоциированные функции и методы:

Ассоциированные функции

  • fn new() -> Self: Создает новый экземпляр Foo.

Трейты

По ряду причин, включая размер gencode, проблемы с коллизиями имен и стабильность gencode, большая часть распространенной функциональности для сообщений реализована в виде трейтов, а не как inherent implementations (встроенные реализации).

Большинству пользователей следует импортировать наш prelude (прелюдию), которая включает только трейты и наш макрос proto! и никакие другие типы (use protobuf::prelude::*). Если вы предпочитаете избегать прелюдий, вы всегда можете импортировать конкретные трейты по мере необходимости (см.

документацию здесь для имен и определений трейтов, если вы хотите импортировать их напрямую).

  • fn parse(data: &[u8]) -> Result<Self, ParseError>: Разбирает новый экземпляр сообщения.
  • fn parse_dont_enforce_required(data: &[u8]) -> Result<Self, ParseError>: То же, что и parse, но не завершается ошибкой при отсутствии proto2 required полей.
  • fn clear(&mut self): Очищает сообщение.
  • fn clear_and_parse(&mut self, data: &[u8]) -> Result<(), ParseError>: Очистка и разбор в существующий экземпляр.
  • fn clear_and_parse_dont_enforce_required(&mut self, data: &[u8]) -> Result<(), ParseError>: То же, что и parse, но не завершается ошибкой при отсутствии proto2 required полей.
  • fn serialize(&self) -> Result<Vec<u8>, SerializeError>: Сериализует сообщение в формат Protobuf wire format. Сериализация может завершиться неудачей, но редко. Причины неудачи включают в себя, если представление превышает максимальный размер закодированного сообщения (должен быть меньше 2 GiB), и required поля (proto2), которые не установлены.
  • fn take_from(&mut self, other): Перемещает other в self, отбрасывая любое предыдущее состояние, которое содержал self.
  • fn copy_from(&mut self, other): Копирует other в self, отбрасывая любое предыдущее состояние, которое содержал self. other не изменяется.
  • fn merge_from(&mut self, other): Объединяет other в self.
  • fn as_view(&self) -> FooView<'_>: Возвращает неизменяемый handle (представление, view) для Foo. Это более подробно рассматривается в разделе о proxy types (типах-посредниках).
  • fn as_mut(&mut self) -> FooMut<'_>: Возвращает изменяемый handle (mut) для Foo. Это более подробно рассматривается в разделе о proxy types.

Foo дополнительно реализует следующие стандартные трейты:

  • std::fmt::Debug
  • std::default::Default
  • std::clone::Clone
  • std::marker::Send
  • std::marker::Sync

Плавное создание новых экземпляров

Дизайн API сеттеров следует нашим установленным идиомам Protobuf, но многословие при создании новых экземпляров является небольшой проблемой в некоторых других языках. Чтобы смягчить это, мы предлагаем макрос proto!, который можно использовать для более краткого/плавного создания новых экземпляров.

Например, вместо написания этого:

#![allow(unused)]
fn main() {
let mut msg = SomeMsg::new();
msg.set_x(1);
msg.set_y("hello");
msg.some_submessage_mut().set_z(42);
}

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

#![allow(unused)]
fn main() {
let msg = proto!(SomeMsg {
  x: 1,
  y: "hello",
  some_submsg: SomeSubmsg {
    z: 42
  }
});
}

Типы-посредники сообщений

По ряду технических причин мы решили избегать использования нативных ссылок Rust (&T и &mut T) в определенных случаях. Вместо этого нам нужно выражать эти концепции с помощью типов — View и Mut. Эти ситуации — это разделяемые и изменяемые ссылки на:

  • Сообщения
  • Повторяющиеся поля
  • Поля-карты

Например, компилятор создает структуры FooView<'a> и FooMut<'msg> наряду с Foo. Эти типы используются вместо &Foo и &mut Foo, и они ведут себя так же, как нативные ссылки Rust, с точки зрения поведения borrow checker. Так же, как и нативные заимствования, View являются Copy, и borrow checker будет обеспечивать, чтобы у вас могло быть либо любое количество View, либо не более одного Mut одновременно.

Для целей этой документации мы сосредотачиваемся на описании всех методов, создаваемых для owned message type (типа сообщения, которым владеют) (Foo). Подмножество этих функций с получателем &self также будет включено в FooView<'msg>. Подмножество этих функций с получателем либо &self, либо &mut self также будет включено в FooMut<'msg>.

Чтобы создать owned message type из типа View / Mut, вызовите to_owned(), что создает глубокую копию.

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

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

Для объявления сообщения:

message Foo {
  message Bar {
      enum Baz { ... }
  }
}

В дополнение к структуре с именем Foo создается модуль с именем foo для содержания структуры для Bar. И аналогично вложенный модуль с именем bar для содержания глубоко вложенного перечисления Baz:

#![allow(unused)]
fn main() {
pub struct Foo {}

pub mod foo {
   pub struct Bar {}
   pub mod bar {
      pub struct Baz { ... }
   }
}
}

Поля

В дополнение к методам, описанным в предыдущем разделе, компилятор protocol buffer генерирует набор методов доступа для каждого поля, определенного внутри сообщения в файле .proto.

В соответствии со стилем Rust, методы пишутся в нижнем регистре/snake-case, например has_foo() и clear_foo(). Обратите внимание, что капитализация части имени поля в методе доступа сохраняет стиль из исходного .proto файла, который, в свою очередь, должен быть в нижнем регистре/snake-case согласно руководству по стилю файлов .proto.

Поля с явным присутствием

Явное присутствие означает, что поле различает значение по умолчанию и неустановленное значение. В proto2 поля optional имеют явное присутствие. В proto3 только поля сообщений и поля oneof или optional имеют явное присутствие. Присутствие устанавливается с помощью опции features.field_presence в редакциях.

Числовые поля

Для этого определения поля:

int32 foo = 1;

Компилятор генерирует следующие методы доступа:

  • fn has_foo(&self) -> bool: Возвращает true, если поле установлено.
  • fn foo(&self) -> i32: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.
  • fn foo_opt(&self) -> protobuf::Optional<i32>: Возвращает optional с вариантом Set(value), если поле установлено, или Unset(default value), если оно не установлено.
  • fn set_foo(&mut self, val: i32): Устанавливает значение поля. После вызова этого has_foo() будет возвращать true, а foo() будет возвращать value.
  • fn clear_foo(&mut self): Очищает значение поля. После вызова этого has_foo() будет возвращать false, а foo() будет возвращать значение по умолчанию.

Для других числовых типов полей (включая bool) int32 заменяется на соответствующий тип Rust в соответствии с таблицей скалярных типов значений.

Строковые поля и поля bytes

Для этих определений полей:

string foo = 1;
bytes foo = 1;

Компилятор генерирует следующие методы доступа:

  • fn has_foo(&self) -> bool: Возвращает true, если поле установлено.
  • fn foo(&self) -> &protobuf::ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.
  • fn foo_opt(&self) -> protobuf::Optional<&ProtoStr>: Возвращает optional с вариантом Set(value), если поле установлено, или Unset(default value) если оно не установлено.
  • fn set_foo(&mut self, val: impl IntoProxied<ProtoString>): Устанавливает значение поля.
  • fn clear_foo(&mut self): Очищает значение поля. После вызова этого has_foo() будет возвращать false, а foo() будет возвращать значение по умолчанию.

Для полей типа bytes компилятор сгенерирует тип ProtoBytes вместо этого.

Поля перечислений

Для этого определения перечисления в любой версии синтаксиса proto:

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

Компилятор генерирует структуру, где каждый вариант является ассоциированной константой:

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Bar(i32);

impl Bar {
  pub const Unspecified: Bar = Bar(0);
  pub const Value: Bar = Bar(1);
  pub const OtherValue: Bar = Bar(2);
}
}

Для этого определения поля:

Bar foo = 1;

Компилятор генерирует следующие методы доступа:

  • fn has_foo(&self) -> bool: Возвращает true, если поле установлено.
  • fn foo(&self) -> Bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.
  • fn foo_opt(&self) -> Optional<Bar>: Возвращает optional с вариантом Set(value), если поле установлено, или Unset(default value), если оно не установлено.
  • fn set_foo(&mut self, val: Bar): Устанавливает значение поля. После вызова этого has_foo() будет возвращать true, а foo() будет возвращать value.
  • fn clear_foo(&mut self): Очищает значение поля. После вызова этого has_foo() будет возвращать false, а foo() будет возвращать значение по умолчанию.

Встроенные поля сообщений

Для типа сообщения Bar из любой версии синтаксиса proto:

message Bar {}

Для любого из этих определений полей:

message MyMessage {
  Bar foo = 1;
}

Компилятор сгенерирует следующие методы доступа:

  • fn foo(&self) -> BarView<'_>: Возвращает view текущего значения поля. Если поле не установлено, возвращает пустое сообщение.
  • fn foo_mut(&mut self) -> BarMut<'_>: Возвращает изменяемый handle к текущему значению поля. Устанавливает поле, если оно не установлено. После вызова этого метода has_foo() возвращает true.
  • fn foo_opt(&self) -> protobuf::Optional<BarView>: Если поле установлено, возвращает вариант Set с его value. Иначе возвращает вариант Unset со значением по умолчанию.
  • fn set_foo(&mut self, value: impl protobuf::IntoProxied<Bar>): Устанавливает поле в value. После вызова этого метода has_foo() возвращает true.
  • fn has_foo(&self) -> bool: Возвращает true, если поле установлено.
  • fn clear_foo(&mut self): Очищает поле. После вызова этого метода has_foo() возвращает false.

Поля с неявным присутствием (proto3 и редакции)

Неявное присутствие означает, что поле не различает значение по умолчанию и неустановленное значение. В proto3 поля по умолчанию имеют неявное присутствие. В редакциях вы можете объявить поле с неявным присутствием, установив функцию field_presence в IMPLICIT.

Числовые поля

Для этих определений полей:

// proto3
int32 foo = 1;

// редакции
message MyMessage {
  int32 foo = 1 [features.field_presence = IMPLICIT];
}

Компилятор генерирует следующие методы доступа:

  • fn foo(&self) -> i32: Возвращает текущее значение поля. Если поле не установлено, возвращает 0.
  • fn set_foo(&mut self, val: i32): Устанавливает значение поля.

Для других числовых типов полей (включая bool) int32 заменяется на соответствующий тип Rust в соответствии с таблицей скалярных типов значений.

Строковые поля и поля bytes

Для этих определений полей:

// proto3
string foo = 1;
bytes foo = 1;

// редакции
string foo = 1 [features.field_presence = IMPLICIT];
bytes bar = 2 [features.field_presence = IMPLICIT];

Компилятор сгенерирует следующие методы доступа:

  • fn foo(&self) -> &ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.
  • fn set_foo(&mut self, value: IntoProxied<ProtoString>): Устанавливает поле в value.

Для полей типа bytes компилятор сгенерирует тип ProtoBytes вместо этого.

Сингулярные строковые поля и поля bytes с поддержкой Cord

[ctype = CORD] позволяет байтам и строкам храниться как absl::Cord в C++ Protobuf. absl::Cord в настоящее время не имеет эквивалентного типа в Rust. Protobuf Rust использует перечисление для представления поля cord:

#![allow(unused)]
fn main() {
enum ProtoStringCow<'a> {
  Owned(ProtoString),
  Borrowed(&'a ProtoStr)
}
}

В обычном случае, для маленьких строк, absl::Cord хранит свои данные как непрерывную строку. В этом случае методы доступа cord возвращают ProtoStringCow::Borrowed. Если лежащий в основе absl::Cord не является непрерывным, метод доступа копирует данные из cord в owned ProtoString и возвращает ProtoStringCow::Owned. ProtoStringCow реализует Deref<Target=ProtoStr>.

Для любого из этих определений полей:

optional string foo = 1 [ctype = CORD];
string foo = 1 [ctype = CORD];
optional bytes foo = 1 [ctype = CORD];
bytes foo = 1 [ctype = CORD];

Компилятор генерирует следующие методы доступа:

  • fn my_field(&self) -> ProtoStringCow<'_>: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.
  • fn set_my_field(&mut self, value: IntoProxied<ProtoString>): Устанавливает поле в value. После вызова этой функции foo() возвращает value и has_foo() возвращает true.
  • fn has_foo(&self) -> bool: Возвращает true, если поле установлено.
  • fn clear_foo(&mut self): Очищает значение поля. После вызова этого has_foo() возвращает false, а foo() возвращает значение по умолчанию. Cords еще не реализованы.

Для полей типа bytes компилятор генерирует тип ProtoBytesCow вместо этого.

Компилятор генерирует следующие методы доступа:

  • fn foo(&self) -> &ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.
  • fn set_foo(&mut self, value: impl IntoProxied<ProtoString>): Устанавливает поле в value.

Поля перечислений

Для типа перечисления:

enum Bar {
  BAR_UNSPECIFIED = 0;
  BAR_VALUE = 1;
  BAR_OTHER_VALUE = 2;
}

Компилятор генерирует структуру, где каждый вариант является ассоциированной константой:

#![allow(unused)]
fn main() {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Bar(i32);

impl Bar {
  pub const Unspecified: Bar = Bar(0);
  pub const Value: Bar = Bar(1);
  pub const OtherValue: Bar = Bar(2);
}
}

Для этих определений полей:

// proto3
Bar foo = 1;

// редакции
message MyMessage {
 Bar foo = 1 [features.field_presence = IMPLICIT];
}

Компилятор сгенерирует следующие методы доступа:

  • fn foo(&self) -> Bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.
  • fn set_foo(&mut self, value: Bar): Устанавливает значение поля. После вызова этого has_foo() будет возвращать true, а foo() будет возвращать value.

Повторяющиеся поля

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

В редакциях вы можете управлять кодированием wire format повторяющихся примитивных полей с помощью функции repeated_field_encoding.

// proto2
repeated int32 foo = 1; // EXPANDED по умолчанию

// proto3
repeated int32 foo = 1; // PACKED по умолчанию

// редакции
repeated int32 foo = 1 [features.repeated_field_encoding = PACKED];
repeated int32 bar = 2 [features.repeated_field_encoding = EXPANDED];

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

  • fn foo(&self) -> RepeatedView<'_, i32>: Возвращает view базового повторяющегося поля.
  • fn foo_mut(&mut self) -> RepeatedMut<'_, i32>: Возвращает изменяемый handle к базовому повторяющемуся полю.
  • fn set_foo(&mut self, src: impl IntoProxied<Repeated<i32>>): Устанавливает базовое повторяющееся поле в новое повторяющееся поле, предоставленное в src.

Для разных типов полей будут меняться только соответствующие generic-типы RepeatedView, RepeatedMut и Repeated. Например, для поля типа string метод доступа foo() возвращал бы RepeatedView<'_, ProtoString>.

Поля-карты

Для этого определения поля-карты:

map<int32, int32> weight = 1;

Компилятор сгенерирует следующие 3 метода доступа:

  • fn weight(&self) -> protobuf::MapView<'_, i32, i32>: Возвращает неизменяемый view базовой карты.
  • fn weight_mut(&mut self) -> protobuf::MapMut<'_, i32, i32>: Возвращает изменяемый handle к базовой карте.
  • fn set_weight(&mut self, src: protobuf::IntoProxied<Map<i32, i32>>): Устанавливает базовую карту в src.

Для разных типов полей будут меняться только соответствующие generic-типы MapView, MapMut и Map. Например, для поля типа string метод доступа foo() возвращал бы MapView<'_, int32, ProtoString>.

Any

Any в настоящее время не обрабатывается особым образом в Rust Protobuf; он будет вести себя так, как если бы это было простое сообщение с этим определением:

message Any {
  string type_url = 1;
  bytes value = 2;
}

Oneof

Для определения oneof, подобного этому:

oneof example_name {
    int32 foo_int = 4;
    string foo_string = 9;
    ...
}

Компилятор сгенерирует методы доступа (геттеры, сеттеры, hazzer'ы) для каждого поля так, как если бы то же поле было объявлено как optional поле вне oneof. Таким образом, вы можете работать с полями oneof как с обычными полями, но установка одного из них очистит другие поля в блоке oneof. Кроме того, для блока oneof создаются следующие типы:

#![allow(unused)]
fn main() {
  #[non_exhaustive]
  #[derive(Debug, Clone, Copy)]

  pub enum ExampleNameOneof<'msg> {
    FooInt(i32) = 4,
    FooString(&'msg protobuf::ProtoStr) = 9,
    not_set(std::marker::PhantomData<&'msg ()>) = 0
  }
}
#![allow(unused)]
fn main() {
  #[derive(Debug, Copy, Clone, PartialEq, Eq)]

  pub enum ExampleNameCase {
    FooInt = 4,
    FooString = 9,
    not_set = 0
  }
}

Дополнительно, он сгенерирует два метода доступа:

  • fn example_name(&self) -> ExampleNameOneof<_>: Возвращает вариант перечисления, указывающий, какое поле установлено, и значение поля. Возвращает not_set, если никакое поле не установлено.
  • fn example_name_case(&self) -> ExampleNameCase: Возвращает вариант перечисления, указывающий, какое поле установлено. Возвращает not_set, если никакое поле не установлено.

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

Для определения перечисления, подобного:

enum FooBar {
  FOO_BAR_UNKNOWN = 0;
  FOO_BAR_A = 1;
  FOO_B = 5;
  VALUE_C = 1234;
}

Компилятор сгенерирует:

#![allow(unused)]
fn main() {
  #[derive(Clone, Copy, PartialEq, Eq, Hash)]
  #[repr(transparent)]
  pub struct FooBar(i32);

  impl FooBar {
    pub const Unknown: FooBar = FooBar(0);
    pub const A: FooBar = FooBar(1);
    pub const FooB: FooBar = FooBar(5);
    pub const ValueC: FooBar = FooBar(1234);
  }
}

Обратите внимание, что для значений с префиксом, совпадающим с именем перечисления, префикс будет удален; это сделано для улучшения эргономики. Значения перечисления обычно имеют префикс с именем перечисления, чтобы избежать коллизий имен между sibling enums (которые следуют семантике перечислений C++, где значения не ограничены областью видимости их содержащего перечисления). Поскольку сгенерированные константы Rust ограничены областью видимости внутри impl, дополнительный префикс, который полезно добавлять в файлы .proto, был бы избыточным в Rust.

Расширения (только proto2)

Rust API для расширений в настоящее время находится в разработке. Поля расширений будут сохраняться через parse/serialize, и в случае C++ interop любые установленные расширения будут сохранены, если к сообщению обращаются из Rust (и распространены в случае копирования сообщения или слияния).

Выделение в Arena

Rust API для сообщений, выделенных в arena, еще не реализован.

Внутренне, Protobuf Rust на ядре upb использует arenas, но на ядрах C++ он не использует. Однако ссылки (как константные, так и изменяемые) на сообщения, которые были выделены в arena в C++, могут быть безопасно переданы в Rust для доступа или изменения.

Сервисы

Rust API для сервисов еще не реализован.