#serialization #protobuf #codec #data-encoding #no-alloc

无std micropb

Rust Protobuf库,针对嵌入式系统和无std环境

1个不稳定版本

0.1.0 2024年7月14日

#124嵌入式开发

每月 34次下载
用于 micropb-gen

MIT/Apache

115KB
2K SLoC

Micropb

micropbRust实现的Protobuf格式,主要面向嵌入式环境。micropb.proto文件生成Rust模块。

与其他Protobuf库不同,micropb旨在针对没有分配器的受限环境。此外,它旨在高度可配置,允许用户根据字段粒度自定义生成的代码。因此,micropb与其他Protobuf库相比提供了一组不同的权衡。

优点

  • 支持无std和无分配环境
  • 生成的代码内存使用量减少
  • 允许使用静态分配的容器(如heaplessarrayvec)或来自alloc的动态分配容器
  • 代码生成器高度可配置
  • 字段可以有自定义处理程序,具有用户定义的编码和解码行为
  • 支持不同的编码和解码数据源,由PbEncoderPbDecoder特质抽象
  • 可以单独启用编码器或解码器

限制

  • 为了内存使用而牺牲了一些速度
  • 不支持Protobuf组
  • 未知字段和扩展只能通过自定义处理程序捕获
  • 不支持反射
  • 不执行循环检测,因此用户需要通过装箱字段或使用自定义处理程序自行断开循环引用
  • stringbytes、重复和map字段需要一些基本用户配置,如后文所述

概述

micropb项目由两个crate组成

  • micropb-gen:代码生成工具,从一组.proto文件生成Rust模块。将其作为构建依赖项包含在内。

  • micropb:Protobuf 传输数据的编码和解码例程。生成的模块将假定它被作为一个常规依赖项导入。

入门指南

micropb 载件包添加到您的 Cargo.toml

[dependencies]
micropb = "0.1"

[build-dependencies]
# Allow types from `heapless` to be used for container fields
micropb-gen = { version = "0.1", features = ["container-heapless"] }

micropb-gen 需要 protoc 来构建 .proto 文件,因此请安装 protoc 并将其添加到您的 PATH 中,然后在 build.rs 中调用代码生成器

fn main() {
    let mut gen = micropb_gen::Generator::new();
    // Compile example.proto into a Rust module
    gen.compile_protos(&["example.proto"], std::env::var("OUT_DIR").unwrap() + "/example.rs").unwrap();
}

最后,将生成的文件包含到您的代码中

// main.rs

use micropb::{PbRead, PbDecoder, MessageDecode, MessageEncode};

mod example {
    #![allow(clippy::all)]
    #![allow(nonstandard_style, unused, irrefutable_let_patterns)]
    // Let's assume that Example is the only message define in the .proto file that has been 
    // converted into a Rust struct
    include!(concat!(env!("OUT_DIR"), "/example.rs"));
}

fn main() {
    let mut example = example::Example::default();

    let data: &[u8] = &[ /* Protobuf data bytes */ ];
    // Construct new decoder from byte slice
    let mut decoder = PbDecoder::new(data);

    // Decode a new instance of `Example` into an existing struct
    example.decode(&mut decoder, data.len()).expect("decoding failed");

    // Use heapless::Vec as the output stream and build an encoder around it
    let mut encoder = PbEncoder::new(micropb::heapless::Vec::<u8, 10>::new());

    // Compute the size of the `Example` on the wire
    let size = example.compute_size();
    // Encode the `Example` to the data stream
    example.encode(&mut encoder).expect("Vec over capacity");
}

有关在嵌入式应用程序中 micropb 的具体示例,请参阅 arm-app

生成代码

消息

Protobuf 消息直接转换为 Rust 结构体,每个消息字段转换为 Rust 字段。

给定以下 Protobuf 定义

syntax = "proto3";

package example;

message Example {
    int32 f_int32 = 1;
    int64 f_int64 = 2;
    uint32 f_uint32 = 3;
    uint64 f_uint64 = 4;
    sint32 f_sint32 = 5;
    sint64 f_sint64 = 6;
    bool f_bool = 7;
    fixed32 f_fixed32 = 8;
    fixed64 f_fixed64 = 9;
    sfixed32 f_sfixed32 = 10;
    sfixed64 f_sfixed64 = 11;
    float f_float = 12;
    double f_double = 13;
}

