Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GitHub

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

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:

DependencyPurpose
muxio-rpc-serviceProvides RpcMethodPrebuffered trait
bitcodeBinary serialization (with derive feature)
serdeRequired 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

MemberDescription
InputThe type of the method’s parameters
OutputThe type of the method’s return value
METHOD_NAMEA unique string identifier (used for ID generation)
METHOD_IDA compile-time generated u64 via xxhash of METHOD_NAME

Required Methods

MethodPurpose
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:

  1. Access the RpcServiceEndpoint from the server instance
  2. Call register_prebuffered with the service’s METHOD_ID
  3. Provide an async handler that:
    • Decodes the request using ServiceType::decode_request
    • Performs the business logic
    • Encodes the response using ServiceType::encode_response

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 get Vec<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 call method 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

ConditionStrategyLocation
encoded_args.len() < DEFAULT_SERVICE_MAX_CHUNK_SIZESend in header framerpc_param_bytes field
encoded_args.len() >= DEFAULT_SERVICE_MAX_CHUNK_SIZEStream as payloadrpc_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:

ComponentImplementationExample for Add
Service structZero-sized typepub struct Add;
Input typeAssociated typeVec<f64>
Output typeAssociated typef64
Method nameStatic string"Add"
Method IDCompile-time hashxxhash64("Add")
Request encoderBitcode serializebitcode::encode(input)
Request decoderBitcode deserializebitcode::decode(bytes)
Response encoderBitcode serializebitcode::encode(output)
Response decoderBitcode deserializebitcode::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:

  1. Start a real RpcServer with handlers registered
  2. Connect a client (native or WASM)
  3. Invoke services using the call method
  4. 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:

  1. Creating a shared crate for service definitions
  2. Defining input/output types with bitcode serialization
  3. Implementing the RpcMethodPrebuffered trait with encode/decode methods
  4. Using compile-time generated METHOD_ID for registration and dispatch
  5. Registering handlers on the server using the service’s static methods
  6. Invoking methods on the client using the RpcCallPrebuffered::call trait

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