shmring (Rust)
Rust implementation of shmring: a
fixed-capacity, single-producer/single-consumer byte ring buffer, using the
same wire format as the Go and JS implementations. A Rust process can
exchange bytes with a Go process (or a browser tab, via the JS/wasm build)
over the same named shared-memory segment -- no sockets, pipes, or copies
through the kernel beyond the initial mmap.
Platform support
OS shared memory (create_shm/open_shm) is implemented for Unix (Linux,
macOS) via POSIX shm_open. Windows isn't implemented yet.
Writer::new/Reader::new over a custom [backend::Storage] work on every
platform regardless -- only the OS-shared-memory convenience functions are
Unix-only for now.
Install
Quick start
use ;
use ;
// process A (producer)
let mut w = create_shm?; // capacity must be a power of two
w.write_all?;
w.close?; // signal EOF to the reader once done
// process B (consumer)
let mut r = open_shm?;
let mut out = Vecnew;
r.read_to_end?; // reads until the writer closes and the buffer drains
// once both sides are done, the creating side releases the OS segment:
w.close_storage?;
See examples/producer.rs and
examples/consumer.rs for a runnable two-process
demo:
&
API
create_shm(name: &str, capacity: u64, options: Options) -> Result<Writer<ShmStorage>>creates a new shared-memory ring buffer.open_shm(name: &str, capacity: u64, options: Options) -> Result<Reader<ShmStorage>>opens one created bycreate_shm.Writer<S>implements [std::io::Write] (blocking, combine with the trait's defaultwrite_allfor the equivalent of Go's blockingWrite), plus non-blockingtry_writeand a deadline-boundwrite_timeout.Reader<S>implements [std::io::Read] (blocking; returnsOk(0)at end-of-stream, once the writer has closed and all buffered data has been drained), plus non-blockingtry_read(returnsErr(Error::Eof)at the same point) and a deadline-boundread_timeout.Writer::closemarks the ring buffer closed (readable data already written is still drained normally);Writer::close_storageadditionally releases the OS shared-memory segment and should be called once, by whichever side created it, after the other side is done.backend::Storageis the pluggable storage trait;backend::MemStorage(an in-process,Clone-able byte buffer) is what this crate's own tests run against, and is a usefulWriter::new/Reader::newbackend anywhere OS shared memory isn't available or applicable.
Design
Pluggable storage. The ring buffer algorithm never talks to OS shared
memory directly -- it depends only on the backend::Storage trait
(read_at/write_at/size/close). create_shm/open_shm use
backend::ShmStorage, backed directly by POSIX shm_open/mmap.
Writer::new/Reader::new accept any backend::Storage, including
backend::MemStorage. This is the extension point for a future Windows
backend, or any other transport: add a new backend::Storage impl, not
touch the ring buffer logic.
Resource cleanup is RAII, not manual. Go's backend.ShmStorage
requires callers to remember to call Close, and Go's own CreateShm/
OpenShm explicitly clean up a partially constructed storage on error path
by hand. Rust's ownership rules make that unnecessary: ShmStorage unmaps
(and, for the creating side, shm_unlinks) itself in Drop, so a Writer/
Reader that fails to construct -- or is simply dropped without an
explicit close() -- can't leak the mapping. Storage::close's Result
return is kept for trait conformance with the Go/JS API shape, not because
failure here is actionable.
Concurrency model. A ring buffer has exactly one Writer and one
Reader, each used from a single thread at a time -- this is a
single-producer/single-consumer (SPSC) structure, not a general-purpose
concurrent queue. Head/tail/closed are 32-bit counters, matching the header
format shared with the Go and JS implementations. Coordination goes through
plain, 4-byte aligned loads and stores, which is safe over real OS shared
memory (hardware-coherent across processes) and over MemStorage (which
serializes access with a mutex instead of relying on coherency, since two
threads in one process need an explicit happens-before edge that a plain
byte buffer alone doesn't give them).
Blocking calls poll. There's no cross-process wakeup primitive
available through shared memory alone, so the blocking Read/Write
implementations (and read_timeout/write_timeout) block by polling the
shared counters with an exponential backoff (tunable via Options). Use
try_write/try_read if busy-polling isn't acceptable for your use case.
Development
Or via the repo's Mage targets from the repo root:
mage -l lists them once added (see the main README).
License
MIT, see LICENSE.