micropb 将生成以下 Rust 结构体和 API

pub mod example_ {
    #[derive(Debug, Clone, PartialEq)]
    pub struct Example {
        pub f_int32: i32,
        pub f_int64: i64,
        pub f_uint32: u32,
        pub f_uint64: u64,
        pub f_sint32: i32,
        pub f_sint64: i64,
        pub f_bool: bool,
        pub f_fixed32: u32,
        pub f_fixed64: u64,
        pub f_sfixed32: u32,
        pub f_sfixed64: u64,
        pub f_float: f32,
        pub f_double: f64,
    }

    impl Default for Example {
        // ...
    }

    impl micropb::MessageDecode for Example {
        // ...
    }

    impl micropb::MessageEncode for Example {
        // ...
    }
}

生成的 MessageDecodeMessageEncode 实现为 Example 的解码、编码和计算大小提供了 API。

重复、mapstringbytes 字段

重复、mapstringbytes 字段需要 Rust "容器" 类型,因为它们可以包含多个元素或字符。通常使用标准类型如 StringVec,但在没有分配器的平台上不可用。在这种情况下,需要具有固定大小的静态分配容器。由于 Rust 中没有静态容器的既定标准,因此预计用户将使用自己的容器类型配置代码生成器。

例如,给定以下 Protobuf 定义

message Containers {
    string f_string = 1;
    bytes f_bytes = 2;
    repeated int32 f_repeated = 3;
    map<int32, int64> f_map = 4;
}

以及以下在 build.rs 中的配置

// Use container types from `heapless`, which are statically-allocated
gen.use_container_heapless();

// We can also use container types from `arrayvec` or `alloc`
/*
gen.use_container_arrayvec();
gen.use_container_alloc();
*/

// We can even use our own container types
/*
gen.configure(".",
    micropb_gen::Config::new()
        .string_type("crate::MyString")
        .vec_type("crate::MyVec")
        .map_type("crate::MyMap")
);
*/

// Since we're using static containers, we need to specify the max capacity of each field.
// For simplicity, configure capacity of all repeated/map fields to 4 and string/bytes to 8.
gen.configure(".", micropb_gen::Config::new().max_len(4).max_bytes(8));

micropb 将生成以下 Rust 定义

pub struct Containers {
    f_string: heapless::String<8>,
    f_bytes: heapless::Vec<u8, 8>,
    f_repeated: heapless::Vec<i32, 4>,
    f_map: heapless::FnvIndexMap<i32, i64, 4>,
}

期望容器类型实现 PbVecPbStringPbMapmicropb::container,具体取决于它用于哪种类型的字段。为了方便起见,micropb 随带为来自 heaplessarrayvecalloc(有关详细信息,请参阅功能标志)的类型提供了容器特质的内置实现。

可选字段

给定以下 Protobuf 消息

message Example {
    optional int32 f_int32 = 1;
    optional int64 f_int64 = 2;
    optional bool f_bool = 3;
}

micropb 生成以下 Rust API

#[derive(Debug, Clone, PartialEq)]
pub struct Example {
    pub f_int32: i32,
    pub f_int64: i64,
    pub f_bool: bool,

    pub _has: Example_::_Hazzer,
}

impl Example {
    /// Return reference to f_int32 as an Option
    pub fn f_int32(&self) -> Option<&i32>;
    /// Return mutable reference to f_int32 as an Option
    pub fn mut_f_int32(&mut self) -> Option<&mut i32>;
    /// Set value and presence of f_int32
    pub fn set_f_int32(&mut self, val: i32);
    /// Clear presence of f_int32
    pub fn clear_f_int32(&mut self);

    // Same APIs for other optional fields
}

pub mod Example_ {
    /// Tracks whether the optional fields are present
    #[derive(Debug, Default, Clone, PartialEq)]
    pub struct _Hazzer([u8; 1]);

