Rust 生成程式碼指南

描述 protocol buffer 編譯器為任何給定的協議定義所生成的訊息物件的 API。

本頁面精確描述了協議緩衝區編譯器針對任何給定協議定義生成的 Rust 程式碼。

本文件涵蓋了協議緩衝區編譯器如何為 proto2、proto3 和 protobuf 版本生成 Rust 程式碼。並突出顯示了 proto2、proto3 和版本生成程式碼之間的任何差異。在閱讀本文件之前,您應該閱讀 proto2 語言指南proto3 語言指南版本指南

Protobuf Rust

Protobuf Rust 是 Protocol Buffers 的一個實現,旨在能夠基於我們稱之為“核心”的其他現有 Protocol Buffers 實現之上執行。

支援多個非 Rust 核心的決定顯著影響了我們的公共 API,包括選擇使用自定義型別如 ProtoStr 而非 Rust std 型別如 str。有關此主題的更多資訊,請參閱 Rust Proto 設計決策

包(Packages)

與大多數其他語言不同,.proto 檔案中的 package 宣告未在 Rust 程式碼生成中使用。

當使用 rust_proto_library 時,該庫將對應一個 crate。目標名稱用作 crate 名稱。請相應地選擇您的庫名稱。

使用 Cargo 時,我們建議您將 generated.rs 入口點 include! 到一個適當名稱的模組中,如我們的 示例 Crate 中所示。

訊息

給定訊息宣告

message Foo {}

編譯器生成一個名為 Foo 的結構體。Foo 結構體定義了以下關聯函式和方法

關聯函式

  • fn new() -> Self: 建立 Foo 的新例項。

特性(Traits)

由於多種原因,包括 gencode 大小、名稱衝突問題和 gencode 穩定性,訊息上的大多數常見功能都在特性(traits)上實現,而不是作為固有實現。

