Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ice RPC Framework

A modern, high-performance RPC framework for Rust inspired by ZeroC Ice, featuring complete Ice API compatibility, bidirectional RPC, connection multiplexing, type-safe service definitions via #[ice_service] macro, and 100% async/await API.

🚀 Highlights

  • ZeroC Ice API Compatible - camelCase & snake_case dual API, seamless migration
  • Bidirectional RPC - Server-to-client callbacks over existing connections (firewall-friendly)
  • Router Support - Glacier2-compatible routing with createObjectAdapterWithRouter
  • Zero Boilerplate - One macro generates everything: trait, client, server, servant
  • 100% Async - Built on Tokio, all operations are non-blocking
  • Type-Safe - Compile-time checking, no runtime reflection
  • Connection Multiplexing - Up to 10,000 streams per TCP connection
  • Ice-Inspired Architecture - Proxy-Servant pattern, Communicator, ObjectAdapter
  • Multi-Protocol Support - TCP, UDP, SSL, WebSocket, HTTP/2, HTTP/3, IPC
  • Native Rust - Uses Rust traits and proc-macros, no external IDL compiler

Quick Start

1. Define Your Service

use ice0::prelude::*;

// Define service with one macro
#[ice_service]
pub trait Greeter {
    async fn greet(name: String) -> String;
    async fn add(a: i32, b: i32) -> i32;
}

The #[ice_service] macro automatically generates:

  • Greeter trait - Service interface with context parameter
  • GreeterClient - Client proxy for RPC calls
  • GreeterServer - Request dispatcher
  • GreeterServant - Ice framework integration ⭐

2. Implement the Service

pub struct GreeterImpl;

#[async_trait::async_trait]
impl Greeter for GreeterImpl {
    async fn greet(&self, _context: context::Context, name: String) -> String {
        format!("Hello, {}! Welcome to Ice RPC!", name)
    }

    async fn add(&self, _context: context::Context, a: i32, b: i32) -> i32 {
        a + b
    }
}

3. Create the Server

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create communicator
    let comm = Communicator::new().await?;
    
    // Create adapter with endpoint
    let adapter = comm.create_object_adapter("GreeterAdapter", vec![
        Endpoint::from_string("tcp -h 127.0.0.1 -p 9529")?
    ]).await?;
    
    // ✅ One line to create servant (auto-generated by macro!)
    let servant = GreeterServant::new(GreeterImpl);
    
    // Register servant
    adapter.add_servant(
        Identity::new("Greeter"),  // Must match trait name
        Arc::new(servant),
    ).await;
    
    // Activate adapter
    adapter.activate().await;
    comm.activate_adapter(&adapter).await;
    
    info!("Greeter service listening on port 9529");
    
    // Wait for shutdown
    tokio::signal::ctrl_c().await?;
    comm.shutdown().await?;
    
    Ok(())
}

4. Create the Client

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create communicator
    let comm = Communicator::new().await?;
    
    // Create proxy (Ice's stringToProxy pattern)
    let proxy = comm
        .string_to_proxy("Greeter:tcp -h 127.0.0.1 -p 9529")
        .await?;
    
    // Get multiplexer and create client
    let mux = proxy.mux().unwrap().clone();
    let client = GreeterClient::new(mux, 0);
    
    // Call RPC methods - type-safe!
    let greeting = client.greet("Ice RPC".to_string()).await?;
    println!("{}", greeting); // "Hello, Ice RPC! Welcome to Ice RPC!"
    
    let sum = client.add(42, 58).await?;
    println!("42 + 58 = {}", sum); // 100
    
    Ok(())
}

That's it! No manual serialization, no message handling, no dispatch logic. The macro generates everything!

🔄 Bidirectional RPC (Server-to-Client Callbacks)

Our framework supports Ice-style bidirectional connections where the server can callback the client over the existing client-initiated connection:

Client Side

// 1. Create callback adapter (no endpoints)
let callback_adapter = comm
    .createObjectAdapter("ClientCallback")  // Ice style!
    .await?;

// 2. Register callback servant
adapter.add(
    Identity::new("ClientEventCallback"),
    Arc::new(ClientCallbackServant)
).await;

// 3. Activate adapter
adapter.activate().await;

// 4. Get connection and bind adapter
let connection = proxy.ice_getConnection().unwrap();  // Ice API!
connection.setAdapter(callback_adapter);             // Ice API!

Server Side

// In your servant's dispatch method:
async fn dispatch(&self, identity, operation, request, context) {
    // Get callback proxy from context
    if let Some(callback_proxy) = context.callback_proxy(callback_identity) {
        // Callback to client over existing connection!
        callback_proxy.invoke("onEvent", event_data).await?;
    }
}

