In the previous article, we built a simple Thrift-based client/server application where the server ran on Java and the client ran on Python. Now, let’s take things a step further by introducing a third language: Rust. Rust’s emphasis on performance and safety makes it an intriguing choice for systems programming and high-performance back-end services.
In this article, we’ll demonstrate how to integrate Rust into our Thrift ecosystem. Depending on your preference, we can approach this in one of two ways:
- Add a Rust Client that calls the existing Java server (joining the Python client we already have).
- Convert the Server to Rust and interact with it from the Python client (or even maintain both Java and Python clients).
For clarity, we’ll choose the first scenario: adding a Rust client that communicates with our existing Java-based Thrift server. This approach reinforces how easily Thrift enables cross-language communication.
What We’re Building
We’ll reuse the Calculator service defined in our calculator.thrift
file.
The Java server still listens on port 9090, providing add
and subtract
methods. We’ll implement a Rust client that connects to this server and invokes
the same RPC calls, just as the Python client did previously.
High-Level Steps:
- Ensure we have the existing Java server running.
- Generate Rust code from the same
calculator.thrift
file. - Implement a Rust client that uses the generated code to make RPC calls.
- Run the Rust client and verify it communicates seamlessly with the Java server.
Prerequisites
Make sure you have:
- Existing Java Server Setup: From the previous article, you should have a Java server running on port 9090.
- Thrift Compiler: Installed and in your
PATH
. - Rust Toolchain: Installed via
rustup
and confirmcargo --version
works. - Thrift Rust Library: We’ll add dependencies to interact with Thrift in Rust.
Step 1: Revisit the Thrift IDL
We’ll use the same calculator.thrift
file we used previously:
namespace java com.example.calculator
namespace py calculator_py
namespace rs calculator_rs
service Calculator {
i32 add(1:i32 num1, 2:i32 num2)
i32 subtract(1:i32 num1, 2:i32 num2)
}
We’ve added a namespace rs calculator_rs
line to provide a namespace for Rust.
This is optional, but can help organize generated code.
Step 2: Generate Rust Code
Thrift provides a Rust code generator plugin. Run:
thrift --gen rs calculator.thrift
This will create a gen-rs
directory containing Rust modules for the
Calculator
service. The generated code will include client stubs that we can
call from Rust.
Step 3: Setting Up the Rust Project
Create a new Rust project for our client:
cargo new rust_client
cd rust_client
Your directory now contains:
rust_client/
├── Cargo.toml
└── src
└── main.rs
Cargo.toml
Edit Cargo.toml
to include Thrift-related dependencies. As of this writing, you
may need to specify a Thrift crate from crates.io or GitHub. For example:
[package]
name = "rust_client"
version = "0.1.0"
edition = "2021"
[dependencies]
thrift = "0.17.0" # Check crates.io for the latest version
(Adjust the version as needed based on what’s available.)
We’ll also need to ensure that our generated code can be found. Since the
generated Rust files are in gen-rs
, you can either move them into src
or
adjust your module path. For simplicity, let’s move the generated code into
src/gen
:
mkdir src/gen
cp -r ../gen-rs/calculator_rs src/gen/
Now we have src/gen/calculator_rs
containing mod.rs
, calculator.rs
, and so
forth.
In src/main.rs
, we’ll mod
the generated code:
mod gen {
pub mod calculator_rs;
}
// Bring items from the generated code into scope
use gen::calculator_rs::{CalculatorSyncClient, CalculatorSyncClientImpl};
use thrift::protocol::{TBinaryInputProtocol, TBinaryOutputProtocol};
use thrift::transport::{TBufferedReadTransport, TBufferedWriteTransport, TSocket};
Step 4: Implement the Rust Client
Now we write the code in main.rs
to connect to the server and invoke methods:
fn main() {
// Connect to the server
let mut transport = TSocket::new("127.0.0.1", 9090).expect("Unable to create socket");
transport.open().expect("Unable to open socket");
let (i_chan, o_chan) = transport.split().unwrap();
let i_transport = TBufferedReadTransport::new(i_chan);
let o_transport = TBufferedWriteTransport::new(o_chan);
let i_proto = TBinaryInputProtocol::new(i_transport, true);
let o_proto = TBinaryOutputProtocol::new(o_transport, true);
let mut client = CalculatorSyncClientImpl::new(i_proto, o_proto);
// Call add method
match client.add(10, 5) {
Ok(result) => println!("add(10, 5) = {}", result),
Err(e) => eprintln!("Error calling add: {:?}", e),
}
// Call subtract method
match client.subtract(10, 5) {
Ok(result) => println!("subtract(10, 5) = {}", result),
Err(e) => eprintln!("Error calling subtract: {:?}", e),
}
}
What’s happening here?
- We create a
TSocket
to connect to127.0.0.1:9090
, where the Java server is running. - We wrap this in
TBuffered
transports, then create binary protocols for input/ output. - We instantiate a
CalculatorSyncClientImpl
which comes from the generated code. - We call
add
andsubtract
just like we did in Python.
Step 5: Run the Rust Client
First, ensure your Java server (from the previous article) is running. In another terminal, run:
cargo run
You should see:
add(10, 5) = 15
subtract(10, 5) = 5
And your Java server console will log the requests as before.
Verifying Cross-Language Communication
We now have three languages in play: Java (server), Python (client), and Rust (client). All rely on the same Thrift IDL, ensuring compatibility and shared understanding of types and services.
This scenario demonstrates the core value of Thrift: simplicity and consistency in building multi-language RPC systems. By focusing on a single IDL, we’ve added Rust to our existing setup with minimal friction.
Next Steps
You’ve successfully integrated a third language (Rust) into the Thrift environment. This proves how flexible Thrift is for polyglot systems.
In the next articles, we’ll explore Protobuf and gRPC, and replicate a similar setup (Java server, Python and Rust clients) to see how it compares to Thrift. We’ll then discuss advanced topics and best practices for maintaining multi-language, multi-framework systems at scale.
Stay tuned for the next article, where we shift gears and introduce Protobuf, examining how to build a similar service and connect it with Java and Python, eventually adding Rust into the mix as well.