Back in 2018 I published a couple of posts around Using Protocol Buffers and Using gRPC with Protocol Buffers.
For this post we’re going to look at using gRPC and Protocol Buffers from Rust.
Getting Started
Before we begin to do anything in Rust we’ll need protoc on our machine, so checkout https://github.com/protocolbuffers/protobuf/releases for a release.
Note: On Windows we can just use winget install protobuf then run protoc –version to check it was installed.
Also ensure protoc.exe is in your path or set-up via your development tools – in my case I’m using JetBrains RustRover and added the environment variable PROTOC to the project configuration with a value of C:\Users\{your-username}\AppData\Local\Microsoft\WinGet\Links\protoc.exe as I installed on Windows via winget.
Next, let’s create the bare bones project.
Obviously change the dependencies versions to suit.
The build-dependencies will generate the source code from our .proto file.
Creating the proto file(s)
Let’s create a simple proto file.
- Create a folder, we’ll use a standard name, so ours is called proto off of the root folder
- Create a file names hello.proto and copy the code below into it (this is a sort of “Hello World” of proto files)
syntax = "proto3";
package hello;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
- To generate the code from the .proto, create a build.rs file with the following code
use tonic_prost_build::configure;
fn main() -> Result<(), Box<dyn std::error::Error>> {
configure()
.out_dir("src/generated")
.compile_protos(&["proto/hello.proto"], &["proto"])
.unwrap();
Ok(())
}
I had problems getting the build.rs to generate source for the proto file, so you might need to create a folder /src/generated before running the command and the proto folder is off on the project root i.e. alongside the src folder as mentioned previous, so ensure that’s correct.
To generate the source files for the project we can run the build from a tool such as RustRover or use cargo build from your project folder.
I’m not going to include the whole file that’s generated but you should see bits like the following
/// Generated client implementations.
pub struct HelloRequest {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HelloReply {
#[prost(string, tag = "1")]
pub message: ::prost::alloc::string::String,
}
pub mod greeter_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
As you can see we have representations of the request and reply from the .proto file.
I also added a mod.rs file to the src/generated folder which looks like this
pub mod hello;
This will make our generated source available to the main.rs file for importing.
This example exists on the tonic GitHub repo https://github.com/hyperium/tonic/tree/master/examples/src/helloworld, I hadn’t realised when I started this but I would suggest you check out their examples.
I’m going to place everything in the main.rs file for simplicity, but ofcourse the code should be split into client, server and main code when using in anything other than such a simple example, but let’s look at each section of code separately…
We have a GreeterSever generated from our proto code but we need to create the equivalent of an “endpoint” or “service”, so we’ll create service with the following code
#[derive(Default)]
pub struct GreeterService {}
#[tonic::async_trait]
impl Greeter for GreeterService {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
let name = request.into_inner().name;
let reply = HelloReply {
message: format!("Hello, {}!", name),
};
Ok(Response::new(reply))
}
}
This essentially responds to a HelloRequest returning a HelloReply – as mentioned, think of this as your service endpoint.
We’re going to need to create a server, which will look like this
async fn grpc_server() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let greeter = GreeterService::default();
println!("Server listening on {}", addr);
Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;
Ok(())
}
Notice that we are indeed creating a server, listening on a port. We supply the service to the Server::builder via add_service and that’s pretty much it.
Next we’re going to need a client to send some request, so here’s an example
async fn grpc_client() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;
let request = Request::new(HelloRequest {
name: "PutridParrot".into(),
});
let response = client.say_hello(request).await?;
println!("Response is {:?}", response.into_inner().message);
Ok(())
}
Ofcourse the client connects to the server, creates a request and sends it to the server via the say_hello function. This is a call via the generated code, not to be confused with the GreeterService function of the same name, however ofcourse this will then go via the wire to the server and be handled by the GreeterService’s say_hello function.
We await the response and println! it.
Now let’s just create a simple main/entry point to run the server then run the client and get a response (again this is made simple just for ease of using the one file (main.rs) and ofcourse should be separated in a real world use.
Note: I’ll also include all the use code as well in this sample
mod generated;
use tokio::spawn;
use tokio::time::{sleep, Duration};
use tonic::{Request, Response, Status};
use tonic::transport::Server;
use crate::generated::hello::greeter_client::GreeterClient;
use crate::generated::hello::greeter_server::{Greeter, GreeterServer};
use crate::generated::hello::{HelloReply, HelloRequest};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
spawn(async {
grpc_server().await.unwrap();
});
sleep(Duration::from_millis(500)).await;
grpc_client().await?;
Ok(())
}
Use cargo run or run via RustRover or your preferred development tools and you should see
Server listening on [::1]:50051
Response is "Hello, PutridParrot!"
Code
Code is available for GitHub. Don’t forget to install protoc and ensure the path is set if you wish to run the code.