rapace-cell

High-level cell runtime for rapace that eliminates boilerplate.
This crate provides simple APIs for building rapace cells that communicate via SHM transport. It handles all the common setup that every cell needs:
- CLI argument parsing (
--shm-path or positional args)
- Waiting for the host to create the SHM file
- SHM session setup with standard configuration
- RPC session creation with correct channel ID conventions (cells use even IDs)
- Service dispatcher setup
Before & After
Before (95+ lines of boilerplate)
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use rapace::transport::shm::{ShmSession, ShmSessionConfig, ShmTransport};
use rapace::{Frame, RpcError, RpcSession};
const SHM_CONFIG: ShmSessionConfig = ShmSessionConfig {
ring_capacity: 256,
slot_size: 65536,
slot_count: 128,
};
struct Args {
shm_path: PathBuf,
}
fn parse_args() -> Result<Args> {
let mut args = std::env::args();
args.next();
let shm_path = match args.next() {
Some(path) => PathBuf::from(path),
None => return Err(eyre!("Usage: cell <shm_path>")),
};
Ok(Args { shm_path })
}
fn create_dispatcher(
impl_: MyServiceImpl,
) -> impl Fn(u32, u32, Vec<u8>)
-> Pin<Box<dyn Future<Output = Result<Frame, RpcError>> + Send>>
+ Send + Sync + 'static
{
move |_channel_id, method_id, payload| {
let impl_ = impl_.clone();
Box::pin(async move {
let server = MyServiceServer::new(impl_);
server.dispatch(method_id, &payload).await
})
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args = parse_args()?;
while !args.shm_path.exists() {
tokio::time::sleep(Duration::from_millis(100)).await;
}
let shm_session = ShmSession::open_file(&args.shm_path, SHM_CONFIG)?;
let transport = Arc::new(ShmTransport::new(shm_session));
let session = Arc::new(RpcSession::with_channel_start(transport, 2));
let dispatcher = create_dispatcher(MyServiceImpl);
session.set_dispatcher(dispatcher);
session.run().await?;
Ok(())
}
After (3 lines!)
use rapace_cell::run;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
run(MyServiceServer::new(MyServiceImpl)).await?;
Ok(())
}
Usage
Single-service cells
For simple cells that expose a single service:
use rapace_cell::{run, ServiceDispatch};
use rapace::{Frame, RpcError};
use std::future::Future;
use std::pin::Pin;
#[derive(Clone)]
struct MyServiceImpl;
struct MyServiceServer {
impl_: MyServiceImpl,
}
impl MyServiceServer {
fn new(impl_: MyServiceImpl) -> Self {
Self { impl_ }
}
async fn dispatch(&self, method_id: u32, payload: &[u8]) -> Result<Frame, RpcError> {
todo!()
}
}
impl ServiceDispatch for MyServiceServer {
fn dispatch(
&self,
method_id: u32,
payload: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Frame, RpcError>> + Send + 'static>> {
Box::pin(Self::dispatch(self, method_id, payload))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let server = MyServiceServer::new(MyServiceImpl);
run(server).await?;
Ok(())
}
Multi-service cells
For cells that expose multiple services:
use rapace_cell::{run_multi, DispatcherBuilder, ServiceDispatch};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
run_multi(|builder| {
builder
.add_service(MyServiceServer::new(MyServiceImpl))
.add_service(AnotherServiceServer::new(AnotherServiceImpl))
}).await?;
Ok(())
}
Using RpcSessionExt for custom setups
If you need more control but still want simplified service setup:
use rapace_cell::{RpcSessionExt, DEFAULT_SHM_CONFIG};
use rapace::transport::shm::{ShmSession, ShmTransport};
use rapace::RpcSession;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let shm_session = ShmSession::open_file(&shm_path, DEFAULT_SHM_CONFIG)?;
let transport = Arc::new(ShmTransport::new(shm_session));
let session = Arc::new(RpcSession::with_channel_start(transport, 2));
session.set_service(MyServiceServer::new(MyServiceImpl));
session.run().await?;
Ok(())
}
Configuration
The default SHM configuration is:
pub const DEFAULT_SHM_CONFIG: ShmSessionConfig = ShmSessionConfig {
ring_capacity: 256, slot_size: 65536, slot_count: 128, };
You can customize this with run_with_config() or run_multi_with_config():
use rapace_cell::{run_with_config, DEFAULT_SHM_CONFIG};
use rapace::transport::shm::ShmSessionConfig;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let custom_config = ShmSessionConfig {
ring_capacity: 512,
slot_size: 131072, slot_count: 256,
};
run_with_config(MyServiceServer::new(MyServiceImpl), custom_config).await?;
Ok(())
}
CLI Arguments
The cell runtime accepts the SHM path in two formats:
./my-cell --shm-path=/tmp/my-app.shm
./my-cell /tmp/my-app.shm
Channel ID Conventions
The cell runtime automatically uses the correct channel ID convention:
- Cells: Even channel IDs starting from 2 (2, 4, 6, ...)
- Hosts: Odd channel IDs starting from 1 (1, 3, 5, ...)
You don't need to worry about this - it's handled automatically.
Error Handling
The cell runtime provides a CellError type that covers common failure modes:
CellError::Args - Invalid command-line arguments
CellError::ShmTimeout - SHM file not created by host within 5 seconds
CellError::ShmOpen - Failed to open SHM session
CellError::Rpc - RPC session error
Tracing
The cell runtime doesn't configure tracing by default - you should set it up yourself in main():
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
run(MyServiceServer::new(MyServiceImpl)).await?;
Ok(())
}
License
MIT OR Apache-2.0