/rust-elrpc

Rust implementation of Emacs RPC (EPC) protocol

Primary LanguageRust

Rust ELRPC

Crates.io Documentation MIT License Apache 2.0

A high-performance, async Rust implementation of the Emacs RPC (EPC) protocol, enabling seamless communication between Emacs and Rust applications using S-expressions over TCP.

Features

  • Async-first design built on Tokio
  • Cross-language compatibility with Python, Ruby, Node.js, and Emacs EPC implementations
  • Type-safe method registration with automatic serialization/deserialization
  • Bidirectional communication - both client and server can define methods
  • Process management for spawning and managing external processes
  • Comprehensive error handling with application and protocol-level errors
  • S-expression serialization via lexpr and serde-lexpr
  • Zero-copy message handling for optimal performance
  • Connection pooling and efficient resource management

Quick Start

Server Example

use elrpc::{Result, Server};
use tokio;

#[tokio::main]
async fn main() -> Result<()> {
    // Create server
    let mut server = Server::new();
    let addr = server.bind("127.0.0.1:0").await?;
    
    // Register methods
    server
        .register_method(
            "add",
            |(a, b): (i64, i64)| Ok(a + b),
            Some("a b"),
            Some("Add two numbers"),
        )
        .await?;
    
    server
        .register_value_method(
            "echo",
            |args| Ok(args),
            Some("args"),
            Some("Echo back the arguments"),
        )
        .await?;
    
    println!("Server starting on port {}", addr.port());
    server.print_port()?; // Print port for Emacs compatibility
    
    // Start serving
    server.serve().await?;
    Ok(())
}

Client Example

use elrpc::{Client, Result};
use tokio;

#[tokio::main]
async fn main() -> Result<()> {
    // Connect to server
    let client = Client::connect("127.0.0.1:12345").await?;
    
    // Call remote methods
    let sum: i64 = client.call_sync("add", (5, 3)).await?;
    println!("5 + 3 = {}", sum);
    
    let echo: String = client.call_sync("echo", "Hello from Rust!").await?;
    println!("Echo: {}", echo);
    
    client.close().await?;
    Ok(())
}

Installation

Add to your Cargo.toml:

[dependencies]
elrpc = "0.1"
tokio = { version = "1.0", features = ["full"] }

Usage

Creating a Server

use elrpc::{Server, Result};

async fn create_server() -> Result<()> {
    let mut server = Server::new();
    
    // Bind to any available port
    let addr = server.bind("127.0.0.1:0").await?;
    println!("Server bound to: {}", addr);
    
    // Register typed methods
    server
        .register_method(
            "calculate",
            |(x, y, op): (f64, f64, String)| {
                match op.as_str() {
                    "+" => Ok(x + y),
                    "-" => Ok(x - y),
                    "*" => Ok(x * y),
                    "/" => Ok(x / y),
                    _ => Err("Invalid operation".into()),
                }
            },
            Some("x y op"),
            Some("Perform calculation"),
        )
        .await?;
    
    // Register methods working with raw S-expressions
    server
        .register_value_method(
            "process_list",
            |args| {
                if let Some(list) = args.as_vec() {
                    Ok(list.len().into())
                } else {
                    Err("Expected list".into())
                }
            },
            Some("list"),
            Some("Process a list and return its length"),
        )
        .await?;
    
    server.serve().await?;
    Ok(())
}

Creating a Client

use elrpc::{Client, Result};

async fn create_client() -> Result<()> {
    let client = Client::connect("127.0.0.1:12345").await?;
    
    // Synchronous calls
    let result: f64 = client.call_sync("calculate", (10.0, 5.0, "*")).await?;
    println!("10 * 5 = {}", result);
    
    // Asynchronous calls
    let future = client.call_async::<_, i64>("add", (1, 2));
    let sum = future.await?;
    println!("1 + 2 = {}", sum);
    
    // Query available methods
    let methods = client.query_methods().await?;
    for method in methods {
        println!("{}: {}", method.name, method.docstring.unwrap_or_default());
    }
    
    client.close().await?;
    Ok(())
}

Process Management

use elrpc::{Process, Result};

