oneshot
A lock-free Rust spsc channel built to send exactly one message, seamlessly, between sync threads and async tasks.
Repository Health
Technical Analysis
oneshot is a Rust crate providing a single-producer, single-consumer channel purpose-built for one-time message handoff. Unlike a general-purpose mpsc channel, it bakes the one-message guarantee into the type system: Sender::send consumes self, so it is a compile-time error, not a runtime panic, to try sending twice. The Receiver supports both thread-blocking recv/recv_timeout and .await-based asynchronous receiving via IntoFuture, using the same underlying channel object for either mode.
Internally, each channel is a single heap allocation holding an atomic state byte, uninitialized storage for the message, and uninitialized storage for a waker, coordinated through a hand-written lock-free state machine rather than a mutex. The std and async features are both opt-in and off by default, so a minimal no_std build only supports non-blocking try_recv. The crate’s correctness is checked in CI under both Miri and the loom exhaustive-interleaving model, in addition to the normal test suite across the full feature powerset.
What You Get
- A
channel()function returning aSender<T>/Receiver<T>pair backed by one heap allocation - Lock-free, largely wait-free
Sender::send, withis_closed()to check for a dropped receiver - Non-blocking
try_recv, thread-blockingrecv/recv_timeout(via thestdfeature), and.await-based async receiving (via theasyncfeature) - Typed errors (
SendError,RecvError,TryRecvError,RecvTimeoutError) instead of panics no_stdcompatibility when no optional features are enabledSender::into_raw/from_rawfor passing a channel handle across an FFI boundary
Common Use Cases
- Replying to a request routed through an
mpscworker queue, regardless of whether the caller is sync or async - Awaiting a reply from another task or thread inside an async function without depending on a specific executor
- Handing a result computed on a dedicated blocking thread back to an async caller
- Bounding how long a caller waits for a response with
recv_timeoutbefore treating the call as failed - Completing a Rust future from a C callback using
into_raw/from_rawacross an FFI boundary
Under The Hood
Architecture
The crate is a single flat module tree (lib.rs, channel.rs, sender.rs, receiver.rs, states.rs, waker.rs, errors.rs) built around one shared heap allocation defined in channel.rs: a Channel<T> holding an AtomicU8 state, an UnsafeCell<MaybeUninit<T>> message slot, and an UnsafeCell<MaybeUninit<ReceiverWaker>> waker slot. Sender<T> and Receiver<T> are thin NonNull<Channel<T>> wrappers whose methods drive a hand-rolled state machine (states.rs: EMPTY/MESSAGE/RECEIVING/UNPARKING/DISCONNECTED) through compare_exchange/fetch_add/fetch_xor with carefully chosen memory orderings, so coordination is entirely lock-free atomics plus a brief busy-wait window during the UNPARKING transition. Ownership is enforced by consuming self on send/recv, and whichever endpoint is dropped last frees the heap allocation; waker.rs abstracts over std::thread::Thread and task::Waker so the same Channel<T> serves both blocking and async call paths without duplicating the state machine.
Tech Stack
Pure Rust, no_std-capable, with zero mandatory runtime dependencies — Cargo.toml declares no [dependencies], only an optional loom crate gated behind internal correctness testing and dev-dependencies (tokio, async-std, criterion) for tests and benchmarks. The std and async features are both off by default. CI uses cargo-hack to build and test the full feature powerset across Linux/macOS/Windows and stable/beta/nightly/MSRV 1.85.0, plus dedicated loom and Miri jobs and separate linting/formatting/commit-style workflows. The crate targets Rust 2024 edition with an enforced MSRV.
Code Quality
Testing is unusually rigorous for a small crate: dedicated suites cover sync receiving, async receiving/futures, raw-pointer FFI usage, memory-leak assertions, Miri, and a loom-based exhaustive concurrency model, backed by ten runnable examples covering send/receive ordering permutations. Errors are fully typed rather than panicking or swallowed, and every unsafe block carries an explicit SAFETY: comment justifying its invariant, with Clippy lints tightened in Cargo.toml (undocumented_unsafe_blocks = "warn", wildcard_dependencies = "deny") and CI running with RUSTFLAGS="--deny warnings".
API Design
The public surface is deliberately minimal — one channel() constructor plus Sender<T>/Receiver<T> — and leans on the type system for correctness: send takes self by value so a second send is a compile error, and Receiver implements IntoFuture so .await works with no boilerplate while recv/recv_timeout/try_recv cover the synchronous cases. Naming mirrors std::sync::mpsc conventions, and every public item’s doc comments include usage examples and explicit ordering guarantees. The main friction point is that std and async are both off by default, so a first-time user must discover and enable a feature flag before recv() or .await will compile — a deliberate minimalism tradeoff the README calls out directly.