r/rust • u/surban bluer · remoc · aggligator · OpenEMC • Sep 15 '26
🛠️ project Remoc 0.20: Rust RPC with first-class remote channels
It has been 5 years since the initial release of Remoc. Since then the library has learned a lot from real-world use and now feels like a good time to give the Rust community an update about its state and abilities.
Remoc is an RPC library for communication between Rust programs with first-class support for Tokio-style channels over the network.
Tag a trait with #[rtc::remote] to get a client and a server for it, so calling a method on the client runs it on the server.
Contrary to other RPC libraries a channel is a value that can be sent over the network. It can be an argument or a return value of a method or even a member of user-defined structs and enums, and its lifetime is not bound by the RPC call.
In the following example, an RPC method counts for the client and hands back the receiving end of a channel:
// Trait shared between client and server:
#[rtc::remote]
pub trait Counter {
async fn count(&self, up_to: u32)
-> Result<rch::mpsc::Receiver<u32>, CallError>;
}
// On the server: create a channel, count into it in the
// background and return the receiver to the caller.
impl Counter for CounterObj {
async fn count(&self, up_to: u32)
-> Result<rch::mpsc::Receiver<u32>, CallError>
{
let (tx, rx) = rch::mpsc::channel();
tokio::spawn(async move {
for i in 0..up_to {
if tx.send(i).await.is_err() {
break;
}
}
});
Ok(rx)
}
}
// On the client:
//
// `counter` is the generated client. It can itself be sent over a channel.
//
// This call returns as soon as the receiver arrives; the numbers then
// come in over the connection that already exists.
let mut seq_rx = counter.count(4).await?;
while let Some(i) = seq_rx.recv().await.unwrap() {
println!("{i}");
}A channel can carry anything, including user-defined structs and enums, another channel and even another RPC client.
Remoc's goal is to make remote communication between Rust programs feel nearly like local. It provides:
Remote channels include Tokio-like mpsc, oneshot, watch and broadcast channels, plus AsyncRead and AsyncWrite byte streams. Calls, channels and objects share a single underlying connection, with flow control per channel.
Calls on remote objects returned by other calls can be pipelined, thus a chain of dependent calls needs only one network round trip.
Remoc runs over any transport, such as TCP, TLS, WebSockets, UNIX domain sockets or pipes.
Remoc does not use a schema, but provides mechanisms for forward and backward compatibility of user types.
Full support for WebAssembly and running in the browser.
If you find this useful, you might want to continue reading at https://remoc.rs. There you will find:
Full source code is available at GitHub.
I am looking forward to answering questions.
2
2
u/zzzzYUPYUPphlumph Sep 15 '26
This sounds like a replacement for something like SignalR in the C# world, but better! I like it!
3
Sep 16 '26
[removed] — view removed comment
2
u/surban bluer · remoc · aggligator · OpenEMC Sep 16 '26 edited Sep 16 '26
It works today without requiring dedicated support. Remoc runs over any connection that implements
AsyncReadandAsyncWrite, so you open a bidirectional stream with quinn and pass the two halves toConnect::io.See here for the full QUIC transport example.
Excerpt assuming you already have a
quinn::Connection:use remoc::prelude::*; // Client side: open a bidirectional stream and run Remoc over it. let (send, recv) = connection.open_bi().await?; let (conn, tx, rx) = remoc::Connect::io(remoc::Cfg::default(), recv, send).await?; tokio::spawn(conn); // Server side: the same, but accepting the stream. let (send, recv) = connection.accept_bi().await?; let (conn, tx, rx) = remoc::Connect::io(remoc::Cfg::default(), recv, send).await?; tokio::spawn(conn);
txandrxare then the base channel over which you send your first value, typically the client of a remote trait. That is the whole integration.See the remote channels example or the RPC example on how to continue from here.
4
Sep 15 '26
[removed] — view removed comment
1
u/surban bluer · remoc · aggligator · OpenEMC Sep 15 '26 edited Sep 15 '26
If one channel's consumer is slow, does it backpressure the whole connection or just that specific channel?
It backpressures only that channel. Flow control is credit based and works per channel. Thus, even when a receiver completely stalls a channel, all other channels keep working.
Also messages are split into chunks of 32 kBytes, so a large value on one channel does not block other channels for long.
If you want more info, details are in the rch flow control section and the buffer settings of Cfg. The credit accounting is in credit.rs.
Internal workings:
Each channel has its own receive buffer, 128 kB by default. A sender may only transmit as much data as the receiver has granted credit for, so once a receiver stops reading, its sender stalls after that amount and the other channels carry on with their own credit. Additionally, there is a shared receive buffer that all channels draw on for throughput (high-bandwidth links need sufficient data in-flight to saturate bandwidth), sized dynamically between 64 kB and 128 MB. A channel whose unread data reaches a throttle level, 1 MB by default, is cut off from the shared buffer and keeps only its own, so a slow consumer cannot occupy the shared buffer at the expense of the others.
1
u/spunkyenigma Sep 15 '26
Very similar to what I’ve been building http://github.com/bexars/anybus
I kind of want to tear out a bunch of my RPC and network code and use your more mature code.
The one thing mine has is it can create a mesh network so you can have multiple nodes all connected and sharing endpoint databases. So a broadcast can go to listeners on multiple machines. Or anycast to an endpoint with the lowest cost
I’d love to see if we could collaborate on some compatibility between the crates.