Pragmatic handle on setting up a gRPC Rust project, using Prost.
(and a little Tonic)
![]() |
![]() |
|---|
codeberg.org/arichtman/prost-crash-course
| Standards | Rust |
|---|---|
| gRPC | Tonic |
| Protobuf | Prost |
grpc.io

protobuf.dev
.proto files into Rust code using macrosdocs.rs/prost/latest/prost/
docs.rs/tonic/latest/tonic/
| Component | Purpose |
|---|---|
| gRPC | Transmission && service definition |
| Protobuf | Wire format && message definition |
| Prost | Code generation |
| Tonic | gRPC server |
Hello world:
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
..
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto
git submodule update --init --recursive --remote.gitignoreAll dependencies of tonic, tonic-prost, and prost flavors should be the same version.
Heed, lest your errors be incomprehensible, inconsistent, and numerous.
Build-time dependencies:
tonic-prost-buildprost-typesbuild.rs:
fn main() -> Result<..> {
let my_builder = tonic_prost_build::configure()
.out_dir("./src/protos");
my_builder.compile_protos(
&["proto/helloworld/examples/protos/helloworld.proto"],
&["proto"],
)?;
Ok(())
}
main.rs:
include!("protos/helloworld.rs");
Dependencies:
tonicprosttonic-prosttokio
macrosrt-multi-threadmain.rs:
// Imports + Empty struct
#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(&self, request: Request<HelloRequest>) ->
Result<Response<HelloReply>, Status> {
let reply = HelloReply {
// We must use .into_inner() as the fields
// of gRPC requests and responses are private
message: format!("Hello {}!", request.into_inner().name) };
Ok(Response::new(reply)) // Send back our formatted greeting
}
build.rs:
fn main() -> Result<..> {
let my_builder = tonic_prost_build::configure()
// We can dodge a full implementation
// by having Prost fill out some defaults
.generate_default_stubs(true)
..
}
main.rs:
// Imports etc
#[tokio::main]
async fn main() -> Result<..> {
let addr = "[::1]:50051".parse()?;
let greeter = MyGreeter::default();
Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;
Ok(())
}
$ grpcurl -plaintext -d '{"name": "Ariel"}' \
localhost:50051 helloworld.Greeter/SayHello
{
"message": "Hello Ariel!"
}
main.rs:
mod protos {
pub mod helloworld {
include!("./protos/helloworld.rs");
}
}
use protos::helloworld::HelloRequest;
fn main() {
let _ = HelloRequest {
name: "myName".to_string(),
};
}
build.rs:
fn main() -> Result<..> {
let my_builder = tonic_prost_build::configure()
.out_dir("./src/protos")
// Creates an includes file for easier import
.include_file("_includes.rs");
..
}
main.rs:
include!("protos/_includes.rs");
use crate::helloworld::HelloRequest;
build.rs:
fn main() -> Result<..> {
let mut my_builder = tonic_prost_build::configure()
// Removed -> .out_dir("./src/protos")
.include_file("_includes.rs")
..
}
main.rs:
include!(concat!(env!("OUT_DIR"), "/_includes.rs"));
use crate::helloworld::HelloRequest;
build.rs:
fn main() -> Result<..> {
let my_builder = tonic_prost_build::configure()
.type_attribute("HelloRequest",
r#"#[derive(Facet::Deserialize)]"#)
..
}
$ grpcurl -plaintext localhost:50051 list
grpc.reflection.v1.ServerReflection
helloworld.Greeter
Dependency: tonic-reflection
build.rs:
fn main() -> Result<..> {
let original_out_dir = PathBuf::from(var("OUT_DIR")?);
let my_builder = tonic_prost_build::configure()
// Deposit our reflection-enabling service
// descriptions alongside our code
.file_descriptor_set_path(
original_out_dir.join("descriptor.bin")
)
..
}
main.rs:
pub const DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("descriptor");
#[tokio::main]
async fn main() -> Result<..> {
..
let reflection_server = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(DESCRIPTOR_SET)
.build_v1()?;
Server::builder()
.add_service(GreeterServer::new(greeter))
.add_service(reflection_server)
..
}
$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
{
"status": "SERVING"
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
}
ServingStatus status = 1;
}
Dependency: tonic-health
build.rs:
fn main() -> Result<..> {
..
my_builder.compile_protos(
&[
// Added health.proto
"proto/grpc/src/proto/grpc/health/v1/health.proto",
..
],
&["proto"])?;
}
main.rs:
#[tokio::main]
async fn main() -> Result<..> {
..
let (health_reporter, health_service) = tonic_health::server::health_reporter();
health_reporter
.set_serving::<GreeterServer<MyGreeter>>()
.await;
Server::builder()
.add_service(health_service)
..
}
Generate client code (or not):
build.rs:
fn main() -> Result<..> {
let my_builder = tonic_prost_build::configure()
.build_client(false)
..
}
Viewing generated code: cargo-expand
Indirecting generated module names:
pub mod my_hello_world {
tonic::include_proto!("helloworld");
}
| More: | richtman.au |
|---|---|
![]() |