async fn manage_process() -> Result<()> {
    // Spawn a Python EPC server
    let mut process = Process::spawn(
        "python3",
        &["-m", "epc.server"]
    ).await?;
    
    // Call methods on the spawned process
    let result: String = process.call_sync("greet", "World").await?;
    println!("Python says: {}", result);
    
    // Stop the process
    process.stop().await?;
    Ok(())
}

Working with Complex Data Types

use serde::{Deserialize, Serialize};
use lexpr::Value;

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
    skills: Vec<String>,
}

async fn complex_types() -> elrpc::Result<()> {
    let client = Client::connect("127.0.0.1:12345").await?;
    
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
        skills: vec!["Rust".to_string(), "Emacs".to_string()],
    };
    
    // Automatic serialization/deserialization
    let processed: Person = client.call_sync("process_person", person).await?;
    
    // Working with S-expressions directly
    let sexp = lexpr::from_str("((name . \"Bob\") (age . 25))")?;
    let result: Value = client.call_sync("process_sexp", sexp).await?;
    
    client.close().await?;
    Ok(())
}

Protocol Details

Message Format

  • Transport: TCP
  • Encoding: UTF-8
  • Format: Length-prefixed S-expressions
  • Structure: [6-byte length][S-expression payload]

Message Types

Type Format Description
Call (call uid method-name [args...]) Invoke remote method
Return (return uid result) Successful method return
Error (return-error uid [class message backtrace]) Application error
Protocol Error (epc-error uid message) Protocol-level error
Methods (methods uid) Query available methods

Cross-language Compatibility

Rust ELRPC is compatible with existing EPC implementations:

  • Python: python-epc
  • Ruby: ruby-elrpc
  • Node.js: node-elrpc
  • Emacs: Built-in EPC support via epc.el

Example: Python Client → Rust Server

# Python client
from epc.client import EPCClient

client = EPCClient(('localhost', 12345))
client.connect()

result = client.call_sync('add', [5, 3])
print(f"5 + 3 = {result}")  # 5 + 3 = 8

Example: Rust Client → Python Server

# Python server
from epc.server import EPCServer

server = EPCServer(('localhost', 0))

@server.register_function
def greet(name):
    return f"Hello, {name}!"

server.print_port()
server.serve_forever()
// Rust client
let client = Client::connect("localhost:12345").await?;
let greeting: String = client.call_sync("greet", "World").await?;
println!("{}", greeting); // Hello, World!

Error Handling

use elrpc::ERPCError;

match client.call_sync::<_, i64>("divide", (10, 0)).await {
    Ok(result) => println!("Result: {}", result),
    Err(ERPCError::ApplicationError { message, .. }) => {
        println!("Application error: {}", message);
    }
    Err(ERPCError::MethodNotFound(name)) => {
        println!("Method not found: {}", name);
    }
    Err(e) => println!("Other error: {}", e),
}

Configuration

Server Configuration

use elrpc::ServerConfig;
use std::time::Duration;

let config = ServerConfig {
    bind_addr: "127.0.0.1:0".to_string(),
    max_connections: 100,
    worker_threads: 4,
    timeout: Duration::from_secs(30),
    ..Default::default()
};

let server = Server::with_config(config).await?;

Client Configuration

use elrpc::ClientConfig;

let config = ClientConfig {
    connect_timeout: Duration::from_secs(5),
    request_timeout: Duration::from_secs(30),
    max_retries: 3,
    retry_delay: Duration::from_millis(100),
};

let client = Client::connect_with_config("127.0.0.1:12345", config).await?;

Examples

Run the included examples:

# Terminal 1: Start the echo server
cargo run --example echo_server

# Terminal 2: Run the client
cargo run --example echo_client

Testing

# Run tests
cargo test

# Run with tracing
cargo test -- --nocapture

# Run specific integration tests
cargo test --test integration_tests

Performance

  • Throughput: 10,000+ calls/second (local)
  • Latency: <1ms round-trip (local)
  • Memory: <1MB per connection
  • CPU: Minimal overhead with zero-copy serialization

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/new-feature
  3. Commit changes: git commit -am 'Add new feature'
  4. Push to branch: git push origin feature/new-feature
  5. Submit a pull request

License

Licensed under either of:

Related Projects

Acknowledgments

This project builds upon the excellent work of the EPC protocol designers and the lexpr-rs team for S-expression handling in Rust.