Prost Crash Course

“That ain’t going to Protobuf out”

Aims/Outcomes

Pragmatic handle on setting up a gRPC Rust project, using Prost.

(and a little Tonic)

Self-paced

Commit history Repo URL QR

codeberg.org/arichtman/prost-crash-course

Component Overview

Standards Rust
gRPC Tonic
Protobuf Prost

gRPC

gRPC diagram

grpc.io

Protobuf

Protobuf example

protobuf.dev

Prost

  • Protobuf implementation for Rust
  • Turns .proto files into Rust code using macros

docs.rs/prost/latest/prost/

Tonic

Tonic banner

  • gRPC implementation for Rust
  • Based on Hyper, Tower, Tokio (and others)

docs.rs/tonic/latest/tonic/

Component summary

Component Purpose
gRPC Transmission && service definition
Protobuf Wire format && message definition
Prost Code generation
Tonic gRPC server

Interface

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

Protobuf setup

  • Submodules
  • Manual

Submodules

  • git submodule update --init --recursive --remote
  • Niche: Not supported by Jujutsu fully #494

Manual

  • Clone or curl (…and commit?)
  • Gotcha: .gitignore

⚠ BIG FUCKING WARNING ⚠

All dependencies of tonic, tonic-prost, and prost flavors should be the same version.

Heed, lest your errors be incomprehensible, inconsistent, and numerous.

Initializing the project

Build-time dependencies:

  • tonic-prost-build
  • prost-types

build.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");

Running

Dependencies:

  • tonic
  • prost
  • tonic-prost
  • tokio
    • macros
    • rt-multi-thread

Implementing the interface

main.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
  }

Easing into implementation

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)
    ..
}

Wiring up Tokio

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(())
}

Usage

$ grpcurl -plaintext -d '{"name": "Ariel"}' \
    localhost:50051 helloworld.Greeter/SayHello

{
  "message": "Hello Ariel!"
}

Improving the project

Moar includes

main.rs:

mod protos {
  pub mod helloworld {
    include!("./protos/helloworld.rs");
  }
}

use protos::helloworld::HelloRequest;

fn main() {
  let _ = HelloRequest {
    name: "myName".to_string(),
  };
}

Generated includes

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;

Hiding generated code

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;

Attribute macros

build.rs:

fn main() -> Result<..> {
  let my_builder = tonic_prost_build::configure()
    .type_attribute("HelloRequest",
      r#"#[derive(Facet::Deserialize)]"#)
    ..
}

It’s the little things 🏖️

Reflection

$ grpcurl -plaintext localhost:50051 list

grpc.reflection.v1.ServerReflection
helloworld.Greeter

Reflection

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")
      )
      ..
}

Reflection

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)
    ..
}

Health check

$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

{
  "status": "SERVING"
}

Health check

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;
}

Health check

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"])?;
}

Health check

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)
    ..
}

Client code generation

Generate client code (or not):

build.rs:

fn main() -> Result<..> {
    let my_builder = tonic_prost_build::configure()
        .build_client(false)
        ..
}

Bonuses

Viewing generated code: cargo-expand

Indirecting generated module names:

pub mod my_hello_world {
    tonic::include_proto!("helloworld");
}

Thank you

More: richtman.au
My webbed site