This documentation is part of the "Projects with Books" initiative at zenOSmosis.
The source code for this project is available on GitHub.
Design Philosophy
Relevant source files
Purpose and Scope
This document details the fundamental design principles that guide the rust-muxio architecture. It explains the non-async runtime model, binary protocol design choices, transport abstraction strategy, and core goals that shape the system. For information about how these principles manifest in the layered architecture, see Layered Architecture. For practical implementation details of the binary protocol, see Binary Framing Protocol.
Non-Async Runtime Model
The core muxio library is implemented using a callback-driven, synchronous control flow rather than async/await. This design choice enables maximum portability and minimal runtime dependencies while still supporting concurrent operations, streaming, and cancellation.
Callback-Driven Architecture
The fundamental mechanism is the on_message_bytes callback pattern. The core dispatcher accepts a closure that will be invoked whenever bytes need to be sent:
Sources: DRAFT.md:48-52 README.md34
graph LR
subgraph "Application Code"
AppLogic["Application Logic"]
end
subgraph "Core Dispatcher"
RpcDispatcher["RpcDispatcher"]
CallbackRegistry["on_message_bytes callback"]
end
subgraph "Transport Layer (Async or Sync)"
AsyncTransport["Tokio WebSocket"]
SyncTransport["Standard Library TCP"]
WasmTransport["WASM JS Bridge"]
end
AppLogic --> RpcDispatcher
RpcDispatcher --> CallbackRegistry
CallbackRegistry --> AsyncTransport
CallbackRegistry --> SyncTransport
CallbackRegistry --> WasmTransport
AsyncTransport --> RpcDispatcher
SyncTransport --> RpcDispatcher
WasmTransport --> RpcDispatcher
Runtime Independence Benefits
This model provides several critical advantages:
| Benefit | Description |
|---|---|
| WASM Compatibility | Works in single-threaded JavaScript environments where async tasks are limited |
| Runtime Flexibility | Same core code runs on Tokio, async-std, or synchronous runtimes |
| Deterministic Execution | No hidden async state machines or yield points |
| FFI-Friendly | Callbacks can cross language boundaries more easily than async functions |
| Zero Runtime Overhead | No async runtime machinery in the core library |
The following diagram maps this philosophy to actual code entities:
Sources: muxio/src/rpc_dispatcher.rs DRAFT.md:48-52 README.md34
Binary Protocol Foundation
Muxio uses a low-overhead binary framing protocol for all communication. This is a deliberate architectural choice prioritizing performance over human readability.
graph LR
subgraph "Text-Based Approach (JSON/XML)"
TextData["Human-readable strings"]
TextParsing["Complex parsing\nTokenization\nString allocation"]
TextSize["Larger payload size\nQuotes, brackets, keys"]
TextCPU["High CPU cost\nUTF-8 validation\nEscape sequences"]
end
subgraph "Binary Approach (Muxio)"
BinaryData["Raw byte arrays"]
BinaryParsing["Simple framing\nFixed header offsets\nZero-copy reads"]
BinarySize["Minimal payload\nCompact encoding\nNo metadata"]
BinaryCPU["Low CPU cost\nDirect memory access\nNo parsing"]
end
TextData -->
TextParsing -->
TextSize --> TextCPU
BinaryData -->
BinaryParsing -->
BinarySize --> BinaryCPU
Why Binary Over Text
Performance Impact:
| Metric | Text-Based | Binary (Muxio) | Improvement |
|---|---|---|---|
| Serialization overhead | High (string formatting) | Minimal (bitcode) | ~10-100x faster |
| Payload size | Verbose | Compact | ~2-5x smaller |
| Parse complexity | O(n) with allocations | O(1) header reads | Constant time |
| CPU cache efficiency | Poor (scattered strings) | Good (contiguous bytes) | Better locality |
Sources: README.md32 README.md45 DRAFT.md11
Binary Protocol Stack
The following diagram shows how binary data flows through the protocol layers:
Sources: README.md:32-33 muxio/src/rpc_request_response.rs Cargo.toml (bitcode dependency)
Transport and Runtime Agnosticism
A core principle is that the muxio core makes zero assumptions about the transport or runtime. This is enforced through careful API design.
graph TB
subgraph "Muxio Core (Transport-Agnostic)"
CoreDispatcher["RpcDispatcher\nGeneric over callback\nNo transport dependencies"]
CoreTypes["RpcRequest\nRpcResponse\nRpcHeader"]
end
subgraph "Transport Abstraction Layer"
CallerInterface["RpcServiceCallerInterface\n(muxio-rpc-service-caller)\nAbstract trait"]
EndpointInterface["RpcServiceEndpointInterface\n(muxio-rpc-service-endpoint)\nAbstract trait"]
end
subgraph "Concrete Implementations"
TokioImpl["MuxioRpcServer\nmuxio-tokio-rpc-server\nUses: tokio-tungstenite"]
TokioClientImpl["RpcClient\nmuxio-tokio-rpc-client\nUses: tokio-tungstenite"]
WasmImpl["RpcWasmClient\nmuxio-wasm-rpc-client\nUses: wasm-bindgen"]
CustomImpl["Custom implementations\n(IPC, gRPC, etc.)"]
end
CoreDispatcher --> CoreTypes
CoreTypes --> CallerInterface
CoreTypes --> EndpointInterface
CallerInterface --> TokioClientImpl
CallerInterface --> WasmImpl
CallerInterface --> CustomImpl
EndpointInterface --> TokioImpl
Transport Abstraction Strategy
Key Abstraction Points:
The RpcServiceCallerInterface trait provides transport abstraction for clients:
- Method:
call_prebuffered()- Send request, receive response - Implementation: Each transport provides its own
RpcServiceCallerInterfaceimpl - Portability: Application code depends only on the trait, not concrete implementations
Sources: README.md47 extensions/muxio-rpc-service-caller/src/caller_interface.rs README.md34
Runtime Environment Support Matrix
| Runtime Environment | Server Support | Client Support | Implementation Crate |
|---|---|---|---|
| Tokio (async) | ✓ | ✓ | muxio-tokio-rpc-server, muxio-tokio-rpc-client |
| async-std | ✗ (possible) | ✗ (possible) | Not implemented |
| Standard Library (sync) | ✗ (possible) | ✗ (possible) | Not implemented |
| WASM/Browser | N/A | ✓ | muxio-wasm-rpc-client |
| Node.js/Deno | ✗ (possible) | ✗ (possible) | Not implemented |
The core's agnosticism means new runtime support requires only implementing the appropriate wrapper crates, not modifying core logic.
Sources: README.md:34-40 extensions/README.md
graph TB
subgraph "Layer 4: Application"
AppCode["Application Logic\nBusiness rules"]
end
subgraph "Layer 3: RPC Abstraction"
ServiceDef["RpcMethodPrebuffered\n(muxio-rpc-service)\nDefines API contract"]
Caller["RpcServiceCallerInterface\n(muxio-rpc-service-caller)\nClient-side calls"]
Endpoint["RpcServiceEndpointInterface\n(muxio-rpc-service-endpoint)\nServer-side dispatch"]
end
subgraph "Layer 2: Multiplexing"
Dispatcher["RpcDispatcher\n(muxio/rpc_dispatcher.rs)\nRequest correlation\nFrame multiplexing"]
end
subgraph "Layer 1: Binary Framing"
Framing["Binary Protocol\n(muxio/framing.rs)\nChunk/reassemble frames"]
end
subgraph "Layer 0: Transport"
Transport["WebSocket/TCP/IPC\nExternal implementations"]
end
AppCode --> ServiceDef
ServiceDef --> Caller
ServiceDef --> Endpoint
Caller --> Dispatcher
Endpoint --> Dispatcher
Dispatcher --> Framing
Framing --> Transport
Transport -.bytes up.-> Framing
Framing -.frames up.-> Dispatcher
Dispatcher -.responses up.-> Caller
Dispatcher -.requests up.-> Endpoint
Layered Separation of Concerns
Muxio enforces strict separation between system layers, with each layer unaware of layers above it:
Layer Independence Guarantees:
| Layer | Knowledge | Ignorance |
|---|---|---|
| Binary Framing | Bytes, frame headers | No knowledge of RPC, methods, or requests |
| Multiplexing | Request IDs, correlation | No knowledge of method semantics or serialization |
| RPC Abstraction | Method IDs, request/response pattern | No knowledge of specific transports |
| Application | Business logic | No knowledge of framing or multiplexing |
This enables:
- Testing: Each layer can be unit tested independently
- Extensibility: New transports don't affect RPC logic
- Reusability: Same multiplexing layer works for non-RPC protocols
- Maintainability: Changes isolated to single layers
Sources: README.md:16-17 README.md22 DRAFT.md:9-26
graph TB
subgraph "Shared Service Definition Crate"
ServiceTrait["RpcMethodPrebuffered\n(muxio-rpc-service)"]
AddMethod["impl RpcMethodPrebuffered for Add\nMETHOD_ID = xxhash('Add')\nRequest = Vec<f64>\nResponse = f64"]
MultMethod["impl RpcMethodPrebuffered for Mult\nMETHOD_ID = xxhash('Mult')\nRequest = Vec<f64>\nResponse = f64"]
end
subgraph "Server Code"
ServerHandler["endpoint.register_prebuffered(\n Add::METHOD_ID,\n /bytes, ctx/ async move {\n let params = Add::decode_request(&bytes)?;\n let sum = params.iter().sum();\n Add::encode_response(sum)\n }\n)"]
end
subgraph "Client Code"
ClientCall["Add::call(\n &rpc_client,\n vec![1.0, 2.0, 3.0]\n).await"]
end
subgraph "Compile-Time Guarantees"
TypeCheck["Type mismatch → Compiler error"]
MethodIDCheck["Duplicate METHOD_ID → Compiler error"]
SerdeCheck["Serde incompatibility → Compiler error"]
end
ServiceTrait --> AddMethod
ServiceTrait --> MultMethod
AddMethod -.defines.-> ServerHandler
AddMethod -.defines.-> ClientCall
AddMethod --> TypeCheck
AddMethod --> MethodIDCheck
AddMethod --> SerdeCheck
Type Safety Through Shared Definitions
The system enforces compile-time API contracts between client and server via shared service definitions.
Compile-Time Contract Enforcement
What Gets Checked at Compile Time:
- Type Consistency: Client and server must agree on
RequestandResponsetypes - Method ID Uniqueness: Hash collisions in method names are detected
- Serialization Compatibility: Both sides use identical
encode/decodeimplementations - API Changes: Modifying service definition breaks both client and server simultaneously
Example Service Definition Structure:
The trait RpcMethodPrebuffered requires:
METHOD_ID: u64- Compile-time constant generated from method name hashencode_request()/decode_request()- Serialization for parametersencode_response()/decode_response()- Serialization for results
Sources: README.md49 extensions/muxio-rpc-service/src/prebuffered.rs README.md:69-117 (example code)
Core Design Goals
The following table summarizes the fundamental design goals that drive all architectural decisions:
| Goal | Rationale | Implementation Approach |
|---|---|---|
| Binary Protocol | Minimize overhead, maximize performance | Raw byte arrays, bitcode serialization |
| Framed Transport | Discrete, ordered chunks enable multiplexing | Fixed-size frame headers, variable payloads |
| Bidirectional | Client/server symmetry | Same RpcDispatcher logic for both directions |
| WASM-Compatible | Deploy to browsers | Non-async core, callback-driven model |
| Streamable | Support large payloads | Chunked transmission via framing protocol |
| Cancelable | Abort in-flight requests | Request ID tracking in RpcDispatcher |
| Metrics-Capable | Observability for production | Hooks for latency, throughput measurement |
| Transport-Agnostic | Flexible deployment | Callback-based abstraction, no hard transport deps |
| Runtime-Agnostic | Work with any async runtime | Non-async core, async wrappers |
| Type-Safe | Eliminate runtime API mismatches | Shared service definitions, compile-time checks |
Sources: DRAFT.md:9-26 README.md:41-52
sequenceDiagram
participant App as "Application\n(Type-safe)"
participant Service as "Add::call()\n(Shared definition)"
participant Client as "RpcClient\n(Transport wrapper)"
participant Dispatcher as "RpcDispatcher\n(Core, non-async)"
participant Callback as "on_message_bytes\n(Callback)"
participant Transport as "WebSocket\n(Async transport)"
App->>Service: Add::call(&client, vec![1.0, 2.0])
Note over Service: Compile-time type check
Service->>Service: encode_request(vec![1.0, 2.0])\n→ bytes
Service->>Client: call_prebuffered(METHOD_ID, bytes)
Client->>Dispatcher: send_request(METHOD_ID, bytes)
Note over Dispatcher: Assign request ID\nStore pending request
Dispatcher->>Dispatcher: Serialize to binary frames
Dispatcher->>Callback: callback(frame_bytes)
Note over Callback: Synchronous invocation
Callback->>Transport: send_websocket_frame(frame_bytes)
Note over Transport: Async I/O (external)
Design Philosophy in Practice
The following sequence shows how these principles work together in a single RPC call:
This demonstrates:
- Type safety:
Add::call()enforces parameter types - Shared definitions: Same
encode_request()on both sides - Transport agnostic:
RpcDispatcherknows nothing about WebSocket - Non-async core:
RpcDispatcheris synchronous, invokes callback - Binary protocol: Everything becomes bytes before transmission
Sources: README.md:69-161 muxio/src/rpc_dispatcher.rs extensions/muxio-rpc-service-caller/src/caller_interface.rs
Dismiss
Refresh this wiki
Enter email to refresh