    impl _Hazzer {
        /// Query presence of f_int32
        pub fn f_int32(&self) -> bool;
        /// Set presence of f_int32
        pub fn set_f_int32(&mut self);
        /// Clear presence of f_int32
        pub fn clear_f_int32(&mut self);
        /// Builder method that toggles on the presence of f_int32. Useful for initializing the Hazzer.
        pub fn init_f_int32(mut self) -> Self;

        // Same APIs for other optional fields
    }
}

与其它 Protobuf 库相比,micropb 的一个主要区别是它不会为可选字段生成 Option。这是因为当类型 T 没有无效表示或未使用位时,Option<T> 会占用额外的空间。这对于像 u32 这样的数值类型是正确的,这可能导致字段大小加倍。因此,micropb 通过一个名为“hazzer”的单独位域来跟踪所有可选字段的存在。如果 hazzer 足够小,可以放入消息结构的填充中,则不会增加任何大小。字段的存在可以通过直接查询 hazzer 或通过返回 Option 的消息 API 来查询。

默认情况下,装箱的可选字段使用 Option 来跟踪存在,而其他可选字段使用 hazzer。用户可以通过配置覆盖此行为。

必需字段

由于 Protobuf 必需字段的语义有缺陷,micropb 将像对待可选字段一样精确地处理必需字段。

枚举

Protobuf 枚举在 Rust 中被转换为“打开”的枚举,而不是常规的 Rust 枚举。这是因为 proto3 要求枚举能够存储未识别的值,这只能通过打开的枚举来实现。

例如,给定以下 Protobuf 枚举

enum Language {
    RUST = 0,
    C = 1,
    CPP = 2,
}

micropb 生成以下 Rust 定义

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Language(pub i32);

impl Language {
    pub const Rust: Self = Self(0);
    pub const C: Self = Self(1);
    pub const Cpp: Self = Self(2);
}

// Default impl that returns the default variant

// From<i32> impl

"enum" 类型实际上是一个封装整数的薄结构体。已知的枚举变体实现为常量。枚举值可以像常规 Rust 枚举一样创建和匹配。如果枚举值未知,则可以直接从结构体访问其底层整数值。

Oneof 字段

Protobuf oneof 被转换为真实的 Rust 枚举。枚举类型在消息的内部模块中定义,其类型名与 oneof 字段的名称相同。

例如,给定以下 Protobuf 定义

message Example {
    oneof number {
        int32 int = 1;
        float decimal = 2;
    }
}

micropb 生成以下定义

#[derive(Debug, Clone, PartialEq)]
pub struct Example {
    pub number: Option<Example_::Number>,
}

pub mod Example_ {
    #[derive(Debug, Clone, PartialEq)]
    pub enum Number {
        Int(i32),
        Decimal(f32),
    }
}

micropb 通过添加下划线将 Protobuf 包名称转换为 Rust 模块。例如,如果 Protobuf 文件有 package foo.bar;,则从该文件生成的所有 Rust 类型都将位于 foo_::bar_ 模块中。没有包指定符的 Protobuf 文件生成的代码将位于模块根目录。

嵌套类型

消息名称也通过添加下划线转换为 Rust 模块,因此 oneof 和嵌套消息/枚举在 Name_ 模块中定义,其中 Name 是消息名称。

解码器和编码器

micropb 不强制对 Protobuf 数据流使用特定表示。相反,数据流通过用户可以实现的读取和写入特性来表示,类似于标准库中的 ReadWrite。此外,micropb 提供了解码器和编码器类型,这些类型在上述特性之上工作,用于在 Protobuf 数据流和 Rust 类型之间进行转换。解码器和编码器类型是访问 Protobuf 数据的主要接口。

PbDecoderPbRead

输入数据流由PbRead特质表示,该特质默认在字节切片上实现。类型PbDecoder围绕输入流进行封装,并从中读取Protobuf结构,包括由micropb-gen生成的消息类型。

use micropb::{PbRead, PbDecoder, MessageDecode};
                                                                                                                                      
let data = [0x08, 0x96, 0x01, /* additional bytes */];
// Create decoder out of a byte slice, which is our input data stream
let mut decoder = PbDecoder::new(data.as_slice());

