Rust 生成代码指南

描述 protocol buffer 编译器为任何给定的协议定义所生成的消息对象的 API。

本页面详细描述了 Protocol Buffer 编译器如何为任何给定的协议定义生成 Rust 代码。

本文档涵盖了 Protocol Buffer 编译器如何为 proto2、proto3 和 Protobuf Editions 生成 Rust 代码。proto2、proto3 和 Editions 生成代码之间的任何差异都会被重点说明。在阅读本文档之前,您应该先阅读 proto2 语言指南proto3 语言指南Editions 指南

Protobuf Rust

Protobuf Rust 是 Protocol Buffers 的一种实现,旨在能够构建于其他现有的 Protocol Buffer 实现之上,我们称之为“内核”(kernels)。

支持多种非 Rust 内核的决定对我们的公共 API 产生了显著影响,包括选择使用像 ProtoStr 这样的自定义类型,而不是 Rust 标准库中的 str 等类型。有关此主题的更多信息,请参阅Rust Proto 设计决策

生成的文件名

每个 rust_proto_library 都将被编译成一个 crate。最重要的是,对于相应 proto_librarysrcs 中的每个 .proto 文件,都会生成一个 Rust 文件,所有这些文件共同组成一个 crate。

编译器生成的文件因内核而异。通常,输出文件的名称是通过获取 .proto 文件的名称并替换其扩展名来计算的。

生成的文件

  • C++ 内核
    • .c.pb.rs - 生成的 Rust 代码
    • .pb.thunks.cc - 生成的 C++ thunk(Rust 代码调用的胶水代码,它会委托给 C++ Protobuf API)。
  • C++ Lite 内核
    • 与 C++ 内核相同
  • UPB 内核
    • .u.pb.rs - 生成的 Rust 代码。

每个 proto_library 还会有一个 generated.rs 文件,该文件被视为 crate 的入口点。该文件将重新导出 crate 中所有其他 Rust 文件的符号。

包(Packages)

与大多数其他语言不同,.proto 文件中的 package 声明在 Rust 代码生成中不被使用。相反,每个 rust_proto_library(name = "some_rust_proto") 目标都会生成一个名为 some_rust_proto 的 crate,其中包含该目标中所有 .proto 文件的生成代码。

消息

给定消息声明

message Foo {}

编译器会生成一个名为 Foo 的结构体。Foo 结构体定义了以下关联函数和方法:

关联函数

  • fn new() -> Self:创建一个新的 Foo 实例。

Trait

出于多种原因,包括生成代码的大小、名称冲突问题以及生成代码的稳定性,大多数消息的通用功能都是通过 trait 而不是固有实现(inherent implementations)来实现的。

大多数用户应该导入我们的 prelude,它只包含 trait 和我们的 proto! 宏,不包含其他类型(use protobuf::prelude::*)。如果您不想使用 prelude,可以随时根据需要导入特定的 trait(如果您想直接导入它们,请参阅

此处的文档以了解 trait 的名称和定义)。

  • 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 线格式。序列化可能会失败,但很少发生。失败的原因包括表示形式超过最大编码消息大小(必须小于 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 的一个不可变句柄(视图)。这在关于代理类型的章节中有进一步介绍。
  • fn as_mut(&mut self) -> FooMut<'_>:返回 Foo 的一个可变句柄(mut)。这在关于代理类型的章节中有进一步介绍。

Foo 还实现了以下 std trait:

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

字段

除了上一节中描述的方法外,protocol buffer 编译器还会为 .proto 文件中消息内定义的每个字段生成一组访问器方法。

遵循 Rust 风格,这些方法采用小写/蛇形命名法(lower-case/snake-case),例如 has_foo()clear_foo()。请注意,访问器中字段名部分的大小写保持了原始 .proto 文件中的风格,而根据 .proto 文件风格指南,该风格应为小写/蛇形命名法。

具有显式存在性(Presence)的字段

显式存在性意味着字段能够区分默认值和未设置值。在 proto2 中,optional 字段具有显式存在性。在 proto3 中,只有消息字段和 oneofoptional 字段具有显式存在性。在 Editions 中,存在性是使用 features.field_presence 选项设置的。

数值字段

对于此字段定义:

int32 foo = 1;

编译器会生成以下访问器方法:

  • fn has_foo(&self) -> bool:如果字段已设置,则返回 true
  • fn foo(&self) -> i32:返回字段的当前值。如果字段未设置,则返回默认值。
  • fn foo_opt(&self) -> protobuf::Optional<i32>:如果字段已设置,则返回一个带有 Set(value) 变体的 optional;如果未设置,则返回 Unset(default value)
  • 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:返回字段的当前值。如果字段未设置,则返回默认值。
  • fn foo_opt(&self) -> protobuf::Optional<&ProtoStr>:如果字段已设置,则返回一个带有 Set(value) 变体的 optional;如果未设置,则返回 Unset(default value)
  • fn set_foo(&mut self, val: impl IntoProxied<ProtoString>):设置字段的值。
  • 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<Bar>:如果字段已设置,则返回一个带有 Set(value) 变体的 optional;如果未设置,则返回 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<BarView>:如果字段已设置,则返回带有其 valueSet 变体。否则返回带有默认值的 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

具有隐式存在性(Presence)的字段(proto3 和 Editions)

隐式存在性意味着字段不区分默认值和未设置值。在 proto3 中,字段默认具有隐式存在性。在 Editions 中,您可以通过将 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:返回字段的当前值。如果字段未设置,则返回空字符串/空字节数组。
  • fn set_foo(&mut self, value: IntoProxied<ProtoString>):将字段设置为 value

对于 bytes 类型的字段,编译器将生成 ProtoBytes 类型。

支持 Cord 的奇异字符串和字节字段

[ctype = CORD] 使得字节和字符串可以作为 absl::Cord 存储在 C++ Protobufs 中。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<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() 返回 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<ProtoString>):将字段设置为 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

重复字段

对于任何重复字段定义,编译器都会生成三个相同的访问器方法,仅在字段类型上有所不同。

在 Editions 中,您可以使用 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>:返回底层重复字段的视图。
  • fn foo_mut(&mut self) -> RepeatedMut<'_, i32>:返回底层重复字段的可变句柄。
  • fn set_foo(&mut self, src: impl IntoProxied<Repeated<i32>>):将底层重复字段设置为 src 中提供的新的重复字段。

对于不同的字段类型,只有 RepeatedViewRepeatedMutRepeated 类型的相应泛型类型会改变。例如,给定一个 string 类型的字段,foo() 访问器将返回一个 RepeatedView<'_, ProtoString>

映射字段

对于此 map 字段定义:

map<int32, int32> weight = 1;

编译器将生成以下 3 个访问器方法:

  • fn weight(&self) -> protobuf::MapView<'_, i32, i32>:返回底层 map 的不可变视图。
  • fn weight_mut(&mut self) -> protobuf::MapMut<'_, i32, i32>:返回底层 map 的可变句柄。
  • fn set_weight(&mut self, src: protobuf::IntoProxied<Map<i32, i32>>):将底层 map 设置为 src

对于不同的字段类型,只有 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 分配

用于 arena 分配消息的 Rust API 尚未实现。

在内部,Protobuf Rust 在 upb 内核上使用 arena,但在 C++ 内核上不使用。但是,在 C++ 中通过 arena 分配的消息的引用(包括 const 和可变引用)可以安全地传递给 Rust 进行访问或修改。

服务(Services)

用于服务的 Rust API 尚未实现。