Key Features

  • Firewall-Friendly - Reuses client-initiated connection
  • Ice API Compatible - ice_getConnection(), setAdapter(), createProxy()
  • Full ObjectAdapter - Uses complete Ice adapter infrastructure
  • Type-Safe - Compile-time servant dispatch
  • No Extra Ports - No need for server to connect back to client

Important: Correct Callback Handling

When receiving callbacks on the client side, you must dispatch through ObjectAdapter, not directly to Multiplexer:

// ✅ CORRECT - Dispatch through ObjectAdapter
match callback_adapter.dispatch(identity, operation, data, context).await {
    Ok(response) => {
        let response_msg = Message::response(request_id, 0, response);
        mux.send_message(response_msg).await?;
    }
    Err(e) => {
        let error_msg = Message::error(request_id, 0, e.to_string());
        mux.send_message(error_msg).await?;
    }
}

// ❌ WRONG - Multiplexer doesn't have registered servants
mux.dispatch_request(&identity, &operation, data, request_id).await?;

See examples/bidirectional_client.rs and examples/bidirectional_server.rs for complete examples.

Ice API Compatibility

Core Features

Type-Safe Service Definition - #[ice_service] macro generates all code
Auto-Generated Servant - No boilerplate, just implement the trait
Proxy-Servant Architecture - Clean separation of client/server
100% Async/Await - Built on Tokio, zero blocking
Connection Multiplexing - Multiple logical streams over one TCP connection
Multi-Protocol Transports - TCP, UDP, SSL, WebSocket, HTTP/2, HTTP/3, IPC
Ice-Style Identity - Object references with string format
Deadline Propagation - Automatic timeout handling
Request Context - Metadata propagation
Error Handling - Typed error types with thiserror
Dual API Styles - camelCase (Ice) and snake_case (Rust)

Ice API Methods

Our framework provides complete Ice API compatibility with both camelCase and snake_case styles:

Communicator

// Ice style (camelCase)
let proxy = comm.stringToProxy("Greeter:tcp -h 127.0.0.1 -p 9630").await?;
let adapter = comm.createObjectAdapter("MyAdapter").await?;
let adapter = comm.createObjectAdapterWithEndpoints("MyAdapter", "tcp -h 127.0.0.1 -p 9630").await?;
let router_adapter = comm.createObjectAdapterWithRouter("CallbackAdapter", Some(router_proxy)).await?;

// Rust style (snake_case)
let proxy = comm.string_to_proxy("Greeter:tcp -h 127.0.0.1 -p 9630").await?;
let adapter = comm.create_object_adapter("MyAdapter", vec![]).await?;

Router Configuration

// Set default router (all new proxies will use it)
let router = comm.string_to_proxy("Glacier2/router:tcp -h router:4063").await?;
comm.setDefaultRouter(Some(router)).await;
comm.set_default_router(Some(router)).await;  // Rust style

// Get default router
let router = comm.getDefaultRouter().await;
let router = comm.get_default_router().await;  // Rust style

// Create adapter with router (for receiving callbacks)
let callback_adapter = comm.createObjectAdapterWithRouter(
    "ClientCallbackAdapter",
    Some(router_proxy)
).await?;

// Check if adapter has router
if adapter.has_router().await {
    let router = adapter.get_router().await;
}

Proxy Router Methods

// Configure router for specific proxy
let proxy = proxy.ice_router(Some(router));
let router = proxy.ice_getRouter();

Proxy

// Get connection for bidirectional RPC
let conn = proxy.ice_getConnection().unwrap();
let conn = proxy.ice_getCachedConnection().unwrap();

// Configure proxy
let proxy = proxy.ice_oneway();
let proxy = proxy.ice_twoway();
let proxy = proxy.ice_timeout(30000);  // 30 seconds
let proxy = proxy.ice_isFixed();

Connection

// Bidirectional RPC setup
conn.setAdapter(callback_adapter);
let adapter = conn.getAdapter();
let callback_proxy = conn.createProxy(callback_identity);

// Connection management
conn.close().await?;
let active = conn.is_active();
let type_ = conn.type_();

ObjectAdapter

// Lifecycle
adapter.activate().await;
adapter.hold().await;
adapter.deactivate().await;
adapter.waitForHold().await;

// Servant management
adapter.add(identity, servant).await;
adapter.addDefaultServant(servant, Some("category")).await;
adapter.remove_servant(&identity).await;

// Information
let name = adapter.get_name();

See ICE_API_COMPATIBILITY.md for complete API reference.

