# Wireframe
**Wireframe** is an experimental Rust library that simplifies building servers
and clients for custom binary protocols. The design borrows heavily from
[Actix Web](https://actix.rs/) to provide a familiar, declarative API for
routing, extractors, and middleware.
## Motivation
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
- Call `with_preamble::<T>()` before registering success or failure callbacks
- **Serialization engine** using `bincode` or a `wire-rs` wrapper
- **Routing engine** that dispatches messages by ID
- **Handler invocation** with extractor support
- **Middleware chain** for request/response processing
- **[Connection lifecycle hooks](#connection-lifecycle)** for per-connection
setup and teardown
These layers correspond to the architecture outlined in the design document[^2].
## API overview
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:
```rust
.app_data(state.clone())
.route(MessageType::Login, handle_login)
.wrap(MyLoggingMiddleware::default())
})
.bind("127.0.0.1:7878")?
.run()
.await
```
By 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>` extractor[^3].
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 concise[^4].
## Example
The design document includes a simple echo server that demonstrates routing
based on a message ID and the use of a length‑delimited codec:
```rust
async fn handle_echo(req: Message<EchoRequest>) -> WireframeResult<EchoResponse> {
Ok(EchoResponse {
original_payload: req.payload.clone(),
echoed_at: time_now(),
})
}
.serializer(BincodeSerializer)
.route(MyMessageType::Echo, handle_echo)
})
.bind("127.0.0.1:8000")?
.run()
.await
```
This example showcases how derive macros and the framing abstraction simplify a
binary protocol server. See the <!-- markdownlint-disable-next-line MD013 -->
[full example](docs/rust-binary-router-library-design.md#5-6-illustrative-api-usage-examples)
in the design document for further details.
## Custom envelopes
`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:
```rust
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](docs/api.md#packetparts) for field details.
This allows integration with existing packet formats without modifying
`handle_frame`.
## Response serialization and framing
Handlers can return types implementing the `Responder` trait. These values are
encoded using the application's configured serializer and framed by a
length‑delimited codec[^5].
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 header[^6].
```rust
let app = WireframeApp::new()?;
```
## Push Queues
Push queues buffer frames before they are written to a connection. Configure
them with capacities, rate limits, and an optional dead-letter queue:
```rust,no_run
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:
```rust,no_run
use wireframe::push::PushQueues;
let (_queues, _handle) = PushQueues::<u8>::builder()
.high_capacity(8)
.low_capacity(8)
.unlimited()
.build()
.expect("failed to build PushQueues");
```
## Connection Lifecycle
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.
```rust
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) {}
}
```
```rust
let app = WireframeApp::new().with_protocol(MySqlProtocolImpl);
```
## Session registry
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.
```rust
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);
}
```
## Custom extractors
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 Safety[^7].
```rust
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:
```rust
async fn handle_ping(token: SessionToken, info: ConnectionInfo) {
println!("{} from {:?}", token.0, info.peer_addr());
}
```
## Middleware
Middleware allows inspecting or modifying requests and responses. The `from_fn`
helper builds middleware from an async function or closure:
```rust
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)
});
```
## Examples
Example programs are available in the `examples/` directory:
- `echo.rs` — minimal echo server using routing
- `ping_pong.rs` — showcases serialization and middleware in a ping/pong
protocol. See [examples/ping_pong.md](examples/ping_pong.md) for a detailed
overview.
- [`packet_enum.rs`](examples/packet_enum.rs) — shows packet type discrimination
with a bincode enum and a frame containing container types like `HashMap` and
`Vec`.
Run an example with Cargo:
```bash
cargo run --example echo
```
Try the echo server with netcat:
```bash
$ cargo run --example echo
# in another terminal
$ printf '\x00\x00\x00\x00\x01\x00\x00\x00' | nc 127.0.0.1 7878 | xxd
```
Try the ping‑pong server with netcat:
```bash
$ 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 | xxd
```
## Current limitations
Connection handling now processes frames and routes messages. Although the
server is still experimental, it now compiles in release mode for evaluation or
production use.
## Roadmap
Development priorities are tracked in [docs/roadmap.md](docs/roadmap.md). Key
tasks include building the Actix‑inspired API, implementing middleware and
extractor traits, and providing example applications[^8].
## Licence
Wireframe is distributed under the terms of the ISC licence. See
[LICENSE](LICENSE) for details.