More than a byte pipe
The operating system gives you a byte stream for inter-process communication, but what the processes actually need are commands with typed arguments and results, progress and event notifications, and an interface that can grow without breaking installed clients.
The usual approaches each carry a burden. A hand-rolled protocol over a UNIX
socket means framing, a message enum and dispatch code that grow with every
command. JSON over stdin and stdout is quick to start with, but untyped and
without a versioning story. D-Bus integrates well with system services, but
brings a bus daemon and its own type system. gRPC over a UNIX socket works,
but maintaining a .proto schema and generated code is a lot of
machinery for two programs that you compile yourself.
Your Rust traits as the interface
With Remoc, the daemon's interface is a Rust trait carrying
#[rtc::remote],
which generates the client and server implementations. Arguments and results
are ordinary Serde types. Where the daemon needs to push, a method returns a
channel, for example a stream of log lines, progress values or state
changes; the channels work in both
directions over the same socket.
Since Remoc serializes with the Postbag codec, the interface can evolve. You can add fields, methods and enum variants and an updated daemon keeps understanding an older installed client, without version negotiation code.
The transport is whatever local byte stream suits you: a UNIX socket, the stdin and stdout pipes of a child process, a named pipe on Windows or a TCP connection on localhost. Remoc implements no transport itself, so nothing else is pulled in.
Connecting to the daemon
The client connects to the daemon's socket and receives a typed client for the daemon's interface trait. One call sets up the connection and delivers it.
use tokio::net::UnixStream;
let socket = UnixStream::connect("/run/myapp.sock").await?;
let (socket_rx, socket_tx) = socket.into_split();
// Receive the client for the daemon's interface trait.
let mut daemon: DaemonClient =
remoc::Connect::io(remoc::Cfg::default(), socket_rx, socket_tx).consume().await?;
daemon.status().await?;
Worked transport examples, including pipes to a child process →
Questions
Does this work on Windows?
Yes. Remoc runs over anything implementing AsyncRead and AsyncWrite, which includes Tokio's named pipes on Windows. A TCP connection on localhost works on every platform.
Can I talk to a child process I spawned?
Yes. The standard input and output pipes of a child process form a byte stream that Remoc can run over. The transports module of the documentation contains a worked example for this.
Can several clients connect to one daemon?
Yes. The daemon accepts connections on its socket as usual and establishes one Remoc connection per client. Each client gets its own channels and trait clients, isolated from the others.
What happens when daemon and client are different versions?
Remoc serializes with the Postbag codec, which supports schema evolution. Fields and enum variants can be added, removed and reordered, so an updated daemon keeps working with an already installed older client and vice versa.
How does this compare to D-Bus?
D-Bus is the right choice for talking to system services and desktop components, since it is a shared bus with cross-language support. Remoc fits when both processes are your own Rust code and you want your Rust types as the interface, without a bus daemon in between.
Can a non-Rust process participate?
No. The protocol is defined by Rust types serialized with Serde, so both processes must be written in Rust.
Where to go next
The remote trait calling example shows a complete service with client and server crates. If the daemon needs to notify its clients, server push and callbacks covers the patterns, and channels between processes explains the underlying channel model.