micropb-gen compiles .proto files into Rust code. It is intended to be used inside
build.rs for build-time code generation.
Unlike other Protobuf code generators in the Rust ecosystem, micropb is aimed for constrained
environments without an allocator.
The entry point of this crate is the [Generator] type.
For info on the "library layer" of micropb-gen, see [micropb].
Getting Started
Add micropb crates to your Cargo.toml:
[dependencies]
# Allow types from `heapless` to be used for container fields
micropb = { version = "0.3.0", features = ["container-heapless"] }
[build-dependencies]
micropb-gen = "0.3.0"
Then, place your .proto file into the project's root directory:
// example.proto
message Example {
int32 field1 = 1;
bool field2 = 2;
double field3 = 3;
}
micropb-gen requires protoc to build .proto files, so install
protoc and add it to your PATH, then invoke the
code generator in build.rs:
let mut gen = new;
// Compile example.proto into a Rust module
gen.compile_protos.unwrap;
Finally, include the generated file in your code:
// main.rs
use ;
let example = Example ;
// Maximum size of the message type on the wire, scaled to the next power of 2 for heapless::Vec
const CAPACITY: usize = MAX_SIZE.unwrap.next_power_of_two;
// For the example message above we can use a smaller capacity
// const CAPACITY: usize = 32;
// Use heapless::Vec as the output stream and build an encoder around it
let mut encoder = new;
// Compute the size of the `Example` on the wire
let _size = example.compute_size;
// Encode the `Example` to the data stream
example.encode.expect;
// Decode a new instance of `Example` into a new struct
let mut new = default;
let data = encoder.as_writer.as_slice;
new.decode_from_bytes.expect;
assert_eq!;
Messages
Protobuf messages are translated directly into Rust structs, and each message field translates into a Rust field.
Given the following Protobuf definition:
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-gen will generate the following Rust structs and APIs:
The generated MessageDecode and
MessageEncode implementations provide APIs for decoding, encoding,
and computing the size of Example.
Optional Fields
While the obvious choice for representing optional fields is [Option], this is not actually
ideal in embedded systems because Option<T> actually takes up twice as much space as T for
many types, such as u32 and i32. Instead, micropb tracks the presence of all optional
fields of a message in a separate bitfield called a hazzer, which is usually small enough to
fit into the padding. Field presence can either be queried directly from the hazzer or from
message APIs that return Option.
For example, given the following Protobuf message:
message Example {
optional int32 f_int32 = 1;
optional int64 f_int64 = 2;
optional bool f_bool = 3;
}
micropb-gen generates the following Rust struct and APIs:
// trait impls, decode/encode logic, etc
Note on Initialization
A field will be considered empty (and ignored by the encoder) if its bit in the hazzer is not set, even if the field itself has been written. The following is an easy way to initialize a message with all optional fields set:
default.init_f_int32.init_f_int64.init_f_bool
Alternatively, we can initialize the message using the constructor:
Example
Fallback to [Option]
By default, optional fields are represented by bitfields, as shown above. If an optional field
is configured to be boxed via [Config::boxed], it will instead be represented as an Option,
because Option<Box<T>> doesn't take up extra space compared to Box<T>. To override these default
behaviours, see [Config::optional_repr].
Required fields
The generator treats required fields exactly the same way it treats optional fields.
Oneof Fields
Protobuf oneofs are translated into Rust enums. The enum type is defined in an internal module under the message, and its type name is the same as the name of the oneof field.
For example, given this Protobuf definition:
message Example {
oneof number {
int32 int = 1;
float decimal = 2;
}
}
micropb-gen generates the following definition:
Repeated, map, string, and bytes Fields
Repeated, map, string, and bytes fields need to be represented as Rust "container" types,
since they contain multiple elements or bytes. Normally standard types like String and Vec
are used, but they aren't available in no-alloc environments. Instead, we need stack-allocated
containers with fixed capacity. Since there is no defacto standard for such containers in Rust,
users are expected to configure the code generator with their own container types (see
[Config] for more details).
For example, given the following Protobuf definition:
message Containers {
string f_string = 1;
bytes f_bytes = 2;
repeated int32 f_repeated = 3;
map<int32, int64> f_map = 4;
}
and the following configuration in build.rs:
let mut gen = new;
// Configure our own container types
gen.configure;
// We can also use container types from `heapless`, which have fixed capacity
gen.use_container_heapless;
// Same shorthand exists for containers from `arrayvec` or `alloc`
// gen.use_container_arrayvec();
// gen.use_container_alloc();
// Since we're using fixed 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;
The following Rust struct will be generated:
# use heapless as heapless;
For decoding, container types should implement PbVec (repeated fields),
PbString, PbBytes, or PbMap
For convenience, [micropb] comes with built-in implementations of the container traits for
types from heapless,
arrayvec, and
alloc, as well as implementations on [u8; N] arrays and
FixedLenString.
For encoding, container types need to dereference into &[T] (repeated fields), &str, or
&[u8]. Maps just need to iterate through key-value pairs.
Message Lifetime
A message struct may have up to one lifetime parameter. micropb-gen automatically generates
the lifetime parameter for each message by checking if there's a lifetime in any of the fields.
For example, given the Protobuf file from the previous section and the following build.rs
config:
# use ;
# let mut gen = new;
// Use `Cow` as container type with lifetime of 'a
gen.configure;
// Use a custom type for the `f_map` field, also with lifetime of 'a
gen.configure;
micropb-gen generates the following struct:
# extern crate alloc;
# ;
Note
micropb-gen cannot detect if a message field contains a lifetime or not, so any field that's
a message with a lifetime must be configured with field_lifetime.
Enums
Protobuf enums are translated into "open" enums in Rust, rather than normal Rust enums. This is because proto3 requires enums to store unrecognized values, which is only possible with open enums.
For example, given this Protobuf enum:
enum Language {
RUST = 0,
C = 1,
CPP = 2,
}
micropb-gen generates the following Rust definition:
;
Packages and Modules
micropb-gen translates Protobuf package names into Rust modules by appending an underscore.
For example, given the following Protobuf file:
package foo.bar;
// Protobuf contents
The generated Rust file will look like:
If a Protobuf file does not have a package specifier, the generated code will instead live in the root module
Message names are also translated into Rust modules by appending an underscore. For example,
code generated from oneofs and nested messages within the Name message will live in the
Name_ module.
Configuring the Generator
One of micropb-gen's main features is its granular configuration system, which allows users
to control how code is generated at the level of the module, message, or even individual
fields. See [Generator::configure] and [Config] for more info on the configuration system.
Notable Configurations
-
Integer size: Controls the width of the integer types used to represent integer fields. This can also be done for enums.
-
Custom fields: Substitute your own type into the generated code, allowing complete control over the encode and decode behaviour. Can be applied to normal fields or unknown fields.
-
Max container size: Specify the max capacity of
string/bytesfields as well as repeated fields, which is necessary when using fixed-capacity containers likeArrayVec.
Configuration Files
Configurations can be stored in TOML files rather than in build.rs. See
[Generator::parse_config_file] for more info.