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.
- 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
lexprandserde-lexpr - Zero-copy message handling for optimal performance
- Connection pooling and efficient resource management
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(())
}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(())
}Add to your Cargo.toml:
[dependencies]
elrpc = "0.1"
tokio = { version = "1.0", features = ["full"] }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(())
}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(())
}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(())
}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(())
}- Transport: TCP
- Encoding: UTF-8
- Format: Length-prefixed S-expressions
- Structure:
[6-byte length][S-expression payload]
| 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 |
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
# 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# 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!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),
}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?;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?;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# Run tests
cargo test
# Run with tracing
cargo test -- --nocapture
# Run specific integration tests
cargo test --test integration_tests- Throughput: 10,000+ calls/second (local)
- Latency: <1ms round-trip (local)
- Memory: <1MB per connection
- CPU: Minimal overhead with zero-copy serialization
- Fork the repository
- Create a feature branch:
git checkout -b feature/new-feature - Commit changes:
git commit -am 'Add new feature' - Push to branch:
git push origin feature/new-feature - Submit a pull request
Licensed under either of:
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or https://opensource.org/licenses/Apache-2.0)
- python-epc - Python EPC implementation
- ruby-elrpc - Ruby ELRPC implementation
- node-elrpc - Node.js ELRPC implementation
- emacs-epc - Emacs EPC library
This project builds upon the excellent work of the EPC protocol designers and the lexpr-rs team for S-expression handling in Rust.