Invocation Modes

  • Twoway - Request-response with response future
  • Oneway - Fire-and-forget
  • Datagram - Best-effort over UDP

Architecture

┌──────────────────────────────────────────────┐
│           Application Layer                   │
│  ┌──────────────────────────────────────┐    │
│  │  #[ice_service] Generated Code       │    │
│  │  - Trait, Client, Server, Servant    │    │
│  └──────────────────────────────────────┘    │
└──────────────────────────────────────────────┘
                   ▲
                   │
┌──────────────────┴──────────────────────────┐
│         Ice Framework Layer                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │Communicat│  │Object    │  │Bidirect-  │  │
│  │or        │  │Adapter   │  │ional RPC  │  │
│  └──────────┘  └──────────┘  └───────────┘  │
└──────────────────────────────────────────────┘
                   ▲
                   │
┌──────────────────┴──────────────────────────┐
│         Multiplexing & Transport             │
│  ┌──────────────┐      ┌─────────────────┐  │
│  │ Multiplexer  │      │ TCP/UDP/SSL/    │  │
│  │ (Streams)    │      │ WS/HTTP2/3/IPC  │  │
│  └──────────────┘      └─────────────────┘  │
└──────────────────────────────────────────────┘

Key Concepts

1. Communicator

Central entry point for the RPC system:

let comm = Communicator::new().await?;

// Create server adapter
let adapter = comm.create_object_adapter("MyAdapter", vec![
    Endpoint::from_string("tcp -h 127.0.0.1 -p 9529")?
]).await?;

// Create client proxy
let proxy = comm.string_to_proxy(
    "ServiceName:tcp -h 127.0.0.1 -p 9529"
).await?;

2. ObjectAdapter

Manages servants and listens for connections:

adapter.add_servant(
    Identity::new("Greeter"),
    Arc::new(GreeterServant::new(GreeterImpl)),
).await;

adapter.activate().await;
comm.activate_adapter(&adapter).await;

3. Identity System

Ice-style object identification:

// Simple identity
let identity = Identity::new("Greeter");

// Identity with category
let identity = Identity::with_category("Greeter", "v1");

// String conversion
let proxy_string = "Greeter:tcp -h 127.0.0.1 -p 9529";

Important: Identity name must match the trait name (case-sensitive)!

4. Service Macro

The #[ice_service] macro is the heart of the framework:

#[ice_service]
pub trait MyService {
    async fn method1(arg1: String, arg2: i32) -> String;
    async fn method2(x: f64) -> Vec<u8>;
}

Generates:

// Trait
#[async_trait]
pub trait MyService: Send + Sync + 'static {
    async fn method1(&self, ctx: Context, arg1: String, arg2: i32) -> String;
    async fn method2(&self, ctx: Context, x: f64) -> Vec<u8>;
}

// Client
pub struct MyServiceClient { ... }
impl MyServiceClient {
    pub async fn method1(&self, arg1: String, arg2: i32) -> ice0::Result<String>;
    pub async fn method2(&self, x: f64) -> ice0::Result<Vec<u8>>;
}

// Server
pub struct MyServiceServer<S: MyService> { ... }
impl<S: MyService> MyServiceServer<S> {
    pub async fn handle_request(&self, ctx: Context, msg: Message) 
        -> ice0::Result<Message>;
}

// Servant (auto-generated!)
pub struct MyServiceServant<S: MyService> { ... }
impl<S: MyService + 'static> ice0::adapter::Servant for MyServiceServant<S> {
    async fn dispatch(&self, ...) -> Result<Vec<u8>, DispatchError>;
}

5. Context

Request metadata and deadline propagation:

pub struct Context {
    pub deadline: Option<Duration>,
    pub trace_id: Option<String>,
}

// Create with deadline
let ctx = Context::new()
    .with_deadline(Duration::from_secs(5));

Examples

Run the Greeter Example

Terminal 1 - Server:

cargo run --bin ice_service_server

Terminal 2 - Client:

cargo run --bin ice_service_client

Expected Output:

greet() response: Hello, Ice RPC! Welcome to Ice RPC!
add(42, 58) = 100

Bidirectional RPC Example

Terminal 1 - Server:

cargo run --bin bidirectional_server

Terminal 2 - Client:

cargo run --bin bidirectional_client

Expected Output:

[Client Callback] 📨 Event: Hello from server! Greeting: Ice RPC Framework
[Client Callback] 📊 Progress: 100%

More Examples

# Router example (Glacier2-compatible routing)
cargo run --bin router_example

# Legacy async API (without macro)
cargo run --bin ice_async_server
cargo run --bin ice_async_client

# Ice-style bidirectional
cargo run --bin ice_style_bidirectional