// ProtoMessage was generated by micropb
let mut message = ProtoMessage::default();
// Decode an instance of `ProtoMessage` from the data stream
message.decode(&mut decoder, data.len())?;

// We can also read Protobuf values directly from the decoder
let i = decoder.decode_int32()?;
let f = decoder.decode_float()?;

PbEncoderPbWrite

输出数据流由PbWrite特质表示,该特质默认在allocheaplessarrayvec的向量类型上实现,具体取决于启用了哪些功能标志。类型PbEncoder围绕输出流进行封装,并将Protobuf结构写入其中,包括由micropb-gen生成的消息类型。

use micropb::{PbEncoder, PbWrite, MessageEncode};
use micropb::heapless::Vec;
                                                                                                 
// Use heapless::Vec as the output stream and build an encoder around it
let mut encoder = PbEncoder::new(Vec::<u8, 10>::new());

// ProtoMessage was generated by micropb
let mut message = ProtoMessage::default();
message.0 = 12;
// Encode a `ProtoMessage` to the data stream
message.encode(&mut encoder)?;

// We can also write Protobuf values directly to the encoder
encoder.encode_int32(-4)?;
encoder.encode_float(12.491)?;

配置代码生成器

micropb的主要特性之一是其细粒度配置系统。使用它,用户可以控制如何从他们选择的单个Protobuf消息和字段生成代码。例如,如果我们有一个名为Example的消息,其字段名为f_int32,我们可以通过在我们的build.rs中放置以下内容来为它的类型生成Box<i32>,而不是i32

generator.configure(".Example.f_int32", micropb_gen::Config::new().boxed(true));

我们通过使用其完整的Protobuf路径来引用f_int32字段,即.Example.f_int32。这允许配置编译的.proto文件中的任何字段或类型。可能的配置选项包括:更改可选字段的表示形式、设置重复字段的容器类型、设置字段/类型属性以及更改整数类型的大小。

有关如何配置从Protobuf类型和字段生成的代码的更多信息,请参阅micropb-gen中的Generator::configureConfig

自定义字段

除了配置字段的生成方式外,用户还可以用他们自己的自定义类型替换字段的生成类型。例如,我们可以按以下方式为f_int32生成自定义类型:

gen.configure(".Example.f_int32", micropb_gen::Config::new().custom_field(CustomField::Type("MyIntField<'a>".to_owned())));

这将生成以下内容:

// If the custom field has a lifetime, then the message struct will also have a lifetime
pub struct Example<'a> {
    f_int32: MyIntField<'a>,
    // Rest of the fields
}

有关自定义字段的更多信息,请参阅micropb-gen中的Config::custom_field

功能标志

  • encode:启用对编码和计算消息大小的支持。如果禁用,则生成器应配置为不生成编码逻辑,方法是通过Generator::encode_decode。默认启用。
  • decode:启用对解码消息的支持。如果禁用,则生成器应配置为不生成解码逻辑,方法是通过Generator::encode_decode。默认启用。
  • enable-64bit:启用64位整数运算。如果禁用,则64位字段(如int64sint64)应将Config::int_size设置为32位或更小。对double字段没有影响。默认启用。
  • alloc:在来自allocVecStringBTreeMap上实现容器特质,允许它们用作容器字段。对应于micropb-gen中的Generator::use_container_alloc。还实现了PbWriteVec上。
  • std:启用标准库和alloc功能。
  • container-heapless:在VecStringIndexMap上实现容器特性,来自heapless,允许它们用作容器字段。对应于来自micropb-genGenerator::use_container_heapless。还实现了PbWriteVec上。
  • container-arrayvec:在ArrayVecArrayString上实现容器特性,来自arrayvec,允许它们用作容器字段。对应于来自micropb-genGenerator::use_container_arrayvec。还实现了PbWriteArrayVec上。

MSRV

micropb支持的Rust最旧版本是1.74.0

许可证

micropb根据MIT许可证和Apache许可证(版本2.0)的条款进行分发。

有关详细信息,请参阅LICENSE-APACHELICENSE-MIT

依赖关系

~100–280KB