大多數使用者應該匯入我們的 prelude,它只包含特性和我們的 proto! 宏,不包含其他型別(use protobuf::prelude::*)。如果您想避免使用 prelude,可以根據需要匯入特定的特性(請參閱

此處文件,瞭解特性的名稱和定義,如果您想直接匯入它們)。

  • fn parse(data: &[u8]) -> Result: 解析訊息的新例項。
  • fn parse_dont_enforce_required(data: &[u8]) -> Result: 與 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, SerializeError>: 將訊息序列化為 Protobuf 線格式。序列化可能會失敗,但很少發生。失敗原因包括表示超出最大編碼訊息大小(必須小於 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<'_>: 返回 Foo 的不可變控制代碼(view)。這將在代理型別部分進一步介紹。
  • fn as_mut(&mut self) -> FooMut<'_>: 返回 Foo 的可變控制代碼(mut)。這將在代理型別部分進一步介紹。

Foo 額外實現了以下標準特性

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

流暢地建立新例項

setter 的 API 設計遵循我們既定的 Protobuf 慣用法,但在構建新例項時,某些其他語言的冗長是一個輕微的痛點。為了緩解這種情況,我們提供了 proto! 宏,它可以更簡潔/流暢地建立新例項。

例如,無需這樣編寫

let mut msg = SomeMsg::new();
msg.set_x(1);
msg.set_y("hello");
msg.some_submessage_mut().set_z(42);

這個宏可以用來這樣編寫

let msg = proto!(SomeMsg {
  x: 1,
  y: "hello",
  some_submsg: SomeSubmsg {
    z: 42
  }
});

訊息代理型別

由於一些技術原因,我們選擇在某些情況下避免使用原生 Rust 引用(&T&mut T)。相反,我們需要使用型別——ViewMut 來表達這些概念。這些情況是共享的和可變的引用到

  • 訊息
  • 重複欄位
  • Map 欄位

例如,編譯器會與 Foo 一起發出結構體 FooView<'a>FooMut<'msg>。這些型別用於代替 &Foo&mut Foo,並且它們在借用檢查器行為方面與原生 Rust 引用行為相同。就像原生借用一樣,View 是 Copy,借用檢查器將強制您在給定時間只能擁有任意數量的 View 或最多一個 Mut。

出於本文件的目的,我們重點描述為所有權訊息型別 (Foo) 發出的所有方法。其中一部分帶有 &self 接收器的方法也將包含在 FooView<'msg> 上。其中一部分帶有 &self&mut self 的方法也將包含在 FooMut<'msg> 上。

要從 View / Mut 型別建立所有權訊息型別,請呼叫 to_owned(),它會建立深複製。

有關為何做出此選擇的更多討論,請參閱我們的 設計決策 文件中的相應部分。

巢狀型別

給定訊息宣告

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

除了名為 Foo 的結構體之外,還建立了一個名為 foo 的模組來包含 Bar 的結構體。類似地,建立了一個名為 bar 的巢狀模組來包含深度巢狀的列舉 Baz

pub struct Foo {}

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

欄位

除了上一節中描述的方法之外,協議緩衝區編譯器還會為 .proto 檔案中訊息內定義的每個欄位生成一組訪問器方法。

按照 Rust 風格,方法使用小寫/蛇形命名,例如 has_foo()clear_foo()。請注意,訪問器中欄位名稱部分的字母大小寫保持與原始 .proto 檔案中的風格一致,而根據 .proto 檔案風格指南,它應該是小寫/蛇形命名。

具有顯式存在性的欄位

顯式存在性意味著欄位區分預設值和未設定值。在 proto2 中,optional 欄位具有顯式存在性。在 proto3 中,只有訊息欄位以及 oneofoptional 欄位具有顯式存在性。存在性透過在版本中設定 features.field_presence 選項來設定。

數字欄位

對於此欄位定義:

int32 foo = 1;

編譯器生成以下訪問器方法

  • fn has_foo(&self) -> bool: 如果欄位已設定,則返回 true
  • fn foo(&self) -> i32: 返回欄位的當前值。如果欄位未設定,則返回預設值。
  • fn foo_opt(&self) -> protobuf::Optional: 如果欄位已設定,則返回帶有變體Set(value)的可選項;如果未設定,則返回Unset(default value)的可選項。請參閱 [`Optional` rustdoc](https://docs.rs/protobuf/4.33.5-release/protobuf/enum.Optional.html)
  • fn set_foo(&mut self, val: i32): 設定欄位的值。呼叫此方法後,has_foo() 將返回 truefoo() 將返回 value
  • fn clear_foo(&mut self): 清除欄位的值。呼叫此方法後,has_foo() 將返回 falsefoo() 將返回預設值。

對於其他數字欄位型別(包括 bool),int32 將根據 標量值型別表 替換為相應的 Rust 型別。

字串和位元組欄位

對於這些欄位定義

string foo = 1;
bytes foo = 1;

編譯器生成以下訪問器方法

  • fn has_foo(&self) -> bool: 如果欄位已設定,則返回 true
  • fn foo(&self) -> &protobuf::ProtoStr: 返回欄位的當前值。如果欄位未設定,則返回預設值。參見 ProtoStr rustdoc
  • fn foo_opt(&self) -> protobuf::Optional<&ProtoStr>: 如果欄位已設定,則返回帶有變體 Set(value) 的可選項;如果未設定,則返回 Unset(default value) 的可選項。
  • fn set_foo(&mut self, val: impl IntoProxied): 設定欄位的值。&strString&ProtoStrProtoString 都實現了 IntoProxied,並且可以傳遞給此方法。
  • fn clear_foo(&mut self): 清除欄位的值。呼叫此方法後,has_foo() 將返回 falsefoo() 將返回預設值。

對於 bytes 型別的欄位,編譯器將生成 ProtoBytes 型別。

列舉欄位

給定任何 proto 語法版本中的此列舉定義

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

編譯器生成一個結構體,其中每個變體都是一個關聯常量

#[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: 如果欄位已設定,則返回帶有變體 Set(value) 的可選項;如果未設定,則返回 Unset(default value) 的可選項。
  • fn set_foo(&mut self, val: Bar): 設定欄位的值。呼叫此方法後,has_foo() 將返回 truefoo() 將返回 value
  • fn clear_foo(&mut self): 清除欄位的值。呼叫此方法後,has_foo() 將返回 false,foo() 將返回預設值。

內嵌訊息欄位

給定任何 proto 語法版本中的訊息型別 Bar

message Bar {}

對於任何這些欄位定義


message MyMessage {
  Bar foo = 1;
}

編譯器將生成以下訪問器方法

  • fn foo(&self) -> BarView<'_>: 返回欄位當前值的檢視。如果欄位未設定,則返回一個空訊息。
  • fn foo_mut(&mut self) -> BarMut<'_>: 返回欄位當前值的可變控制代碼。如果欄位未設定,則設定該欄位。呼叫此方法後,has_foo() 返回 true。
  • fn foo_opt(&self) -> protobuf::Optional: 如果欄位已設定,則返回帶有其 value 的變體 Set。否則返回帶有預設值的變體 Unset
  • fn set_foo(&mut self, value: impl protobuf::IntoProxied): 將欄位設定為 value。呼叫此方法後,has_foo() 返回 true
  • fn has_foo(&self) -> bool: 如果欄位已設定,則返回 true
  • fn clear_foo(&mut self): 清除欄位。呼叫此方法後,has_foo() 返回 false

具有隱式存在性的欄位(proto3 和 Editions)

隱式存在性意味著欄位不區分預設值和未設定值。在 proto3 中,欄位預設具有隱式存在性。在版本中,您可以透過將 field_presence 功能設定為 IMPLICIT 來宣告具有隱式存在性的欄位。

數字欄位

對於這些欄位定義

// proto3
int32 foo = 1;

// editions
message MyMessage {
  int32 foo = 1 [features.field_presence = IMPLICIT];
}

編譯器生成以下訪問器方法

  • fn foo(&self) -> i32: 返回欄位的當前值。如果欄位未設定,則返回 0
  • fn set_foo(&mut self, val: i32): 設定欄位的值。

對於其他數字欄位型別(包括 bool),int32 將根據 標量值型別表 替換為相應的 Rust 型別。

字串和位元組欄位

對於這些欄位定義

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

// editions
string foo = 1 [features.field_presence = IMPLICIT];
bytes bar = 2 [features.field_presence = IMPLICIT];

編譯器將生成以下訪問器方法

  • fn foo(&self) -> &ProtoStr: 返回欄位的當前值。如果欄位未設定,則返回空字串/空位元組。參見 ProtoStr rustdoc
  • fn set_foo(&mut self, value: IntoProxied): 將欄位設定為 value

對於 bytes 型別的欄位,編譯器將生成 ProtoBytes 型別。

支援 Cord 的單字串和位元組欄位

[ctype = CORD] 允許位元組和字串以 absl::Cord 的形式儲存在 C++ Protobuf 中。absl::Cord 目前在 Rust 中沒有等效型別。Protobuf Rust 使用列舉來表示 cord 欄位

enum ProtoStringCow<'a> {
  Owned(ProtoString),
  Borrowed(&'a ProtoStr)
}

在常見情況下,對於小字串,absl::Cord 將其資料儲存為連續字串。在這種情況下,cord 訪問器返回 ProtoStringCow::Borrowed。如果底層 absl::Cord 不連續,訪問器將資料從 cord 複製到擁有的 ProtoString 中,並返回 ProtoStringCow::OwnedProtoStringCow 實現了 Deref

對於任何這些欄位定義

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): 將欄位設定為 value。呼叫此函式後,foo() 返回 valuehas_foo() 返回 true
  • fn has_foo(&self) -> bool: 如果欄位已設定,則返回 true
  • fn clear_foo(&mut self): 清除欄位的值。呼叫此方法後,has_foo() 返回 falsefoo() 返回預設值。Cord 尚未實現。

對於 bytes 型別的欄位,編譯器將生成 ProtoBytesCow 型別。

編譯器生成以下訪問器方法

  • fn foo(&self) -> &ProtoStr: 返回欄位的當前值。如果欄位未設定,則返回空字串/空位元組。
  • fn set_foo(&mut self, value: impl IntoProxied): 將欄位設定為 value

列舉欄位

給定列舉型別

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

編譯器生成一個結構體,其中每個變體都是一個關聯常量

#[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;

// editions
message MyMessage {
 Bar foo = 1 [features.field_presence = IMPLICIT];
}

編譯器將生成以下訪問器方法

  • fn foo(&self) -> Bar: 返回欄位的當前值。如果欄位未設定,則返回預設值。
  • fn set_foo(&mut self, value: Bar): 設定欄位的值。呼叫此方法後,has_foo() 將返回 truefoo() 將返回 value

重複欄位

對於任何重複欄位定義,編譯器將生成相同的三個訪問器方法,這些方法僅在欄位型別上有所不同。

在版本中,您可以使用 repeated_field_encoding 功能來控制重複原始欄位的線格式編碼。

// proto2
repeated int32 foo = 1; // EXPANDED by default

// proto3
repeated int32 foo = 1; // PACKED by default

// editions
repeated int32 foo = 1 [features.repeated_field_encoding = PACKED];
repeated int32 bar = 2 [features.repeated_field_encoding = EXPANDED];

給定上述任何欄位定義,編譯器都會生成以下訪問器方法

  • fn foo(&self) -> RepeatedView<'_, i32>: 返回底層重複欄位的檢視。參見 RepeatedView rustdoc
  • fn foo_mut(&mut self) -> RepeatedMut<'_, i32>: 返回底層重複欄位的可變控制代碼。參見 RepeatedMut rustdoc
  • fn set_foo(&mut self, src: impl IntoProxied>): 將底層重複欄位設定為 src 中提供的新重複欄位。接受 RepeatedViewRepeatedMutRepeated。參見 Repeated rustdoc

對於不同的欄位型別,只有 RepeatedViewRepeatedMutRepeated 型別的相應泛型型別會改變。例如,給定一個 string 型別的欄位,foo() 訪問器將返回 RepeatedView<'_, ProtoString>

對映欄位

對於此 map 欄位定義:

map<int32, int32> weight = 1;

編譯器將生成以下 3 個訪問器方法

  • fn weight(&self) -> protobuf::MapView<'_, i32, i32>: 返回底層對映的不可變檢視。參見 MapView rustdoc
  • fn weight_mut(&mut self) -> protobuf::MapMut<'_, i32, i32>: 返回底層對映的可變控制代碼。參見 MapMut rustdoc
  • fn set_weight(&mut self, src: protobuf::IntoProxied>): 將底層對映設定為 src。接受 MapViewMapMutMap。參見 Map rustdoc

對於不同的欄位型別,只有 MapViewMapMutMap 型別的相應泛型型別會改變。例如,給定一個 string 型別的欄位,foo() 訪問器將返回 MapView<'_, int32, ProtoString>

Any

目前 Rust Protobuf 不會對 Any 進行特殊處理;它會像一個具有此定義的簡單訊息一樣

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

Oneof

給定像這樣的 oneof 定義

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

編譯器將為每個欄位生成訪問器(getter、setter、hazzer),就好像同一個欄位被宣告為 oneof 外部的 optional 欄位一樣。因此,您可以像處理常規欄位一樣處理 oneof 欄位,但設定其中一個欄位將清除 oneof 塊中的其他欄位。此外,還為 oneof 塊發出以下型別

  #[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
  }
  #[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;
}

編譯器將生成

  #[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);
  }

請注意,對於字首與列舉匹配的值,字首將被剝離;這樣做是為了提高人體工程學。列舉值通常以列舉名稱作為字首,以避免同級列舉之間發生名稱衝突(這些列舉遵循 C++ 列舉的語義,其中值不受其包含列舉的範圍限制)。由於生成的 Rust 常量在 impl 中具有作用域,因此在 .proto 檔案中新增的有益的額外字首在 Rust 中將是多餘的。

擴充套件(僅限 proto2)

Rust 擴充套件 API 目前仍在開發中。擴充套件欄位將透過解析/序列化進行維護,並且在 C++ 互操作情況下,如果從 Rust 訪問訊息(並在訊息複製或合併的情況下傳播),則會保留所有設定的擴充套件。

競技場分配(Arena Allocation)

尚未實現 arena 分配訊息的 Rust API。

在內部,Protobuf Rust 在 upb 核心上使用競技場(arenas),但在 C++ 核心上不使用。然而,對在 C++ 中分配到競技場的訊息的引用(const 和可變)可以安全地傳遞給 Rust 以進行訪問或修改。

服務(Services)

尚未實現服務的 Rust API。