This documentation is part of the "Projects with Books" initiative at zenOSmosis.
The source code for this project is available on GitHub.
Service Definitions
Loading…
Service Definitions
Relevant source files
Purpose and Scope
Service definitions provide compile-time type-safe RPC method contracts that are shared between client and server implementations. The muxio-rpc-service crate defines the core traits and utilities for declaring RPC methods with automatic method ID generation and efficient binary serialization. Service definitions serve as the single source of truth for RPC interfaces, ensuring that both sides of the communication agree on method signatures, parameter types, and return types at compile time.
For information about implementing client-side invocation logic, see Service Caller Interface. For server-side handler registration, see Service Endpoint Interface. For a step-by-step guide on creating your own service definitions, see Creating Service Definitions.
Core Architecture
The service definition layer sits at the top of the RPC abstraction layer, providing the foundation for type-safe communication. It bridges application-level Rust types with the underlying binary protocol.
Sources:
graph TB
subgraph "Application Layer"
APP["Application Code\nBusiness Logic"]
end
subgraph "Service Definition Layer"
TRAIT["RpcMethodPrebuffered Trait\nMethod Signature Declaration"]
METHODID["METHOD_ID Constant\nxxhash::xxh3_64(method_name)"]
PARAMS["Parameter Types\nSerialize + Deserialize"]
RESULT["Result Types\nSerialize + Deserialize"]
end
subgraph "RPC Framework Layer"
CALLER["RpcServiceCallerInterface\nClient Invocation"]
ENDPOINT["RpcServiceEndpointInterface\nServer Dispatch"]
SERIALIZER["bitcode::encode/decode\nBinary Serialization"]
end
APP --> TRAIT
TRAIT --> METHODID
TRAIT --> PARAMS
TRAIT --> RESULT
CALLER --> METHODID
ENDPOINT --> METHODID
PARAMS --> SERIALIZER
RESULT --> SERIALIZER
The muxio-rpc-service Crate
The muxio-rpc-service crate provides the foundational types and traits for defining RPC services. It has minimal dependencies to remain runtime-agnostic and platform-independent.
Dependencies
| Dependency | Purpose |
|---|---|
async-trait | Enables async trait methods for service definitions |
futures | Provides stream abstractions for streaming RPC methods |
muxio | Core framing and multiplexing primitives |
num_enum | Discriminated union encoding for message types |
xxhash-rust | Fast compile-time hash generation for method IDs |
bitcode | Compact binary serialization for parameters and results |
Sources:
The RpcMethodPrebuffered Trait
The RpcMethodPrebuffered trait is the primary mechanism for defining RPC methods. It specifies the method signature, parameter types, result types, and automatically generates a unique method identifier.
graph LR
subgraph "RpcMethodPrebuffered Trait Definition"
NAME["const NAME: &'static str\nHuman-readable method name"]
METHODID["const METHOD_ID: u64\nxxh3_64(NAME)
at compile time"]
PARAMS["type Params\nSerialize + Deserialize + Send"]
RESULT["type Result\nSerialize + Deserialize + Send"]
end
subgraph "Example: AddMethod"
NAME_EX["NAME = 'Add'"]
METHODID_EX["METHOD_ID = 0x5f8b3c4a2e1d6f90"]
PARAMS_EX["Params = (i32, i32)"]
RESULT_EX["Result = i32"]
end
NAME --> NAME_EX
METHODID --> METHODID_EX
PARAMS --> PARAMS_EX
RESULT --> RESULT_EX
Key Components
| Component | Type | Description |
|---|---|---|
NAME | const &'static str | Human-readable method name (e.g., “Add”, “Multiply”) |
METHOD_ID | const u64 | Compile-time hash of NAME using xxhash |
Params | Associated Type | Input parameter type, must implement Serialize + Deserialize + Send |
Result | Associated Type | Return value type, must implement Serialize + Deserialize + Send |
Type Safety Guarantees
Service definitions enforce several type safety invariants:
- Compile-time method identification : Method IDs are computed at compile time from method names
- Consistent serialization : Both client and server use the same bitcode schema for parameters
- Type mismatch detection : Mismatched parameter or result types cause compilation errors
- Zero-cost abstraction : Method dispatch has no runtime overhead beyond the hash lookup
Sources:
Method ID Generation with xxhash
Method IDs are 64-bit unsigned integers generated at compile time by hashing the method name. This approach provides efficient dispatch while maintaining human-readable method names in code.
graph LR
subgraph "Compile Time"
METHNAME["Method Name (String)\ne.g., 'Add', 'Multiply', 'Echo'"]
HASH["xxhash::xxh3_64(bytes)"]
METHODID["METHOD_ID: u64\ne.g., 0x5f8b3c4a2e1d6f90"]
METHNAME --> HASH
HASH --> METHODID
end
subgraph "Runtime - Client"
CLIENT_CALL["Client calls method"]
CLIENT_ENCODE["RpcRequest::method_id = METHOD_ID"]
CLIENT_SEND["Send binary request"]
CLIENT_CALL --> CLIENT_ENCODE
CLIENT_ENCODE --> CLIENT_SEND
end
subgraph "Runtime - Server"
SERVER_RECV["Receive binary request"]
SERVER_DECODE["Extract method_id from RpcRequest"]
SERVER_MATCH["Match method_id to handler"]
SERVER_EXEC["Execute handler"]
SERVER_RECV --> SERVER_DECODE
SERVER_DECODE --> SERVER_MATCH
SERVER_MATCH --> SERVER_EXEC
end
METHODID --> CLIENT_ENCODE
METHODID --> SERVER_MATCH
Method ID Properties
| Property | Description |
|---|---|
| Size | 64-bit unsigned integer |
| Generation | Compile-time hash using xxh3_64 algorithm |
| Collision Resistance | Extremely low probability of collision for reasonable method counts |
| Performance | Single integer comparison for method dispatch |
| Stability | Same method name always produces same ID across compilations |
Benefits of Compile-Time Method IDs
- No string comparison overhead : Method dispatch uses integer comparison instead of string matching
- Compact wire format : Only 8 bytes sent over the network instead of method name strings
- Automatic generation : No manual assignment of method IDs required
- Type-safe verification : Compile-time guarantee that client and server agree on method IDs
Sources:
graph TB
subgraph "Parameter Encoding"
RUSTPARAM["Rust Type\ne.g., (i32, String, Vec<u8>)"]
BITCODEENC["bitcode::encode(params)"]
BINARY["Compact Binary Payload\nVariable-length encoding"]
RUSTPARAM --> BITCODEENC
BITCODEENC --> BINARY
end
subgraph "RPC Request Structure"
RPCREQ["RpcRequest"]
REQMETHOD["method_id: u64"]
REQPARAMS["params: Vec<u8>"]
RPCREQ --> REQMETHOD
RPCREQ --> REQPARAMS
end
subgraph "Result Decoding"
RESPBINARY["Binary Payload"]
BITCODEDEC["bitcode::decode::<T>(bytes)"]
RUSTRESULT["Rust Type\ne.g., Result<String, Error>"]
RESPBINARY --> BITCODEDEC
BITCODEDEC --> RUSTRESULT
end
BINARY --> REQPARAMS
REQPARAMS -.Wire Protocol.-> RESPBINARY
Serialization with Bitcode
All RPC parameters and results are serialized using the bitcode crate, which provides compact binary encoding with built-in support for Rust types.
Bitcode Characteristics
| Characteristic | Description |
|---|---|
| Encoding | Compact binary format with variable-length integers |
| Schema | Schemaless - structure implied by Rust types |
| Performance | Zero-copy deserialization where possible |
| Type Support | Built-in support for standard Rust types (primitives, tuples, Vec, HashMap, etc.) |
| Versioning | Field order and type changes require coordinated updates |
Serialization Requirements
For a type to be used as Params or Result in an RPC method definition, it must implement:
Serialize + Deserialize + Send
These bounds ensure:
- The type can be encoded to binary (
Serialize) - The type can be decoded from binary (
Deserialize) - The type can be safely sent across thread boundaries (
Send)
Sources:
graph TB
subgraph "Service Definition Crate"
CRATE["example-muxio-rpc-service-definition"]
subgraph "Method Definitions"
ADD["AddMethod\nNAME: 'Add'\nParams: (i32, i32)\nResult: i32"]
MULT["MultiplyMethod\nNAME: 'Multiply'\nParams: (i32, i32)\nResult: i32"]
ECHO["EchoMethod\nNAME: 'Echo'\nParams: String\nResult: String"]
end
CRATE --> ADD
CRATE --> MULT
CRATE --> ECHO
end
subgraph "Consumer Crates"
CLIENT["Client Application\nUses methods via RpcServiceCallerInterface"]
SERVER["Server Application\nImplements handlers via RpcServiceEndpointInterface"]
end
ADD --> CLIENT
MULT --> CLIENT
ECHO --> CLIENT
ADD --> SERVER
MULT --> SERVER
ECHO --> SERVER
Service Definition Structure
A complete service definition typically consists of multiple method trait implementations grouped together. Here’s the conceptual structure:
Typical Crate Layout
example-muxio-rpc-service-definition/
├── Cargo.toml
│ ├── [dependency] muxio-rpc-service
│ └── [dependency] bitcode
└── src/
└── lib.rs
├── struct AddMethod;
├── impl RpcMethodPrebuffered for AddMethod { ... }
├── struct MultiplyMethod;
├── impl RpcMethodPrebuffered for MultiplyMethod { ... }
└── ...
Sources:
graph TB
subgraph "Service Definition"
SERVICEDEF["RpcMethodPrebuffered Implementation\n- NAME\n- METHOD_ID\n- Params\n- Result"]
end
subgraph "Client Side"
CALLERIFACE["RpcServiceCallerInterface"]
CALLER_INVOKE["call_method<<M: RpcMethodPrebuffered>>()"]
DISPATCHER["RpcDispatcher"]
CALLERIFACE --> CALLER_INVOKE
CALLER_INVOKE --> DISPATCHER
end
subgraph "Server Side"
ENDPOINTIFACE["RpcServiceEndpointInterface"]
ENDPOINT_REGISTER["register<<M: RpcMethodPrebuffered>>()"]
HANDLER_MAP["HashMap<u64, Handler>"]
ENDPOINTIFACE --> ENDPOINT_REGISTER
ENDPOINT_REGISTER --> HANDLER_MAP
end
subgraph "Wire Protocol"
RPCREQUEST["RpcRequest\nmethod_id: u64\nparams: Vec<u8>"]
RPCRESPONSE["RpcResponse\nrequest_id: u64\nresult: Vec<u8>"]
end
SERVICEDEF --> CALLER_INVOKE
SERVICEDEF --> ENDPOINT_REGISTER
DISPATCHER --> RPCREQUEST
RPCREQUEST --> HANDLER_MAP
HANDLER_MAP --> RPCRESPONSE
RPCRESPONSE --> DISPATCHER
Integration with the RPC Framework
Service definitions integrate with the broader RPC framework through well-defined interfaces:
Compile-Time Guarantees
The service definition system provides several compile-time guarantees:
| Guarantee | Mechanism |
|---|---|
| Type Safety | Generic trait bounds enforce matching types across client/server |
| Method ID Uniqueness | Hashing function produces consistent IDs for method names |
| Serialization Compatibility | Shared trait implementations ensure same encoding/decoding |
| Parameter Validation | Rust type system validates parameter structure at compile time |
Runtime Flow
- Client : Invokes method through
RpcServiceCallerInterface::call::<MethodType>(params) - Serialization : Parameters are encoded using
bitcode::encode(params) - Request Construction :
RpcRequestcreated withMETHOD_IDand serialized params - Server Dispatch : Request routed to handler based on
method_idlookup - Handler Execution : Handler deserializes params, executes logic, serializes result
- Response Delivery :
RpcResponsesent back with serialized result - Client Deserialization : Result decoded using
bitcode::decode::<ResultType>(bytes)
Sources:
Cross-Platform Compatibility
Service definitions are completely platform-agnostic. The same service definition crate can be used by:
- Native Tokio-based clients and servers
- WASM browser-based clients
- Custom transport implementations
- Different runtime environments (async/sync)
This cross-platform capability is achieved because:
- Service definitions contain no platform-specific code
- Serialization is handled by platform-agnostic
bitcode - Method IDs are computed at compile time without runtime dependencies
- The trait system provides compile-time polymorphism
Sources:
Summary
Service definitions in muxio provide:
- Type-Safe Contracts : Compile-time verified method signatures shared between client and server
- Efficient Dispatch : 64-bit integer method IDs computed at compile time using xxhash
- Compact Serialization : Binary encoding using bitcode for minimal network overhead
- Platform Independence : Service definitions work across native, WASM, and custom platforms
- Zero Runtime Cost : All method resolution and type checking happens at compile time
The next sections cover how to use service definitions from the client side (Service Caller Interface) and server side (Service Endpoint Interface), as well as the specific patterns for prebuffered (Prebuffered RPC Calls) and streaming (Streaming RPC Calls) RPC methods.
Sources: