This documentation is part of the "Projects with Books" initiative at zenOSmosis.
The source code for this project is available on GitHub.
Defining a Simple RPC Service
Loading…
Defining a Simple RPC Service
Relevant source files
- README.md
- extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs
- extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs
Purpose and Scope
This page provides a step-by-step tutorial for defining RPC services in the muxio framework. It demonstrates how to create a shared service definition crate containing type-safe RPC method contracts that are used by both clients and servers. The tutorial uses the example services Add, Mult, and Echo as practical demonstrations.
For information about implementing server-side handlers that process these requests, see Service Endpoint Interface. For client-side invocation patterns, see Service Caller Interface. For the complete working example application, see WebSocket RPC Application Example.
Overview: Service Definitions as Shared Contracts
Service definitions in muxio are compile-time type-safe contracts shared between clients and servers. By implementing the RpcMethodPrebuffered trait in a common crate, both sides of the communication agree on:
- The method’s unique identifier (via compile-time xxhash)
- The input parameter types
- The output return types
- The serialization/deserialization logic
This approach eliminates an entire class of runtime errors by catching type mismatches at compile time.
Sources: README.md:48-50 README.md:70-74
Architecture: Service Definition Flow
The following diagram illustrates how a service definition crate sits between client and server implementations:
Diagram: Service Definition Shared Between Client and Server
graph TB
subgraph "Shared Service Definition Crate"
SD["example-muxio-rpc-service-definition"]
AddDef["Add Service\nRpcMethodPrebuffered"]
MultDef["Mult Service\nRpcMethodPrebuffered"]
EchoDef["Echo Service\nRpcMethodPrebuffered"]
SD --> AddDef
SD --> MultDef
SD --> EchoDef
end
subgraph "Server Implementation"
Server["RpcServer"]
Endpoint["RpcServiceEndpoint"]
AddHandler["Add::decode_request\nAdd::encode_response"]
MultHandler["Mult::decode_request\nMult::encode_response"]
EchoHandler["Echo::decode_request\nEcho::encode_response"]
Server --> Endpoint
Endpoint --> AddHandler
Endpoint --> MultHandler
Endpoint --> EchoHandler
end
subgraph "Client Implementation"
Client["RpcClient / RpcWasmClient"]
Caller["RpcServiceCallerInterface"]
AddCall["Add::call"]
MultCall["Mult::call"]
EchoCall["Echo::call"]
Client --> Caller
Caller --> AddCall
Caller --> MultCall
Caller --> EchoCall
end
AddDef -.provides.-> AddHandler
AddDef -.provides.-> AddCall
MultDef -.provides.-> MultHandler
MultDef -.provides.-> MultCall
EchoDef -.provides.-> EchoHandler
EchoDef -.provides.-> EchoCall
AddHandler -.uses METHOD_ID.-> AddDef
MultHandler -.uses METHOD_ID.-> MultDef
EchoHandler -.uses METHOD_ID.-> EchoDef
Sources: README.md:66-162
Step 1: Create the Shared Service Definition Crate
The service definitions live in a dedicated crate that is referenced by both client and server applications. In the muxio examples, this is example-muxio-rpc-service-definition.
Crate Dependencies
The service definition crate requires these dependencies:
| Dependency | Purpose |
|---|---|
muxio-rpc-service | Provides RpcMethodPrebuffered trait |
bitcode | Binary serialization (with derive feature) |
serde | Required by bitcode for derive macros |
Sources: Inferred from README.md:71-74 extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:1-6
Step 2: Define Input and Output Types
Each RPC method requires input and output types that implement Serialize and Deserialize from bitcode. For prebuffered methods, these types represent the complete request/response payloads.
Example Type Definitions
For the Add service that sums a vector of floating-point numbers:
- Input:
Vec<f64>(the numbers to sum) - Output:
f64(the result)
For the Echo service that returns data unchanged:
- Input:
Vec<u8>(arbitrary binary data) - Output:
Vec<u8>(same binary data)
For the Mult service that multiplies a vector of floating-point numbers:
- Input:
Vec<f64>(the numbers to multiply) - Output:
f64(the product)
Sources: README.md:102-118 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:21-27
Step 3: Implement the RpcMethodPrebuffered Trait
The RpcMethodPrebuffered trait is the core abstraction for defining prebuffered RPC methods. Each service implements this trait to specify its contract.
classDiagram
class RpcMethodPrebuffered {<<trait>>\n+Input: type\n+Output: type\n+METHOD_NAME: &'static str\n+METHOD_ID: u64\n+encode_request(input) Result~Vec~u8~~\n+decode_request(bytes) Result~Input~\n+encode_response(output) Result~Vec~u8~~\n+decode_response(bytes) Result~Output~}
class Add {+Input = Vec~f64~\n+Output = f64\n+METHOD_NAME = "Add"\n+METHOD_ID = xxhash64("Add")}
class Mult {+Input = Vec~f64~\n+Output = f64\n+METHOD_NAME = "Mult"\n+METHOD_ID = xxhash64("Mult")}
class Echo {+Input = Vec~u8~\n+Output = Vec~u8~\n+METHOD_NAME = "Echo"\n+METHOD_ID = xxhash64("Echo")}
RpcMethodPrebuffered <|.. Add
RpcMethodPrebuffered <|.. Mult
RpcMethodPrebuffered <|.. Echo
Trait Structure
Diagram: RpcMethodPrebuffered Trait Implementation Pattern
Required Associated Types and Constants
| Member | Description |
|---|---|
Input | The type of the method’s parameters |
Output | The type of the method’s return value |
METHOD_NAME | A unique string identifier (used for ID generation) |
METHOD_ID | A compile-time generated u64 via xxhash of METHOD_NAME |
Required Methods
| Method | Purpose |
|---|---|
encode_request(input: Self::Input) | Serialize input parameters to bytes using bitcode |
decode_request(bytes: &[u8]) | Deserialize bytes to input parameters using bitcode |
encode_response(output: Self::Output) | Serialize output value to bytes using bitcode |
decode_response(bytes: &[u8]) | Deserialize bytes to output value using bitcode |
Sources: Inferred from extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:10-21 README.md:102-118
sequenceDiagram
participant Dev as "Developer"
participant Trait as "RpcMethodPrebuffered Trait"
participant Compile as "Compile Time"
participant Runtime as "Runtime"
Note over Dev,Runtime: Service Definition Phase
Dev->>Trait: Define Add struct
Dev->>Trait: Set Input = Vec<f64>
Dev->>Trait: Set Output = f64
Dev->>Trait: Set METHOD_NAME = "Add"
Compile->>Compile: Generate METHOD_ID via xxhash("Add")
Dev->>Trait: Implement encode_request
Note right of Trait: Uses bitcode::encode
Dev->>Trait: Implement decode_request
Note right of Trait: Uses bitcode::decode
Dev->>Trait: Implement encode_response
Note right of Trait: Uses bitcode::encode
Dev->>Trait: Implement decode_response
Note right of Trait: Uses bitcode::decode
Note over Dev,Runtime: Usage Phase
Runtime->>Trait: Call Add::encode_request(vec![1.0, 2.0])
Trait->>Runtime: Returns Vec<u8> (serialized)
Runtime->>Trait: Call Add::decode_response(bytes)
Trait->>Runtime: Returns f64 (deserialized sum)
Step 4: Implementation Example
The following diagram shows the complete implementation flow for a single service:
Diagram: Service Implementation and Usage Flow
Sources: README.md:102-106 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:52-56
Step 5: Using Service Definitions on the Server
Once defined, services are registered on the server using their METHOD_ID constant. The server-side handler receives serialized bytes and uses the service’s decode_request and encode_response methods.
Server Registration Pattern
The server registration follows this pattern:
- Access the
RpcServiceEndpointfrom the server instance - Call
register_prebufferedwith the service’sMETHOD_ID - Provide an async handler that:
- Decodes the request using
ServiceType::decode_request - Performs the business logic
- Encodes the response using
ServiceType::encode_response
- Decodes the request using
Diagram: Server-Side Service Registration Flow
Example: Add Service Handler
From the example application README.md:102-106:
- Handler receives
request_bytes: Vec<u8> - Calls
Add::decode_request(&request_bytes)?to getVec<f64> - Computes sum:
request_params.iter().sum() - Calls
Add::encode_response(sum)?to get response bytes - Returns
Ok(response_bytes)
Sources: README.md:100-119 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:51-69
Step 6: Using Service Definitions on the Client
Clients invoke RPC methods using the RpcCallPrebuffered trait, which is automatically implemented for all types that implement RpcMethodPrebuffered.
Client Invocation Pattern
The RpcCallPrebuffered::call method provides a high-level interface:
Diagram: Client-Side RPC Call Flow
Example: Client Call
From the example application README.md:145-152:
- Call
Add::call(&*rpc_client, vec![1.0, 2.0, 3.0]) - Returns
Result<f64, RpcServiceError> - The
callmethod handles all encoding, transport, and decoding
Sources: README.md:144-159 extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:49-97
graph TD
EncodeArgs["Encode Input Arguments"]
CheckSize{"Size >= DEFAULT_SERVICE_MAX_CHUNK_SIZE?"}
SmallPath["Place in rpc_param_bytes\nSingle frame in header"]
LargePath["Place in rpc_prebuffered_payload_bytes\nStreamed after header"]
Transport["Send to RpcDispatcher"]
EncodeArgs --> CheckSize
CheckSize -->|No| SmallPath
CheckSize -->|Yes| LargePath
SmallPath --> Transport
LargePath --> Transport
Step 7: Smart Transport for Large Payloads
The RpcCallPrebuffered implementation includes automatic handling of large argument sets that exceed the default chunk size.
Transport Strategy Decision
Diagram: Automatic Large Payload Handling
| Condition | Strategy | Location |
|---|---|---|
encoded_args.len() < DEFAULT_SERVICE_MAX_CHUNK_SIZE | Send in header frame | rpc_param_bytes field |
encoded_args.len() >= DEFAULT_SERVICE_MAX_CHUNK_SIZE | Stream as payload | rpc_prebuffered_payload_bytes field |
This ensures that RPC calls work regardless of argument size without requiring application-level chunking logic.
Sources: extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:30-65 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:229-312
Complete Service Definition Structure
The following table summarizes the complete structure of a service definition:
| Component | Implementation | Example for Add |
|---|---|---|
| Service struct | Zero-sized type | pub struct Add; |
| Input type | Associated type | Vec<f64> |
| Output type | Associated type | f64 |
| Method name | Static string | "Add" |
| Method ID | Compile-time hash | xxhash64("Add") |
| Request encoder | Bitcode serialize | bitcode::encode(input) |
| Request decoder | Bitcode deserialize | bitcode::decode(bytes) |
| Response encoder | Bitcode serialize | bitcode::encode(output) |
| Response decoder | Bitcode deserialize | bitcode::decode(bytes) |
Sources: README.md:71-74 extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:1-6
Testing Service Definitions
Service definitions can be tested end-to-end using the integration test pattern shown in extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:39-142:
- Start a real
RpcServerwith handlers registered - Connect a client (native or WASM)
- Invoke services using the
callmethod - Assert on the results
graph TB
Server["Start RpcServer\non random port"]
Register["Register handlers\nAdd, Mult, Echo"]
Client["Create RpcClient\nor RpcWasmClient"]
CallService["Invoke Add::call"]
Assert["Assert results"]
Server --> Register
Register --> Client
Client --> CallService
CallService --> Assert
Test Pattern
Diagram: End-to-End Service Test Pattern
This pattern validates the complete round-trip: serialization, transport, dispatch, execution, and deserialization.
Sources: extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:39-142 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:229-312
Summary
Defining a simple RPC service in muxio requires:
- Creating a shared crate for service definitions
- Defining input/output types with
bitcodeserialization - Implementing the
RpcMethodPrebufferedtrait with encode/decode methods - Using compile-time generated
METHOD_IDfor registration and dispatch - Registering handlers on the server using the service’s static methods
- Invoking methods on the client using the
RpcCallPrebuffered::calltrait
This pattern ensures compile-time type safety, eliminates a large class of runtime errors, and enables the same service definitions to work across native and WASM clients without modification.
Sources: README.md:66-162 extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:1-99 extensions/muxio-wasm-rpc-client/tests/prebuffered_integration_tests.rs:1-312