Skip to main content
Version: 1.2.0

gRPC Support

Fory can generate Rust gRPC service companions for schemas that define services. The generated code uses tonic for transport and Fory for request and response payload serialization.

Use this mode when every RPC peer is generated from the same Fory IDL, protobuf IDL, or FlatBuffers IDL and you want gRPC transport semantics with Fory payload encoding. Use standard protobuf gRPC code generation when clients or tools must consume protobuf message bytes directly.

Add Dependencies

Add tonic and bytes to the crate that compiles the generated service files. Fory Rust crates do not add gRPC as a hard dependency. Add tokio for async servers and clients, and tokio-stream when your service implementation needs to build streaming responses or request streams.

[dependencies]
fory = "1.2.0"
bytes = "1"
tonic = { version = "0.14", features = ["transport"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1"

Use dependency versions that are compatible with the rest of your service stack.

Define a Service

Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers rpc_service definitions. A Fory IDL service looks like this:

package demo.greeter;

message HelloRequest {
string name = 1;
}

message HelloReply {
string reply = 1;
}

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

Generate Rust model and gRPC companion code with --grpc:

foryc service.fdl --rust_out=./generated/rust --grpc

For this schema, the Rust generator emits:

FilePurpose
demo_greeter.rsFory model types and registration helpers
demo_greeter_service.rsAsync service trait and gRPC path constants
demo_greeter_service_grpc.rstonic client, server wrapper, and Fory codec

Add the generated files to your crate root:

pub mod demo_greeter;
pub mod demo_greeter_service;
pub mod demo_greeter_service_grpc;

Implement a Server

Implement the generated async trait and add the generated server wrapper to a normal tonic server.

use demo_greeter::{HelloReply, HelloRequest};
use demo_greeter_service::Greeter;
use demo_greeter_service_grpc::greeter_server::GreeterServer;
use tonic::{Request, Response, Status};

#[derive(Default)]
struct MyGreeter;

#[tonic::async_trait]
impl Greeter for MyGreeter {
async fn say_hello(
&self,
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
let request = request.into_inner();
Ok(Response::new(HelloReply {
reply: format!("Hello, {}", request.name),
}))
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
tonic::transport::Server::builder()
.add_service(GreeterServer::new(MyGreeter::default()))
.serve(addr)
.await?;
Ok(())
}

Generated request and response types are serialized by the generated service code, so service implementations do not perform manual Fory registration.

Create a Client

Use the generated tonic client:

use demo_greeter::HelloRequest;
use demo_greeter_service_grpc::greeter_client::GreeterClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;
let response = client
.say_hello(HelloRequest {
name: "Fory".to_string(),
})
.await?;
println!("{}", response.into_inner().reply);
Ok(())
}

tonic still owns channel configuration, TLS, deadlines, metadata, interceptors, and transport lifecycle.

Streaming RPCs

Fory service definitions can use unary, server-streaming, client-streaming, and bidirectional streaming RPC shapes:

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

Generated Rust code follows tonic conventions:

  • Unary methods use tonic::Request<T> and return tonic::Response<U>.
  • Server-streaming methods return a response whose inner value is a stream of Result<U, tonic::Status>.
  • Client-streaming and bidirectional methods receive tonic::Streaming<T>.
  • The generated client module exposes matching async methods for each service method.
  • The generated codec is used for every message frame, including streaming frames.

Use the generated trait signatures as the source of truth for concrete associated stream types in your service implementation:

use demo_greeter::{HelloReply, HelloRequest};
use demo_greeter_service::Greeter;
use std::pin::Pin;
use tokio_stream::{self as stream, Stream, StreamExt};
use tonic::{Request, Response, Status};

#[derive(Default)]
struct MyGreeter;

type ReplyStream =
Pin<Box<dyn Stream<Item = Result<HelloReply, Status>> + Send + 'static>>;

#[tonic::async_trait]
impl Greeter for MyGreeter {
type LotsOfRepliesStream = ReplyStream;
type ChatStream = ReplyStream;

async fn lots_of_replies(
&self,
request: Request<HelloRequest>,
) -> Result<Response<Self::LotsOfRepliesStream>, Status> {
let name = request.into_inner().name;
let replies = vec![
Ok(HelloReply {
reply: format!("Hello, {name}"),
}),
Ok(HelloReply {
reply: format!("Welcome, {name}"),
}),
];
Ok(Response::new(Box::pin(stream::iter(replies))))
}

async fn lots_of_greetings(
&self,
request: Request<tonic::Streaming<HelloRequest>>,
) -> Result<Response<HelloReply>, Status> {
let mut requests = request.into_inner();
let mut names = Vec::new();
while let Some(request) = requests.next().await {
names.push(request?.name);
}
Ok(Response::new(HelloReply {
reply: names.join(", "),
}))
}

async fn chat(
&self,
request: Request<tonic::Streaming<HelloRequest>>,
) -> Result<Response<Self::ChatStream>, Status> {
let replies = request.into_inner().map(|request| {
request.map(|request| HelloReply {
reply: format!("Hello, {}", request.name),
})
});
Ok(Response::new(Box::pin(replies)))
}
}

Generated clients return tonic streaming responses:

use demo_greeter::HelloRequest;
use demo_greeter_service_grpc::greeter_client::GreeterClient;
use tokio_stream as stream;

let mut client = GreeterClient::connect("http://[::1]:50051").await?;

let mut replies = client
.lots_of_replies(HelloRequest {
name: "Fory".to_string(),
})
.await?
.into_inner();
while let Some(reply) = replies.message().await? {
println!("{}", reply.reply);
}

let greetings = stream::iter(vec![
HelloRequest {
name: "Ada".to_string(),
},
HelloRequest {
name: "Grace".to_string(),
},
]);
let summary = client.lots_of_greetings(greetings).await?.into_inner();
println!("{}", summary.reply);

let chat_requests = stream::iter(vec![
HelloRequest {
name: "Fory".to_string(),
},
HelloRequest {
name: "RPC".to_string(),
},
]);
let mut chat = client.chat(chat_requests).await?.into_inner();
while let Some(reply) = chat.message().await? {
println!("{}", reply.reply);
}

The generated descriptors preserve the exact IDL service and method names for the gRPC path.

Thread Safety and Payload Types

Generated Rust gRPC payloads must be Send + 'static so tonic can move request and response values across async tasks. If a schema uses non-thread-safe reference metadata for a request or response type, Rust gRPC generation rejects that service. Use thread-safe reference shapes for gRPC payloads, or keep the non-thread-safe type out of the RPC boundary.

Operations

The generated service companion only supplies Fory serialization and tonic bindings. Operational behavior remains standard tonic behavior:

  • Deadlines and cancellations
  • TLS and authentication
  • Tower middleware and interceptors
  • Status codes and metadata
  • Channel and server lifecycle
  • Backpressure through async streams

Troubleshooting

Missing tonic or bytes Crates

Add the dependencies shown above to the crate that compiles the generated service files.

UNIMPLEMENTED

Confirm that the generated server wrapper was added with Server::builder().add_service(...), and that the client and server were generated from the same package, service, and method names.

Non-Thread-Safe Reference Errors During Code Generation

Rust gRPC payloads must be Send + 'static. Change the request or response schema to use thread-safe reference shapes, or wrap the non-thread-safe data in a type that is not part of the gRPC payload.

Protobuf Clients Cannot Decode the Service

Fory gRPC companions do not use protobuf wire encoding for messages. Use a Fory-generated client for Fory-generated services, or provide a separate protobuf service endpoint for generic protobuf clients.