Application Interface
QuicD’s application interface (quicd-x) provides a clean, event-driven API for building any QUIC-based protocol. This guide covers everything you need to implement custom applications on QuicD.
Overview
Section titled “Overview”Building an application on QuicD involves:
- Implement
QuicAppFactorytrait - Handle
AppEventstream (connection lifecycle, streams, data) - Use
ConnectionHandleto send data and commands - Register your factory with QuicD
The interface is designed for:
- Zero-copy data transfer via
bytes::Bytes - Non-blocking operations via bounded channels
- Backpressure handling built-in
- Type safety enforced by Rust
QuicAppFactory Trait
Section titled “QuicAppFactory Trait”The core trait for pluggable applications:
use async_trait::async_trait;use quicd_x::{ QuicAppFactory, ConnectionHandle, AppEventStream, TransportControls, ShutdownFuture, ConnectionError};
#[async_trait]pub trait QuicAppFactory: Send + Sync + 'static { /// Returns true if this factory handles the given ALPN fn accepts_alpn(&self, alpn: &str) -> bool;
/// Spawns the application task for a connection async fn spawn_app( &self, alpn: String, handle: ConnectionHandle, events: AppEventStream, transport: TransportControls, shutdown: ShutdownFuture, ) -> Result<(), ConnectionError>;}Parameters Explained
Section titled “Parameters Explained”| Parameter | Type | Purpose |
|---|---|---|
alpn | String | Negotiated ALPN (e.g., "h3", "echo") |
handle | ConnectionHandle | API for sending data and commands |
events | AppEventStream | Stream of events from worker thread |
transport | TransportControls | Transport configuration (datagram support, etc.) |
shutdown | ShutdownFuture | Completes when graceful shutdown begins |
Minimal Example: Echo Protocol
Section titled “Minimal Example: Echo Protocol”use async_trait::async_trait;use futures::StreamExt;use quicd_x::*;
struct EchoFactory;
#[async_trait]impl QuicAppFactory for EchoFactory { fn accepts_alpn(&self, alpn: &str) -> bool { alpn == "echo" }
async fn spawn_app( &self, _alpn: String, _handle: ConnectionHandle, mut events: AppEventStream, _transport: TransportControls, mut shutdown: ShutdownFuture, ) -> Result<(), ConnectionError> { loop { tokio::select! { Some(event) = events.next() => { match event { AppEvent::NewStream { mut recv_stream, send_stream, .. } => { if let Some(send) = send_stream { // Echo: read and write back while let Ok(Some(data)) = recv_stream.read().await { match data { StreamData::Data(bytes) => { send.write(bytes, false).await?; } StreamData::Fin => break, } } send.finish().await?; } } AppEvent::ConnectionClosing { .. } => break, _ => {} } } _ = &mut shutdown => { // Graceful shutdown break; } } } Ok(()) }}Register Factory
Section titled “Register Factory”In main.rs:
let registry = AppRegistry::new() .register("echo", Arc::new(EchoFactory));That’s it! QuicD handles all QUIC protocol details.
ConnectionHandle API
Section titled “ConnectionHandle API”ConnectionHandle provides the API for applications to interact with connections.
Connection Info
Section titled “Connection Info”let conn_id = handle.connection_id(); // u128let peer = handle.peer_addr(); // SocketAddrlet local = handle.local_addr(); // SocketAddrOpening Streams
Section titled “Opening Streams”// Open bidirectional stream (returns request_id)let request_id = handle.open_bi()?;
// Open unidirectional streamlet request_id = handle.open_uni()?;
// Response arrives as AppEventmatch event { AppEvent::StreamOpened { request_id, result } => { let (send, recv) = result?; // Use streams... } _ => {}}Sending Datagrams
Section titled “Sending Datagrams”use bytes::Bytes;
let data = Bytes::from("Unreliable message");let request_id = handle.send_datagram(data)?;
// Responsematch event { AppEvent::DatagramSent { request_id, result } => { let bytes_sent = result?; } _ => {}}Resetting Streams
Section titled “Resetting Streams”// Abort stream with error codelet request_id = handle.reset_stream(stream_id, error_code)?;
// Responsematch event { AppEvent::StreamReset { request_id, result } => { result?; // Check for errors } _ => {}}Closing Connection
Section titled “Closing Connection”// Graceful close with error code and optional reasonhandle.close(0, Some(Bytes::from("Normal close")))?;Requesting Statistics
Section titled “Requesting Statistics”let request_id = handle.stats()?;
// Responsematch event { AppEvent::StatsReceived { request_id, result } => { let stats = result?; println!("RTT: {:?}ms", stats.rtt_estimate_ms); println!("Bytes sent: {}", stats.bytes_sent); } _ => {}}AppEvent Stream
Section titled “AppEvent Stream”Events flow from worker thread to application task. All events are delivered in order.
Event Types
Section titled “Event Types”HandshakeCompleted
Section titled “HandshakeCompleted”AppEvent::HandshakeCompleted { alpn: String, local_addr: SocketAddr, peer_addr: SocketAddr, negotiated_at: Instant,}When: QUIC handshake succeeds, TLS negotiation complete.
Action: Initialize protocol state, prepare to receive streams.
NewStream
Section titled “NewStream”AppEvent::NewStream { stream_id: StreamId, bidirectional: bool, recv_stream: RecvStream, send_stream: Option<SendStream>, // Some if bidirectional}When: Peer opens a new stream.
Action: Spawn task or handler for this stream.
Example:
match event { AppEvent::NewStream { stream_id, recv_stream, send_stream, .. } => { tokio::spawn(async move { handle_stream(stream_id, recv_stream, send_stream).await; }); } _ => {}}StreamReadable
Section titled “StreamReadable”AppEvent::StreamReadable { stream_id: StreamId,}When: Buffered data is available on stream (edge-triggered).
Action: Call recv_stream.read() to consume data.
Purpose: Efficient polling without busy-waiting.
Datagram
Section titled “Datagram”AppEvent::Datagram { payload: Bytes,}When: Unreliable datagram received from peer.
Action: Process payload immediately (no delivery guarantee).
ConnectionClosing
Section titled “ConnectionClosing”AppEvent::ConnectionClosing { error_code: u64, reason: Option<Bytes>,}When: Connection is terminating (gracefully or due to error).
Action: Clean up resources, flush pending writes, exit task.
Timeout: 30 seconds after this event before forceful termination.
SendStream & RecvStream
Section titled “SendStream & RecvStream”RecvStream: Reading Data
Section titled “RecvStream: Reading Data”pub struct RecvStream { pub stream_id: StreamId, // ...}
impl RecvStream { /// Read next chunk from stream pub async fn read(&mut self) -> Result<Option<StreamData>, ConnectionError>;}
pub enum StreamData { Data(bytes::Bytes), // Data chunk Fin, // End of stream}Example:
let mut buffer = Vec::new();
loop { match recv_stream.read().await? { Some(StreamData::Data(chunk)) => { buffer.extend_from_slice(&chunk); } Some(StreamData::Fin) => break, None => break, // Channel closed }}
// Process complete bufferprocess_data(buffer);SendStream: Writing Data
Section titled “SendStream: Writing Data”pub struct SendStream { pub stream_id: StreamId, // ...}
impl SendStream { /// Write data chunk with optional FIN pub async fn write(&self, data: Bytes, fin: bool) -> Result<usize, ConnectionError>;
/// Send FIN without data pub async fn finish(&self) -> Result<(), ConnectionError>;
/// Fluent builder for ergonomic writes pub fn send_data(&self, data: Bytes) -> SendDataBuilder;}Basic write:
let chunk = Bytes::from("Hello, QUIC!");send_stream.write(chunk, false).await?;
// Send FINsend_stream.finish().await?;Fluent API:
// Send data with FIN in one callsend_stream.send_data(response).with_fin(true).send().await?;Zero-Copy Semantics
Section titled “Zero-Copy Semantics”Both RecvStream and SendStream use bytes::Bytes:
// Cloning is O(1) - reference counting, no data copylet data = Bytes::from("Large payload...");let clone = data.clone(); // No memory copy
// Both point to same underlying bufferassert_eq!(data.as_ptr(), clone.as_ptr());Best practice: Avoid converting to Vec<u8> unless mutation is needed.
Lifecycle Management
Section titled “Lifecycle Management”Initialization
Section titled “Initialization”sequenceDiagram
participant C as Client
participant W as Worker Thread
participant A as App Task
C->>W: Initial Packet (QUIC handshake)
W->>W: Negotiate ALPN
W->>A: Spawn via QuicAppFactory
W->>A: AppEvent::HandshakeCompleted
A->>A: Initialize protocol state
Data Transfer
Section titled “Data Transfer”sequenceDiagram
participant C as Client
participant W as Worker Thread
participant A as App Task
C->>W: STREAM frame
W->>A: AppEvent::NewStream
A->>A: Process stream
A->>W: SendStream::write()
W->>C: STREAM frame (response)
Shutdown
Section titled “Shutdown”sequenceDiagram
participant W as Worker Thread
participant A as App Task
W->>A: AppEvent::ConnectionClosing
A->>A: Flush pending writes
A->>A: Clean up resources
A->>A: Exit task (within 30s)
Error Handling
Section titled “Error Handling”ConnectionError Types
Section titled “ConnectionError Types”pub enum ConnectionError { /// Connection closed by peer or locally Closed(String),
/// QUIC protocol error QuicError(u64),
/// TLS handshake failure TlsFail(String),
/// Application protocol error ProtocolError(String),}Handling Errors
Section titled “Handling Errors”match recv_stream.read().await { Ok(Some(StreamData::Data(chunk))) => { // Process data } Ok(Some(StreamData::Fin)) | Ok(None) => { // Stream ended } Err(ConnectionError::Closed(reason)) => { eprintln!("Connection closed: {}", reason); break; } Err(e) => { eprintln!("Error: {:?}", e); return Err(e); }}Propagating Errors
Section titled “Propagating Errors”async fn spawn_app( // ...) -> Result<(), ConnectionError> { // Errors bubble up to QuicD process_connection().await?; Ok(())}QuicD behavior: If app task returns Err, connection is closed with error code.
Advanced Patterns
Section titled “Advanced Patterns”Spawning Per-Stream Tasks
Section titled “Spawning Per-Stream Tasks”For protocols with many concurrent streams (HTTP/3):
match event { AppEvent::NewStream { stream_id, recv_stream, send_stream, .. } => { tokio::spawn(async move { if let Err(e) = handle_stream(stream_id, recv_stream, send_stream).await { eprintln!("Stream {} error: {:?}", stream_id, e); } }); } _ => {}}
async fn handle_stream( stream_id: StreamId, mut recv: RecvStream, send: Option<SendStream>,) -> Result<(), Box<dyn std::error::Error>> { // Process stream independently Ok(())}Request/Response Correlation
Section titled “Request/Response Correlation”Track pending requests by ID:
use std::collections::HashMap;
struct AppState { pending_requests: HashMap<u64, oneshot::Sender<Result>>,}
// Send requestlet request_id = handle.open_bi()?;let (tx, rx) = oneshot::channel();state.pending_requests.insert(request_id, tx);
// Handle responsematch event { AppEvent::StreamOpened { request_id, result } => { if let Some(tx) = state.pending_requests.remove(&request_id) { let _ = tx.send(result); } } _ => {}}
// Wait for responselet (send, recv) = rx.await??;Backpressure Handling
Section titled “Backpressure Handling”Monitor StreamReadable for efficient I/O:
let mut has_data = false;
loop { tokio::select! { Some(event) = events.next() => { match event { AppEvent::StreamReadable { stream_id } => { has_data = true; } _ => {} } } _ = tokio::time::sleep(Duration::from_millis(10)), if has_data => { // Read available data while let Ok(Some(data)) = recv_stream.read().await { process(data); } has_data = false; } }}Testing Applications
Section titled “Testing Applications”Unit Testing
Section titled “Unit Testing”#[cfg(test)]mod tests { use super::*;
#[tokio::test] async fn test_echo_factory() { let factory = EchoFactory; assert!(factory.accepts_alpn("echo")); assert!(!factory.accepts_alpn("h3")); }}Integration Testing
Section titled “Integration Testing”Create mock ConnectionHandle and AppEventStream:
use tokio::sync::mpsc;
#[tokio::test]async fn test_app_logic() { let (event_tx, event_rx) = mpsc::channel(10); let events = Box::pin(tokio_stream::wrappers::ReceiverStream::new(event_rx));
// Send mock events event_tx.send(AppEvent::HandshakeCompleted { .. }).await.unwrap();
// Run app // ...}Best Practices
Section titled “Best Practices”- Handle shutdown gracefully: Always monitor
shutdownfuture - Use fluent API:
send_data().with_fin(true).send()is cleaner - Avoid blocking: All I/O is async, don’t use blocking operations
- Monitor backpressure: Check channel capacities if performance issues
- Log errors: Don’t silently ignore errors
- Test edge cases: Connection closure, malformed data
Next Steps
Section titled “Next Steps”- HTTP/3 Example: See a complete implementation
- Core Concepts: Understand event-driven design
- API Reference: Detailed API documentation
The QuicD application interface is designed for safety, performance, and ergonomics. Build any QUIC-based protocol with confidence.