Getting Started
This tutorial mirrors the examples/basic_greeting
sample and shows, step by
step, how to install RpcNet, run the rpcnet-gen
CLI, and integrate the
generated code into your own project.
Step 0: Prerequisites
- Rust 1.75+ (
rustup show
to confirm) cargo
on yourPATH
- macOS or Linux (QUIC/TLS support is bundled through
s2n-quic
)
Step 1: Create a new crate
cargo new hello-rpc
cd hello-rpc
Step 2: Add the RpcNet runtime crate
cargo add rpcnet
RpcNet enables the high-performance perf
feature by default. If you need to
opt out (e.g. another allocator is already selected), edit Cargo.toml
:
[dependencies]
rpcnet = { version = "0.1", default-features = false }
You will also want serde
for request/response types, just like the example:
serde = { version = "1", features = ["derive"] }
Step 3: Install the rpcnet-gen CLI
Starting with v0.1.0, the CLI is included by default when you install rpcnet:
cargo install rpcnet # CLI automatically included!
Verify the install:
rpcnet-gen --help
You should see the full usage banner:
Generate RPC client and server code from service definitions
Usage: rpcnet-gen [OPTIONS] --input <INPUT>
Options:
-i, --input <INPUT> Input .rpc file (Rust source with service trait)
-o, --output <OUTPUT> Output directory for generated code [default: src/generated]
--server-only Generate only server code
--client-only Generate only client code
--types-only Generate only type definitions
-h, --help Print help
-V, --version Print version
Step 4: Author a service definition
Create src/greeting.rpc.rs
describing your protocol. The syntax is ordinary
Rust with a #[rpcnet::service]
attribute, so you can leverage the compiler and
IDE tooling while you design the API:
#![allow(unused)] fn main() { // src/greeting.rpc.rs use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GreetRequest { pub name: String, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GreetResponse { pub message: String, } #[derive(Serialize, Deserialize, Debug, Clone)] pub enum GreetingError { EmptyName, InvalidInput(String), } #[rpcnet::service] pub trait Greeting { async fn greet(&self, request: GreetRequest) -> Result<GreetResponse, GreetingError>; } }
Step 5: Generate client and server code
Point the CLI at the .rpc
file and choose an output directory. Here we mirror
examples/basic_greeting
by writing into src/generated
:
rpcnet-gen --input src/greeting.rpc.rs --output src/generated
The CLI confirms what it created:
π¦ Generating code for service: Greeting
β
Generated server: src/generated/greeting/server.rs
β
Generated client: src/generated/greeting/client.rs
β
Generated types: src/generated/greeting/types.rs
β¨ Code generation complete!
π Add the following to your code to use the generated service:
#[path = "generated/greeting/mod.rs"]
mod greeting;
use greeting::*;
Inspect the directory to see the modules that were createdβthis matches the
layout under examples/basic_greeting/generated/
:
src/generated/
βββ greeting/
βββ client.rs # async client wrapper for calling the service
βββ mod.rs # re-exports so `use greeting::*` pulls everything in
βββ server.rs # server harness plus `GreetingHandler` trait
βββ types.rs # request/response/error structs cloned from the .rpc file
client.rs
exposes GreetingClient
, server.rs
wires your implementation into
the transport via GreetingServer
, and types.rs
contains the shared data
structures.
Step 6: Wire the generated code into your project
Reference the generated module and bring the types into scope. For example,
in src/main.rs
:
#![allow(unused)] fn main() { #[path = "generated/greeting/mod.rs"] mod greeting; use greeting::client::GreetingClient; use greeting::server::{GreetingHandler, GreetingServer}; use greeting::{GreetRequest, GreetResponse, GreetingError}; use rpcnet::RpcConfig; }
From here there are two pieces to wire up:
-
Server β implement the generated
GreetingHandler
trait and launch the harness. This mirrorsexamples/basic_greeting/server.rs
:struct MyGreetingService; #[async_trait::async_trait] impl GreetingHandler for MyGreetingService { async fn greet(&self, request: GreetRequest) -> Result<GreetResponse, GreetingError> { Ok(GreetResponse { message: format!("Hello, {}!", request.name) }) } } #[tokio::main] async fn main() -> anyhow::Result<()> { let config = RpcConfig::new("certs/test_cert.pem", "127.0.0.1:8080") .with_key_path("certs/test_key.pem") .with_server_name("localhost"); GreetingServer::new(MyGreetingService, config).serve().await?; Ok(()) }
GreetingServer::serve
handles QUIC I/O, wiring your implementation to the generated protocol handlers.Tuning worker threads (optional). By default Tokio uses the number of available CPU cores. To override this for RpcNet services, set
RPCNET_SERVER_THREADS
and build your runtime manually:fn main() -> anyhow::Result<()> { let worker_threads = rpcnet::runtime::server_worker_threads(); let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(worker_threads) .enable_all() .build()?; runtime.block_on(async { // existing async server logic goes here Ok::<_, anyhow::Error>(()) })?; Ok(()) }
Run the binary with a custom thread count:
RPCNET_SERVER_THREADS=8 cargo run
Adjust the command if your server lives in a different binary target (for example
cargo run --bin my-server
).If you keep using the
#[tokio::main]
macro, Tokio will also honour the upstreamTOKIO_WORKER_THREADS
environment variable. -
Client β construct
GreetingClient
to invoke the RPC. Compare withexamples/basic_greeting/client.rs
:#[tokio::main] async fn main() -> anyhow::Result<()> { let config = RpcConfig::new("certs/test_cert.pem", "127.0.0.1:0") .with_server_name("localhost"); let server_addr = "127.0.0.1:8080".parse()?; let client = GreetingClient::connect(server_addr, config).await?; let response = client.greet(GreetRequest { name: "World".into() }).await?; println!("Server replied: {}", response.message); Ok(()) }
The generated client takes care of serialization, TLS, and backpressure while presenting an async function per RPC method.
Step 7: Build and run
Compile and execute as usual:
cargo build
cargo run
While you experiment, keep the reference example nearby:
ls examples/basic_greeting
# client.rs generated/ greeting.rpc.rs server.rs
Comparing your project with the example is a quick way to confirm the wiring matches what the CLI expects.
Where to go next
- Read the rpcnet-gen CLI guide for advanced flags such as
--server-only
,--client-only
, and custom output paths. - Explore the Concepts chapter for runtime fundamentals, server/client wiring, and streaming patterns.