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:
- Confirm the Java server is running.
- Generate Rust code from
calculator.proto
. - Implement a Rust client that uses the generated code to perform RPC calls.
- 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
andprost
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 leveragetonic
andprost
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.