Wireframe is an experimental Rust library that simplifies building servers and clients for custom binary protocols. The design borrows heavily from Actix Web to provide a familiar, declarative API for routing, extractors, and middleware.
Manual handling of binary protocols typically involves verbose serialization
code, custom frame parsing, and complex dispatch logic. wireframe aims to
reduce this boilerplate through layered abstractions:
- Transport adapter built on Tokio I/O
- Framing layer for length‑prefixed or custom frames
- Connection preamble with customizable validation callbacks 1
- Call
with_preamble::<T>()before registering success or failure callbacks - Serialization engine using
bincodeor awire-rswrapper - Routing engine that dispatches messages by ID
- Handler invocation with extractor support
- Middleware chain for request/response processing
- Connection lifecycle hooks for per-connection setup and teardown
These layers correspond to the architecture outlined in the design document2.
Applications are configured using a builder pattern similar to Actix Web. A
WireframeApp defines routes and middleware, while WireframeServer manages
connections and runs the Tokio event loop:
WireframeServer::new(|| {
WireframeApp::new()
.app_data(state.clone())
.route(MessageType::Login, handle_login)
.wrap(MyLoggingMiddleware::default())
})
.bind("127.0.0.1:7878")?
.run()
.awaitBy default, the number of worker tasks equals the number of CPU cores. If the CPU count cannot be determined, the server falls back to a single worker.
The builder supports methods like route, app_data, and wrap for
middleware configuration. app_data stores any Send + Sync value keyed by
type; registering another value of the same type overwrites the previous one.
Handlers retrieve these values using the SharedState<T> extractor3.
Handlers are asynchronous functions whose parameters implement extractor traits
and may return responses implementing the Responder trait. This pattern
mirrors Actix Web handlers and keeps protocol logic concise4.
The design document includes a simple echo server that demonstrates routing based on a message ID and the use of a length‑delimited codec:
async fn handle_echo(req: Message<EchoRequest>) -> WireframeResult<EchoResponse> {
Ok(EchoResponse {
original_payload: req.payload.clone(),
echoed_at: time_now(),
})
}
WireframeServer::new(|| {
WireframeApp::new()
.serializer(BincodeSerializer)
.route(MyMessageType::Echo, handle_echo)
})
.bind("127.0.0.1:8000")?
.run()
.awaitThis example showcases how derive macros and the framing abstraction simplify a binary protocol server. See the full example in the design document for further details.
WireframeApp defaults to a simple Envelope containing a message ID and raw
payload bytes. Applications can supply their own envelope type by calling
WireframeApp::<_, _, MyEnv>::new(). The custom type must implement the
Packet trait:
use wireframe::app::{Packet, PacketParts, WireframeApp};
#[derive(bincode::Encode, bincode::BorrowDecode)]
struct MyEnv { id: u32, correlation_id: Option<u64>, payload: Vec<u8> }
impl Packet for MyEnv {
fn id(&self) -> u32 { self.id }
fn correlation_id(&self) -> Option<u64> { self.correlation_id }
fn into_parts(self) -> PacketParts {
PacketParts::new(self.id, self.correlation_id, self.payload)
}
fn from_parts(parts: PacketParts) -> Self {
let id = parts.id();
let correlation_id = parts.correlation_id();
let payload = parts.payload();
Self { id, correlation_id, payload }
}
}
let app = WireframeApp::<_, _, MyEnv>::new()
.unwrap()
.route(1, std::sync::Arc::new(|env: &MyEnv| Box::pin(async move { /* ... */ })))
.unwrap();A None correlation ID denotes an unsolicited event or server-initiated push.
Use None rather than Some(0) when a frame lacks a correlation ID. See
PacketParts for field details.
This allows integration with existing packet formats without modifying
handle_frame.
Handlers can return types implementing the Responder trait. These values are
encoded using the application's configured serializer and framed by a
length‑delimited codec5.
Frames are length prefixed using tokio_util::codec::LengthDelimitedCodec. The
prefix length and byte order are configurable and default to a 4‑byte
big‑endian header6.
let app = WireframeApp::new()?;Push queues buffer frames before they are written to a connection. Configure them with capacities, rate limits, and an optional dead-letter queue:
use tokio::sync::mpsc;
use wireframe::push::PushQueues;
# async fn demo() {
let (dlq_tx, _dlq_rx) = mpsc::channel(8);
let (_queues, _handle) = PushQueues::<u8>::builder()
.high_capacity(8)
.low_capacity(8)
.rate(Some(100))
.dlq(Some(dlq_tx)) // frames drop if the DLQ is absent or full
.build()
.expect("failed to build PushQueues");
# drop((_queues, _handle));
# }Disable throttling with the unlimited convenience:
use wireframe::push::PushQueues;
let (_queues, _handle) = PushQueues::<u8>::builder()
.high_capacity(8)
.low_capacity(8)
.unlimited()
.build()
.expect("failed to build PushQueues");Protocol callbacks are consolidated under the WireframeProtocol trait,
replacing the individual on_connection_setup/on_connection_teardown
closures. The trait methods are synchronous so the trait remains object safe,
but callbacks can spawn asynchronous tasks when needed. A protocol
implementation registers hooks for connection setup, frame mutation and command
completion. The associated ProtocolError type is used by other parts of the
API, such as request handling.
pub trait WireframeProtocol: Send + Sync + 'static {
type Frame: FrameLike;
type ProtocolError;
fn on_connection_setup(
&self,
handle: PushHandle<Self::Frame>,
ctx: &mut ConnectionContext,
);
fn before_send(&self, frame: &mut Self::Frame, ctx: &mut ConnectionContext);
fn on_command_end(&self, ctx: &mut ConnectionContext);
}
struct MySqlProtocolImpl;
impl WireframeProtocol for MySqlProtocolImpl {
type Frame = Vec<u8>;
type ProtocolError = ();
fn on_connection_setup(
&self,
handle: PushHandle<Self::Frame>,
_ctx: &mut ConnectionContext,
) {
// Spawn an async task to send a heartbeat after setup
tokio::spawn(async move {
let _ = handle.push_high_priority(b"ping".to_vec()).await;
});
}
fn before_send(&self, _frame: &mut Self::Frame, _ctx: &mut ConnectionContext) {}
fn on_command_end(&self, _ctx: &mut ConnectionContext) {}
}let app = WireframeApp::new().with_protocol(MySqlProtocolImpl);The [SessionRegistry] stores weak references to [PushHandle]s for
active connections. Background tasks can look up a handle by [ConnectionId]
to send frames asynchronously without keeping the connection alive. Entries are
pruned on lookup and when calling active_handles(). DashMap::retain holds
per-bucket write locks while collecting, so heavy traffic may experience
contention. Invoke prune() from a maintenance task when only removal of dead
entries is required, without collecting handles.
use wireframe::{
session::{ConnectionId, SessionRegistry},
push::PushHandle,
ConnectionContext,
};
let registry: SessionRegistry<MyFrame> = SessionRegistry::default();
// inside a `WireframeProtocol` implementation
fn on_connection_setup(&self, handle: PushHandle<MyFrame>, _ctx: &mut ConnectionContext) {
let id = ConnectionId::new(42);
registry.insert(id, &handle);
}Extractors are types that implement FromMessageRequest. When a handler lists
an extractor as a parameter, wireframe automatically constructs it using the
incoming [MessageRequest] and remaining [Payload]. Built‑in extractors
like Message<T>, SharedState<T> and ConnectionInfo decode the payload,
access app state or expose peer information.
Custom extractors let you centralize parsing and validation logic that would otherwise be duplicated across handlers. A session token parser, for example, can verify the token before any route-specific code executes Design Guide: Data Extraction and Type Safety7.
use wireframe::extractor::{ConnectionInfo, FromMessageRequest, MessageRequest, Payload};
pub struct SessionToken(String);
impl FromMessageRequest for SessionToken {
type Error = std::convert::Infallible;
fn from_message_request(
_req: &MessageRequest,
payload: &mut Payload<'_>,
) -> Result<Self, Self::Error> {
let len = payload.as_ref()[0] as usize;
let token = std::str::from_utf8(&payload.as_ref()[1..=len]).unwrap().to_string();
payload.advance(1 + len);
Ok(Self(token))
}
}Custom extractors integrate seamlessly with other parameters:
async fn handle_ping(token: SessionToken, info: ConnectionInfo) {
println!("{} from {:?}", token.0, info.peer_addr());
}Middleware allows inspecting or modifying requests and responses. The from_fn
helper builds middleware from an async function or closure:
use wireframe::middleware::from_fn;
let logging = from_fn(|req, next| async move {
tracing::info!("received request: {:?}", req);
let res = next.call(req).await?;
tracing::info!("sending response: {:?}", res);
Ok(res)
});Example programs are available in the examples/ directory:
echo.rs— minimal echo server using routingping_pong.rs— showcases serialization and middleware in a ping/pong protocol. See examples/ping_pong.md for a detailed overview.packet_enum.rs— shows packet type discrimination with a bincode enum and a frame containing container types likeHashMapandVec.
Run an example with Cargo:
cargo run --example echoTry the echo server with netcat:
$ cargo run --example echo
# in another terminal
$ printf '\x00\x00\x00\x00\x01\x00\x00\x00' | nc 127.0.0.1 7878 | xxdTry the ping‑pong server with netcat:
$ cargo run --example ping_pong
# in another terminal
$ printf '\x00\x00\x00\x08\x01\x00\x00\x00\x2a\x00\x00\x00' | nc 127.0.0.1 7878 | xxdConnection handling now processes frames and routes messages. Although the server is still experimental, it now compiles in release mode for evaluation or production use.
Development priorities are tracked in docs/roadmap.md. Key tasks include building the Actix‑inspired API, implementing middleware and extractor traits, and providing example applications8.
Wireframe is distributed under the terms of the ISC licence. See LICENSE for details.
Footnotes
-
<docs/preamble-validator.md> ↩
-
<docs/rust-binary-router-library-design.md#L292-L344> ↩
-
<docs/rust-binary-router-library-design.md#L622-L710> ↩
-
<docs/rust-binary-router-library-design.md#L682-L710> ↩
-
<docs/rust-binary-router-library-design.md#L724-L730> ↩
-
<docs/rust-binary-router-library-design.md#L1082-L1123> ↩
-
<docs/rust-binary-router-library-design.md#53-data-extraction-and-type-safety> ↩
-
<docs/roadmap.md#L1-L24> ↩