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.

Type Safety and Shared Definitions

Relevant source files

Purpose and Scope

This document explains how rust-muxio achieves compile-time type safety for RPC operations through shared service definitions. It covers the RpcMethodPrebuffered trait, how method definitions are shared between clients and servers, and the compile-time guarantees that eliminate entire classes of distributed system bugs.

For step-by-step instructions on implementing custom service definitions, see Creating Service Definitions. For details on METHOD_ID generation and collision prevention, see Method ID Generation. For serialization implementation details, see Serialization with Bitcode.


Architecture Overview

The type safety model in rust-muxio is built on a simple principle: both client and server implementations depend on the same service definition crate. This shared dependency ensures that any incompatibility between client expectations and server behavior results in a compile-time error rather than a runtime failure.

graph TB
    subgraph ServiceDef["Service Definition Crate\n(example-muxio-rpc-service-definition)"]
Trait["RpcMethodPrebuffered trait"]
Add["Add struct\nMETHOD_ID: u64"]
Mult["Mult struct\nMETHOD_ID: u64"]
Echo["Echo struct\nMETHOD_ID: u64"]
end
    
    subgraph Client["Client Implementations"]
TokioClient["muxio-tokio-rpc-client"]
WasmClient["muxio-wasm-rpc-client"]
ClientCode["Application Code:\nAdd::call(client, params)"]
end
    
    subgraph Server["Server Implementation"]
TokioServer["muxio-tokio-rpc-server"]
ServerCode["Handler Registration:\nendpoint.register_prebuffered(Add::METHOD_ID, handler)"]
end
    
 
   Trait --> Add
 
   Trait --> Mult
 
   Trait --> Echo
    
    Add -.depends on.-> ClientCode
    Add -.depends on.-> ServerCode
    
 
   ClientCode --> TokioClient
 
   ClientCode --> WasmClient
 
   ServerCode --> TokioServer
    
    Add -.enforces contract.-> ClientCode
    Add -.enforces contract.-> ServerCode

Shared Definition Pattern

Sources:


The RpcMethodPrebuffered Trait

The [RpcMethodPrebuffered trait](https://github.com/jzombie/rust-muxio/blob/fcb45826/RpcMethodPrebuffered trait) defines the contract for a complete request/response RPC method. Each method implementation provides:

Trait ElementTypePurpose
METHOD_IDu64Compile-time unique identifier (xxhash of method name)
InputAssociated TypeRequest parameter type
OutputAssociated TypeResponse result type
encode_request()MethodSerialize Input to bytes
decode_request()MethodDeserialize bytes to Input
encode_response()MethodSerialize Output to bytes
decode_response()MethodDeserialize bytes to Output

Type Flow Through System Layers

Sources:


Compile-Time Guarantees

Type Safety Enforcement

The shared service definition pattern enforces three critical guarantees at compile time:

  1. Parameter Type Matching : The client's call site must pass arguments that match the Input associated type
  2. Response Type Matching : The handler must return a value matching the Output associated type
  3. METHOD_ID Uniqueness : Duplicate method names result in compile-time constant collision

Example: Type Mismatch Detection

The following table illustrates how type mismatches are caught:

ScenarioClient CodeServer CodeResult
CorrectAdd::call(client, vec![1.0, 2.0])Handler returns f64✓ Compiles
Wrong Input TypeAdd::call(client, "invalid")Handler expects Vec<f64>✗ Compile error: type mismatch
Wrong Output TypeClient expects f64Handler returns String✗ Compile error: trait bound not satisfied
Missing HandlerAdd::call(client, params)No handler registered✓ Compiles, runtime NotFound error

Code Entity Mapping

Sources:


Client-Server Contract Enforcement

RpcCallPrebuffered Implementation

The [RpcCallPrebuffered trait](https://github.com/jzombie/rust-muxio/blob/fcb45826/RpcCallPrebuffered trait) provides the client-side call interface. It is automatically implemented for all types that implement RpcMethodPrebuffered:

This design ensures that:

  • The call() method always receives Self::Input (enforced by trait bounds)
  • The return type is always Result<Self::Output, _> (enforced by trait signature)
  • Both encoding and decoding use the shared implementation from RpcMethodPrebuffered

Sources:

Cross-Platform Type Safety

Sources:


Integration Test Validation

The integration tests demonstrate identical usage patterns across both Tokio and WASM clients, validating the shared definition model:

Tokio Client Test Pattern

1. Server registers handler: endpoint.register_prebuffered(Add::METHOD_ID, handler)
2. Handler decodes:          Add::decode_request(&request_bytes)
3. Handler encodes:          Add::encode_response(sum)
4. Client calls:             Add::call(&client, vec![1.0, 2.0, 3.0])
5. Assertion:                assert_eq!(result, 6.0)

WASM Client Test Pattern

1. Server registers handler: endpoint.register_prebuffered(Add::METHOD_ID, handler)
2. Handler decodes:          Add::decode_request(&request_bytes)
3. Handler encodes:          Add::encode_response(sum)
4. Client calls:             Add::call(&client, vec![1.0, 2.0, 3.0])
5. Assertion:                assert_eq!(result, 6.0)

The patterns are identical except for the client type. This demonstrates that:

  • Application logic is transport-agnostic
  • Service definitions work identically across platforms
  • Type safety is maintained regardless of transport implementation

Sources:


Error Handling and Type Safety

Runtime Error Detection

While type mismatches are caught at compile time, certain errors can only be detected at runtime:

Error TypeDetection TimeExample
Type mismatchCompile timeAdd::call(client, "wrong type") → Compile error
Missing field in structCompile timeClient uses Add v2, server has Add v1 → Compile error if both recompile
Method not registeredRuntimeClient calls Add, server never registered handler → RpcServiceError::NotFound
Handler logic errorRuntimeHandler returns Err("Addition failed")RpcServiceError::Rpc

Runtime Error Propagation

Sources:


Large Payload Handling

The RpcCallPrebuffered implementation includes automatic handling for large payloads. The type system ensures that this complexity is transparent to application code:

Payload SizeTransport StrategyType Safety Impact
< 64KBSent in rpc_param_bytes header fieldNone - same types
≥ 64KBSent in rpc_prebuffered_payload_bytes, chunked automaticallyNone - same types

The decision is made based on the serialized byte length, not the Rust type. The following code from extensions/muxio-rpc-service-caller/src/prebuffered/traits.rs:58-65 shows this logic:

Application code calling Add::call(client, vec![1.0; 1_000_000]) receives the same type safety guarantees as Add::call(client, vec![1.0; 3]).

Sources:


Summary

The type safety model in rust-muxio provides the following guarantees:

  1. Compile-Time Contract Enforcement : Both client and server depend on the same service definition crate, ensuring API compatibility
  2. Type-Safe Method Calls : The RpcCallPrebuffered trait ensures that Input and Output types are correctly used at all call sites
  3. Transport Agnosticism : The same type-safe definitions work identically across Tokio and WASM clients
  4. Automatic Serialization : Encoding and decoding are encapsulated in the method definition, hidden from application code
  5. Early Error Detection : Type mismatches, missing fields, and incompatible changes result in compile errors, not runtime failures

This design eliminates an entire class of distributed system bugs where client and server implementations drift apart over time. Any breaking change to a service definition requires both client and server code to be updated simultaneously, and the compiler enforces this constraint.

Sources:

Dismiss

Refresh this wiki

Enter email to refresh