Rust 🤝🏾 Protobufs - Using Prost
The State of web services using Rust is something that I’ve always wanted to explore.
This particular side-quest started off as a thought process to build a gRPC server with Rust. So I started looking into crates that do code generation from protos.
Protobuf to Rust codegen
Some popular crates:
Some references:
TL;DR - Prost is cool. The codegen is neat and readable.
Note: If you want the code, it’s on GitHub.
Prost
After having chosen the crate, I took it for a spin.
Goal for today
- Define a simple protobuf
- Generate Rust definitions from it
Setup Rust project
Cargo is the dependency management tool for Rust. You can install it from here.
To create a new package with Cargo, use cargo new
:
cargo new rust-protobuf-example
cd rust-protobuf-example
Cargo will now automatically create a git repo. And populate it with bare-essentials.
Add dependencies
Let’s take a look at the Cargo.toml
file. You should see something like this.
[package]
name = "rust-protobuf-example"
version = "0.1.0"
authors = ["adselvar <adhita94@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Add the prost dependencies to the Cargo.toml
file.
[dependencies]
prost = "0.6"
# Only necessary if using Protobuf well-known types:
prost-types = "0.6"
[build-dependencies]
prost-build = { version = "0.6" }
The prost-build version tag should be the same as the prost version you’re using. You can take a look at the prost-build docs for more information.
Fun part
Copy the proto file into the src
directory.
touch src/message.proto
cat << EOF >> src/message.proto
syntax = "proto3";
package greeter;
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
EOF
Use Prost to compile the protobufs
touch build.rs
cat << EOF >> build.rs
extern crate prost_build;
fn main() {
prost_build::compile_protos(&["src/message.proto"],
&["src/"]).unwrap();
}
Any file named build.rs
and placed at the root of the package will act like a build-script. Cargo will compile and execute it before compiling the files inside the src/
directory.
prost_build::compile_protos(&[“src/message.proto”], &[“src/"]).unwrap();
This part uses the `prost_build` library's `compile_protos` function to compile protos into Rust structs.
[Source](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
#### Build the code to generate structs from protobufs
```bash
cargo build
Find the latest build’s hash and substitute it below.
cat target/debug/build/rust-protobuf-example-{hash}/out/greeter.rs
You should see the generated struct! 🎉🎉🎉🎉
Usage within package
You can now use the generated structs with the crate greeter
-> greeter::HelloRequest
or greeter::HelloResponse
.
Let’s try something simple with the generated structs.
We’re going to serialize the protobuf into a Rust datastructure and then de-serialize it back to a protobuf.
cat << EOF >> src/main.rs
use std::io::Cursor;
use prost::Message;
pub mod greeter {
include!(concat!(env!("OUT_DIR"), "/greeter.rs"));
}
pub fn create_hello_request(name: String) -> greeter::HelloRequest {
let mut hello_request = greeter::HelloRequest::default();
hello_request.name = name;
hello_request
}
pub fn serialize_greeter(hello: &greeter::HelloRequest) -> Vec<u8> {
let mut buf = Vec::new();
buf.reserve(hello.encoded_len());
hello.encode(&mut buf).unwrap();
buf
}
pub fn deserialize_greeter(buf: &[u8]) -> Result<greeter::HelloRequest, prost::DecodeError> {
greeter::HelloRequest::decode(&mut Cursor::new(buf))
}
fn main() -> Result<(), prost::DecodeError> {
let request = String::from("Hello, World!");
let greeter_request = create_hello_request(request);
let request_vector = serialize_greeter(&greeter_request);
let request_deserialized_result = match deserialize_greeter(&request_vector) {
Ok(request_deserialized_result) => request_deserialized_result,
Err(e) => return Err(e),
};
println!("{:#?}", request_deserialized_result);
Ok(())
}
pub mod greeter { include!(concat!(env!(“OUT_DIR”), “/greeter.rs”)); }
This gets the greeter structs from the `OUT_DIR` and loads it as a module.
> ```rust
let request = String::from("Hello, World!");
The string which is to be encapsulated in the proto message HelloRequest
.
let greeter_request = create_hello_request(request); let request_vector = serialize_greeter(&greeter_request);
A mutable struct for the protobuf is initialized. And the string input for the `create_hello_request` function is set as a member variable to fill-up the protobuf message.
The `serialize_greeter` function takes in the struct and returns a vector.
> ```rust
let request_deserialized_result = match deserialize_greeter(&request_vector) {
Ok(request_deserialized_result) => request_deserialized_result,
Err(e) => return Err(e),
};
Now, we use the deserialize_greeter
function to convert the vector back into the protobuf struct. Aaaaand, that’s it. We’re done for the day.
If you look carefully, Prost does only the protobuf messages and not services. So next time, we’re going to look at how to build a gRPC server and client with Rust.