# Raw TCP example
cargo run --bin tcp_server
cargo run --bin tcp_client

# UDP example
cargo run --bin udp_server
cargo run --bin udp_client

# HTTP/2 example
cargo run --bin http2_server
cargo run --bin http2_client

# WebSocket example
cargo run --bin ws_server
cargo run --bin ws_client

Project Structure

ice/
├── ice/                   # Main crate (merged ice + ice-core)
│   └── src/
│       ├── adapter.rs         # ObjectAdapter & Servant management
│       ├── communicator.rs    # Communicator (central entry point)
│       ├── connection.rs      # Connection abstraction (bidirectional RPC)
│       ├── identity.rs        # Identity & ObjectReference
│       ├── message.rs         # Message types and protocol
│       ├── mux.rs             # Connection multiplexing
│       ├── proxy.rs           # Proxy (AMI client, Ice API)
│       ├── endpoints/         # Protocol implementations
│       │   ├── tcp.rs         # TCP transport
│       │   ├── udp.rs         # UDP transport
│       │   ├── ssl.rs         # SSL/TLS transport
│       │   ├── ws.rs          # WebSocket transport
│       │   ├── http2.rs       # HTTP/2 transport
│       │   ├── http3.rs       # HTTP/3 (QUIC) transport
│       │   └── ipc.rs         # IPC (Unix/Windows) transport
│       ├── error.rs       # Error types
│       ├── lib.rs         # Re-exports and prelude
│       └── prelude.rs     # Common imports
│
├── ice0-macro/             # #[ice_service] proc-macro
│   └── src/
│       └── lib.rs         # Code generation for trait, client, server, servant
│
├── examples/
│   ├── ice_service_server.rs    # ⭐ Macro-based server
│   ├── ice_service_client.rs    # ⭐ Macro-based client
│   ├── bidirectional_server.rs  # ⭐ Bidirectional RPC server
│   ├── bidirectional_client.rs  # ⭐ Bidirectional RPC client
│   ├── router_example.rs        # ⭐ Router/Glacier2 routing
│   ├── ice_style_bidirectional.rs  # Ice-style bidirectional
│   ├── ice_async_server.rs      # Legacy API server
│   ├── ice_async_client.rs      # Legacy API client
│   ├── tcp_server.rs            # Raw TCP example
│   ├── tcp_client.rs            # Raw TCP example
│   ├── udp_server.rs            # Raw UDP example
│   └── udp_client.rs            # Raw UDP example
│
├── README.md                # 本文件(中文)
├── README.EN.md             # 本文件(英文)
├── ARCHITECTURE.md          # 架构指南(中文)
├── ARCHITECTURE.EN.md       # 架构指南(英文)
└── ICE_API_COMPATIBILITY.md # Ice API 参考

Building & Testing

# Build all
cargo build

# Run tests
cargo test

# Run specific example
cargo run --bin ice_service_server
cargo run --bin ice_service_client

Comparison with ZeroC Ice

Feature ZeroC Ice Ice RPC (This Framework)
Language Multi-language (C++, Java, Python, etc.) Rust only
Code Generation External IDL compiler (slice2cpp, etc.) Rust proc-macro (zero config)
IDL Language Slice (custom language) Rust traits (native syntax)
API Style Synchronous + Async options 100% Async/Await
Runtime Custom runtime Tokio
Servant Pattern Abstract base class (inheritance) Trait implementation (composition)
Multiplexing Connection pooling Native stream multiplexing
Type Safety Runtime checks Compile-time checks
Boilerplate Manual servant implementation Auto-generated servant
Build Steps Compile .ice files first Just cargo build
Ice API Native Fully compatible (camelCase + snake_case)
Bidirectional RPC Yes (setAdapter) Yes (complete Ice pattern)
Protocols TCP, SSL, UDP, WS TCP, SSL, UDP, WS, HTTP/2, HTTP/3, IPC

Why This Framework?

vs. ZeroC Ice

  • No external compiler - Just cargo build
  • Native Rust syntax - No Slice language to learn
  • Zero boilerplate - One macro generates everything
  • Better type safety - Compile-time vs runtime checks
  • Modern async - Built for async/await from the ground up

vs. gRPC-tonic

  • Simpler API - No protobuf files, just Rust traits
  • Ice architecture - Proxy-Servant pattern is more flexible
  • Built-in multiplexing - No HTTP/2 complexity
  • Multiple transports - TCP, UDP (gRPC is HTTP-only)

vs. tarpc

  • More features - Servant locators, identity system, deadlines
  • Better multiplexing - Stream-based, not connection-per-call
  • Ice-inspired - Proven architecture from ZeroC

