Protobuf in Rust: Integrating Another Language into the gRPC Ecosystem

Posted on in programming

cover image for article

We’ve now built similar RPC setups using Thrift and Protobuf/gRPC in Java and Python. Following the pattern established with Thrift, let’s integrate Rust into our Protobuf/gRPC ecosystem. Rust’s performance and safety features make it a popular choice for high-performance back-ends, and this exercise will show how to seamlessly incorporate it into an existing Protobuf-based environment.

In this article, we’ll add a Rust component that communicates with our existing Java gRPC server. This mirrors what we did on the Thrift side, reinforcing our understanding of how these tools behave across multiple languages and frameworks.

What We’re Building

We’ll reuse the Calculator service defined in our calculator.proto file from the previous article. The Java server (running on port 50051) still serves the Add and Subtract RPC methods. Our goal is to create a Rust client that connects to this server and executes these operations, just as our Python client did.

High-Level Steps:

  1. Confirm the Java server is running.
  2. Generate Rust code from calculator.proto.
  3. Implement a Rust client that uses the generated code to perform RPC calls.
  4. Run the Rust client and verify successful communication.

Prerequisites

Ensure you have:

  • Working Java gRPC server: From the previous article, running on port 50051.
  • Protobuf and gRPC tools: protoc and gRPC plugins.
  • Rust Toolchain: Installed via rustup.
  • Rust gRPC Libraries: We’ll use the tonic and prost crates, popular for gRPC and Protobuf in Rust.

Step 1: Revisit the Protobuf Definition

We’ll reuse calculator.proto from before. No changes needed:

syntax = "proto3";

package calculator;

service Calculator {
  rpc Add (ArithmeticRequest) returns (ArithmeticResponse);
  rpc Subtract (ArithmeticRequest) returns (ArithmeticResponse);
}

message ArithmeticRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message ArithmeticResponse {
  int32 result = 1;
}

Step 2: Generate Rust Code

For Rust, we’ll use tonic and prost which rely on protoc to generate .rs files. You can set this up in a build.rs file or run the commands directly, but the simplest approach for this article is to rely on tonic-build in a build.rs script.

Project Setup

Create a new Rust project:

cargo new rust_protobuf_client
cd rust_protobuf_client

Your directory now:

rust_protobuf_client/
├── Cargo.toml
└── src
    └── main.rs

Cargo.toml:

[package]
name = "rust_protobuf_client"
version = "0.1.0"
edition = "2021"

[dependencies]
tonic = "0.9"
prost = "0.11"
prost-types = "0.11"

[build-dependencies]
tonic-build = "0.9"

build.rs

Create a build.rs file at the project root:

fn main() {
    tonic_build::configure()
        .build_server(false) // We only need client stubs
        .compile(&["../calculator.proto"], &["../"])
        .unwrap();
}

This tells tonic-build to compile calculator.proto into Rust code, placing the generated code in OUT_DIR during the build process. We assume calculator.proto is one directory above the project root (../calculator.proto) — adjust paths as needed.

main.rs

mod calculator {
    tonic::include_proto!("calculator");
}

use calculator::calculator_client::CalculatorClient;
use calculator::ArithmeticRequest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = CalculatorClient::connect("http://127.0.0.1:50051").await?;

    let add_request = tonic::Request::new(ArithmeticRequest { num1: 10, num2: 5 });
    let add_response = client.add(add_request).await?;
    println!("add(10, 5) = {}", add_response.into_inner().result);

    let subtract_request = tonic::Request::new(ArithmeticRequest { num1: 10, num2: 5 });
    let subtract_response = client.subtract(subtract_request).await?;
    println!("subtract(10, 5) = {}", subtract_response.into_inner().result);

    Ok(())
}

What’s happening here?

  • tonic::include_proto!("calculator") loads the generated code at runtime.
  • We create a CalculatorClient from the generated stubs.
  • We send requests and await responses asynchronously using tokio.
  • The results are printed, similar to what we saw with Python and Java.

Step 3: Build and Run the Rust Client

Before running, ensure you have protoc and protoc-gen-grpc-java installed so tonic-build can generate the files. Run:

cargo build
cargo run

Make sure the Java gRPC server is running on 127.0.0.1:50051. If all is well, you’ll see:

add(10, 5) = 15
subtract(10, 5) = 5

This means our Rust client successfully communicated with the Java server over gRPC.

Cross-Language Success

We now have a Java server and two distinct clients: Python and Rust. Both rely on the .proto file to ensure consistent data types and service definitions. This scenario showcases the same cross-language interoperability we achieved with Thrift, but this time using Protobuf and gRPC.

Comparing Thrift and Protobuf/gRPC with Rust

  • Setup Complexity:
    Both Thrift and Protobuf require code generation. With Protobuf and gRPC, we leverage tonic and prost for Rust integration, which feels quite idiomatic in Rust’s async ecosystem.
  • Feature-Richness:
    gRPC offers advanced features like streaming and built-in load balancing primitives when combined with the broader ecosystem. Thrift is more minimalistic but integrated.
  • Performance and Safety:
    Both frameworks work well with Rust, maintaining performance and memory safety. The choice often comes down to ecosystem fit and whether you prefer Thrift’s integrated approach or Protobuf’s modular style.

Next Steps

We’ve now seen how to integrate three languages (Java, Python, Rust) using both Thrift and Protobuf/gRPC. In the final article of this series, we’ll discuss best practices, advanced topics like schema evolution, performance considerations, testing strategies, and how to choose the right tool for your use case.


Stay tuned for the next (and final) article, where we’ll wrap up this series by exploring best practices and helping you decide which framework and language combination is the best fit for your projects.

Part 5 of the Cross-Language Client/Server Applications with Thrift and Protobuf series

Slaptijack's Koding Kraken