Muxio: A High-Performance Multiplexing and RPC Framework for Rust
DRAFT -- WORK IN PROGRESS
Muxio provides a robust and flexible foundation for building high-performance, transport-agnostic, and runtime-agnostic services in Rust. It offers a layered architecture that cleanly separates low-level binary stream multiplexing from high-level RPC logic, enabling you to create custom-tailored communication protocols.
What is Muxio?
At its core, Muxio is a set of layered components that enable multiplexed data transmission over a single, unified connection. Think of it as a toolkit for managing multiple, independent data streams, such as RPC calls, file transfers, and real-time data feeds—without the overhead of multiple connections.
On top of this multiplexing layer, Muxio offers a minimal, unopinionated RPC framework. While you can use it directly, it's often more practical to leverage the provided extensions, which offer ready-to-use solutions for common environments (such as tokio or WASM).
Key Features
-
Efficient Multiplexing: Multiple concurrent data streams over a single connection, correctly reassembling interleaved frames.
-
Streaming RPC: Half-duplex request streams with full payload chunking — send large payloads incrementally without blocking other streams.
-
Bidirectional Streaming: True concurrent streams in both directions using independent unidirectional streams — each direction is separately cancellable and independently backpressured.
-
Prebuffered (Unary) RPC: Standard request/response RPC calls where the entire request is buffered before handler invocation.
-
Compile-Time Method IDs: Deterministic
u64identifiers viarpc_method_id!("name")macro using xxHash3 at compile time — no runtime cost, no magic numbers, platform-independent. -
Minimalist RPC Layer: Lightweight RPC on top of framing, giving you freedom to choose your own serialization formats, dispatching logic, and error-handling strategies.
-
Low-Overhead Binary Protocol: Compact binary framing protocol with 17 bytes of header overhead per frame (stream ID, sequence ID, frame kind, timestamp).
-
Transport and Runtime Agnostic: Core logic uses a flexible, callback-driven design, enabling seamless adaptation across Tokio, WASM, and standard library environments.
-
Disconnect Detection: Three-layer mechanism — transport heartbeats (5s ping interval, 15s timeout),
fail_all_pending_requests()on disconnect, and frame-level Cancel/End processing. -
Extensible by Design: Muxio comes with pre-built extensions:
- Tokio-based WebSocket Server/Client: For native, multi-threaded environments.
- WASM-based Web Client: For seamless integration into web applications via a JavaScript byte-passing bridge.
- Tokio-based IPC Server/Client: For local inter-process communication over Unix domain sockets or Windows named pipes.
How Muxio Compares
Minimal framing overhead: 17 bytes per frame (stream ID, sequence ID, frame kind, timestamp) vs HTTP/2's 9 bytes or gRPC's ~50+ bytes per protobuf message. For high-frequency small messages — keystrokes, mouse events, terminal output chunks — this overhead difference is significant.
Transport-agnostic core: The RpcServiceCallerInterface trait abstracts away the transport so the same application code works over WebSocket, Unix domain sockets, or WASM bridges without modification. Not many (if any) other Rust RPC frameworks offer WASM as a first-class transport.
FFI-friendly byte model: The core dispatcher receives and emits raw byte slices, making it straightforward to bridge to C, C++, Python, or JavaScript. The included WASM client demonstrates this pattern with #[wasm_bindgen].
No protobuf dependency: You choose your serialization — bitcode, bincode, manual encoding, or anything else. The framework handles framing and dispatch; it doesn't mandate a schema format.
Tradeoffs:
- No built-in backpressure or flow control. The write channel between encoder and transport I/O is unbounded by design — switching to a bounded channel without per-stream flow control (like HTTP/2
WINDOW_UPDATE) would cause head-of-line blocking. Under sustained producer > consumer load, memory can grow. Real applications should either size their chunks conservatively or implement application-level backpressure.
Note: The proper fix is likely per-stream byte budgets at the encoder. When a stream exceeds its budget,
write_bytespauses that stream's frames without blocking others going through the same channel. The framing layer already hasstream_idon every frame; what's missing is the budget tracking. Not implemented yet.
- No service discovery, load balancing, TLS, or auth. These are left entirely to the user. gRPC and Tonic ship them out of the box.
- Smaller ecosystem. Muxio has one primary author. Tonic/gRPC have broad adoption, protobuf tooling, interceptors, and reflection.
Other Notes:
Muxio is designed to be compiled into Rust first. Interop with other languages happens through FFI (PyO3 for Python, #[wasm_bindgen] for JavaScript, C ABI for other languages) — you embed the Rust core rather than reimplementing the protocol from a spec. This is the same model as libnghttp2 or libssl: the library is consumed as a compiled dependency, not implemented independently.
How Muxio Compares
Minimal framing overhead: 17 bytes per frame (stream ID, sequence ID, frame kind, timestamp) vs HTTP/2's 9 bytes or gRPC's ~50+ bytes per protobuf message. For high-frequency small messages — keystrokes, mouse events, terminal output chunks — this overhead difference is significant.
Transport-agnostic core: The RpcServiceCallerInterface trait abstracts away the transport so the same application code works over WebSocket, Unix domain sockets, or WASM bridges without modification. Not many (if any) other Rust RPC frameworks offer WASM as a first-class transport.
FFI-friendly byte model: The core dispatcher receives and emits raw byte slices, making it straightforward to bridge to C, C++, Python, or JavaScript. The included WASM client demonstrates this pattern with #[wasm_bindgen].
No protobuf dependency: You choose your serialization — bitcode, bincode, manual encoding, or anything else. The framework handles framing and dispatch; it doesn't mandate a schema format.
Core Use Cases & Design Philosophy
Muxio is engineered to solve specific challenges in building modern, distributed systems. Its architecture and features are guided by the following principles:
-
Low-Latency, High-Performance Communication: Muxio is built for speed. It uses a compact, low-overhead binary protocol (instead of text-based formats like JSON). This significantly reduces the size of data sent over the network and minimizes the CPU cycles needed for serialization and deserialization. By avoiding complex parsing, Muxio lowers end-to-end latency, making it well-suited for real-time applications such as financial data streaming, multiplayer games, and interactive remote tooling.
-
Cross-Platform Code with Agnostic Frontends: Write your core application logic once and deploy it across multiple platforms. Muxio achieves this through its generic
RpcServiceCallerInterfacetrait, which abstracts away the underlying transport. The same application code that calls an RPC method using the nativeRpcClientcan also be utilized in a browser with theRpcWasmClientwith minimal changes, while additional client types can also be added, provided they implement the same aformentionedRpcServiceCallerInterface. This design ensures that improvements to the core service logic benefit all clients simultaneously, even custom-built clients. -
Shared Service Definitions for Type-Safe APIs: Enforce integrity between your server and client by defining RPC methods, inputs, and outputs in a shared crate. By implementing the
RpcMethodPrebufferedtrait , both client and server depend on a single source of truth for the API contract. This completely eliminates a common class of runtime errors, as any mismatch in data structures between the client and server will result in a compile-time error. -
A Strong Foundation for Foreign Function Interfaces (FFI): The framework's byte-oriented design makes it an ideal foundation for bridging Rust with other languages. Because the core dispatcher only needs to receive and emit byte slices, you can easily create an FFI layer that connects Muxio to C, C++, Swift, or any language that can handle byte array (including Python). The included
muxio-wasm-rpc-clientserves as a perfect example, usingwasm_bindgento create a simple bridge between the Rust client and the JavaScript host environment.
Installation
For Muxio's core:
This provides the low-level functionality, but Muxio extensions are likely more desirable for most use cases.
WebSocket Usage Example
Let's build a simple sample app which spins up a Tokio-based WebSocket server, adds some routes, then spins up a client, performs some requests, then shuts everything down.
This example code was taken from the example-muxio-ws-rpc-app crate.
use ;
use ;
use ;
use Arc;
use join;
use TcpListener;
async
WASM WebSocket RPC
The WASM client follows a callback-driven pattern — the browser owns the WebSocket, and Rust is called on events. The static_lib module provides #[wasm_bindgen] exports that JS calls on onopen, onmessage, and onclose. The core RpcWasmClient implements the same RpcServiceCallerInterface used above, so calling methods like Add::call(...) works identically in the browser.
IPC Usage Example
The same application code works over Unix domain sockets or Windows named pipes via the IPC transport. Only the client and server types change — the service definitions (Add, Mult, Echo) are identical to the WebSocket example above.
This example code was taken from the example-muxio-ws-rpc-app crate.
use ;
use ;
use ;
use join;
async
Streaming RPC Example
Muxio supports streaming requests over any transport. Each stream is half-duplex: the sender writes chunks and ends the stream, then reads the single response. True bidirectional messaging is achieved with two independent concurrent streams (one per direction).
Streaming a request from the client
use StreamExt;
use RpcRequest;
use rpc_method_id;
use DynamicChannelType;
use ;
// Method IDs are generated at compile time via xxHash3 from string names.
// Each application defines its own — use distinct names for distinct methods.
const STREAM_INPUT_METHOD_ID: u64 = rpc_method_id!;
async
Handling streaming requests on the server
Streaming handlers are registered via register_stream_handler() and receive individual RpcStreamEvents as they arrive from the transport. Unlike prebuffered handlers (which accumulate the entire request into a Vec<u8> before invoking the handler), streaming handlers are called synchronously for each event:
use RpcStreamEvent;
use rpc_method_id;
use ;
const STREAM_INPUT_METHOD_ID: u64 = rpc_method_id!;
Note: The second argument (
_emit) accepts a raw transport byte sink (Box<dyn RpcEmit>). A future API will exposeRpcDispatcher::respond()for sending properly framed response chunks back to the caller.
Client vs Server Streaming: Deliberate Asymmetry
The streaming API is intentionally asymmetric between the two roles:
-
Client side (
call_rpc_streaming): The client initiates a stream, writes chunks via anRpcStreamEncoder, and reads the response via aDynamicReceiver(which implementsStream). This is the producer/consumer pattern — the caller produces request data and consumes the response. -
Server side (
register_stream_handler): The server registers a handler that is invoked synchronously for eachRpcStreamEventas it arrives. The handler receives aStreamResponderfor sending response chunks back. This is the event-driven pattern — the server reacts to stream events rather than driving the stream.
The asymmetry is inherent to the request-reply model: one side initiates (client), the other handles (server). The same RpcServiceCallerInterface trait powers client-style calls from any context, including server-side code that wants to push data to connected clients (see "server-initiated calls" below).
Disconnect Detection
Streaming RPC calls detect remote disconnection through three layers:
-
Transport heartbeats: The transport sends periodic pings (default 5s interval on the WebSocket server) and closes the connection if no response arrives within the timeout (15s).
-
fail_all_pending_requests(): When any transport detects a disconnect, it calls this method on the dispatcher, which fails all pending response handlers. Anyreceiver.next().awaitin application code will returnNoneorErr(...). -
Frame-level signaling: Individual
CancelandEndframes are processed by the stream decoder. Corrupt or invalid frames produceRpcStreamEvent::Error, which propagates as a transport error to the caller.
Applications should handle stream termination by checking the Result from receiver.next().awaitand treatingNone(stream ended) orErr(...)` as signals that the remote peer is gone.
Streaming from the server to the client (server-initiated calls)
Any handle that implements RpcServiceCallerInterface — such as the
ConnectionContextHandle obtained from a ClientConnected event —
can initiate streaming calls:
use RpcRequest;
use rpc_method_id;
use DynamicChannelType;
use RpcServiceCallerInterface;
use RpcServerEvent;
// Applications define their own method IDs — this is just an example.
const SERVER_STREAM_METHOD_ID: u64 = rpc_method_id!;
async
Concurrent bidirectional streaming
Streams are multiplexed over a single connection. Each stream is unidirectional, so bidirectional communication uses two independent streams without a separate connection per direction.
A single bidirectional stream would interleave both directions in one channel, coupling their backpressure, error handling, and lifecycle; two (or more) unidirectional streams keep each direction isolated and individually cancellable.
Single connection, multiple streams.
Client Server
│ │
├─ call_rpc_streaming ────────► │ stream A open
│ ◄──── call_rpc_streaming ─────│ stream B open
│ ── chunk A ─────────────────► │
│ ◄──── chunk B ─────────────── │ interleaved
│ ── chunk A ─────────────────► │ writes
│ ◄──── chunk B ─────────────── │
│ ... │
│ ── End A ───────────────────► │
│ ◄──── End B ───────────────── │
│ ◄──── response A (echo) ───── │
│ ── response B (echo) ───────► │
This is exactly what the concurrent_bidirectional_streaming integration test exercises. It spawns two tokio::spawn tasks that write chunks in opposite directions and yield between each chunk so the writes are truly interleaved at the application level, not buffered and sent in one burst per direction.
License
Licensed under the Apache-2.0 License.