Technical Details

Serialization

Uses bincode for efficient binary serialization:

// Automatic in generated code
let args_bytes = bincode::serialize(&args)?;
let result: ReturnType = bincode::deserialize(&response_bytes)?;

Message Protocol

Custom binary protocol with frame-based encoding:

┌──────────┬─────────┬──────────┬───────────┐
│ Magic    │ Version │ MsgType │ StreamID  │
│ (4B)     │ (2B)    │ (1B)    │ (8B)      │
├──────────┼─────────┼──────────┼───────────┤
│ MessageID│ Service │ Method  │ Deadline  │
│ (8B)     │ (var)   │ (var)   │ (opt)     │
├──────────┼─────────┴──────────┴───────────┤
│ Content Length (4B)                       │
├───────────────────────────────────────────┤
│ Payload (variable length)                 │
└───────────────────────────────────────────┘

Multiplexing

Each TCP connection supports multiple logical streams:

  • Stream ID: 64-bit identifier
  • Message ID: 64-bit per-stream sequence
  • Max Streams: 10,000 per connection
  • Request-Response Correlation: Oneshot channels

Best Practices

1. Naming Convention

  • Trait name: PascalCase (e.g., Greeter, UserService)
  • Identity: Must match trait name exactly (case-sensitive!)
  • Methods: snake_case (e.g., get_user, send_message)
#[ice_service]
pub trait UserService {  // ← PascalCase
    async fn get_user(id: i32) -> User;
}

// Server
adapter.add_servant(
    Identity::new("UserService"),  // ← Must match exactly!
    Arc::new(servant),
).await;

2. Error Handling

// Client side
match client.greet(name).await {
    Ok(response) => println!("{}", response),
    Err(ice0::error::Error::Timeout) => eprintln!("Request timed out"),
    Err(ice0::error::Error::NotFound(msg)) => eprintln!("Method not found: {}", msg),
    Err(ice0::error::Error::Serialization(e)) => eprintln!("Serialization error: {}", e),
    Err(e) => eprintln!("Error: {}", e),
}

3. Context Propagation

// Client creates context with deadline
let ctx = context::Context::new()
    .with_deadline(Duration::from_secs(5));

// Server receives context
async fn greet(&self, context: Context, name: String) -> String {
    if let Some(deadline) = context.deadline {
        info!("Request deadline: {:?}", deadline);
    }
    format!("Hello, {}!", name)
}

4. Concurrent Requests

// Multiple concurrent RPCs
let mut handles = Vec::new();
for i in 1..=5 {
    let client = client.clone();
    let handle = tokio::spawn(async move {
        let name = format!("User{}", i);
        client.greet(name).await
    });
    handles.push(handle);
}

// Wait for all
for handle in handles {
    let result = handle.await.unwrap()?;
    println!("{}", result);
}

5. Bidirectional RPC

// Client: Dispatch callbacks through ObjectAdapter (NOT Multiplexer)
match adapter.dispatch(identity, op, data, context).await {
    Ok(response) => mux.send_message(Message::response(id, 0, response)).await?,
    Err(e) => mux.send_message(Message::error(id, 0, e.to_string())).await?,
}

// Server: Handle Response messages in receive loop
if message.header.message_type == MessageType::Response {
    mux.route_response(message);  // Route to waiting future
}

Roadmap

  • TLS/SSL transport support
  • WebSocket transport
  • HTTP/2 transport
  • HTTP/3 (QUIC) transport
  • IPC transport (Unix domain sockets + Windows named pipes)
  • Complete Ice API compatibility (camelCase + snake_case)
  • Bidirectional RPC (server-to-client callbacks)
  • Ice-style connection pattern (ice_getConnection, setAdapter, createProxy)
  • Response routing for bidirectional callbacks (route_response)
  • Router support (createObjectAdapterWithRouter, setDefaultRouter, Glacier2-compatible)
  • Compression (gzip, zstd)
  • Load balancing
  • Service discovery
  • Metrics export (Prometheus)
  • Retry policies
  • Circuit breaker pattern
  • Streaming RPC (bidirectional streams)
  • gRPC interoperability layer
  • IceGrid router support
  • Collocation optimization

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Acknowledgments

  • ZeroC Ice - For the excellent Proxy-Servant architecture inspiration
  • Tokio - For the async runtime
  • gRPC - For modern RPC design patterns
  • tarpc - For Rust-native RPC concepts

About

A modern, high-performance RPC framework for Rust inspired by ZeroC Ice, featuring complete Ice API compatibility, bidirectional RPC, connection multiplexing, type-safe service definitions via #[ice_service] macro, and 100% async/await API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages