shmring 0.3.0

Fixed-capacity, single-producer/single-consumer byte ring buffer over shared memory (same wire format as the Go and JS implementations)
Documentation
# shmring (Rust)

Rust implementation of [shmring](https://github.com/gofsd/shmring): a
fixed-capacity, single-producer/single-consumer byte ring buffer, using the
same wire format as the Go implementation. A Rust process can exchange
bytes with a Go process over the same named shared-memory segment -- no
sockets, pipes, or copies through the kernel beyond the initial `mmap`.
This crate also compiles to `wasm32-unknown-unknown` for the browser (via
`wasm-bindgen`), published as [`@gofsd/shmring`](https://www.npmjs.com/package/@gofsd/shmring)
on npm -- see [Web](#web) below.

## 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. On `wasm32-unknown-unknown`, `backend::SharedArrayBufferStorage`
provides a browser-`SharedArrayBuffer`-backed `Storage` instead (see
[Web](#web)).

## Install

```sh
cargo add shmring
```

## Quick start

```rust
use std::io::{Read, Write};
use shmring::{create_shm, open_shm, Options};

// process A (producer)
let mut w = create_shm("my-channel", 4096, Options::default())?; // capacity must be a power of two
w.write_all(b"hello\n")?;
w.close()?; // signal EOF to the reader once done

// process B (consumer)
let mut r = open_shm("my-channel", 4096, Options::default())?;
let mut out = Vec::new();
r.read_to_end(&mut out)?; // 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`](examples/producer.rs) and
[`examples/consumer.rs`](examples/consumer.rs) for a runnable two-process
demo:

```sh
cargo run --example producer &
cargo run --example consumer
```

## Web

`src/backend/wasm.rs` implements `Storage` over a JavaScript
`SharedArrayBuffer` (`backend::SharedArrayBufferStorage`), and
`src/wasm_api.rs` exposes it to JavaScript via `wasm-bindgen`
(`WasmWriter`/`WasmReader`, `createWriter`/`openReader`), both compiled
only for `wasm32-unknown-unknown` -- neither affects the native crate or
its crates.io publish at all. Each browser thread (main thread, or a Web
Worker) that wants to be one side of a ring buffer loads its own
independent wasm module instance; coordination between them goes through
real `Atomics.load`/`Atomics.store` on the shared `SharedArrayBuffer`, not
through any Rust-level threading -- this is why plain stable
`wasm32-unknown-unknown` is enough here, no nightly or `-Z build-std`
needed.

You don't have to go through the JS bindings, either: a Rust web app
(compiled to wasm itself, e.g. via Leptos or Yew) can use
`backend::SharedArrayBufferStorage` with `Writer::new`/`Reader::new`
directly, exactly like the native backend.

Build and test the JS-facing package with:

```sh
rustup target add wasm32-unknown-unknown   # once
cargo install wasm-pack                     # once
wasm-pack build rust --target web --out-name shmring_wasm
```

Or via the repo's `mage web:build`/`mage web:test` targets, which drive
this plus [`../web/`](../web)'s example page and a real headless-Chrome
end-to-end check -- see the main [README](../README.md#web).

## 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 by `create_shm`.
- `Writer<S>` implements [`std::io::Write`] blocking, combine with the
  trait's default `write_all` for the equivalent of Go's blocking `Write`,
  plus non-blocking `try_write` and a deadline-bound `write_timeout`.
- `Reader<S>` implements [`std::io::Read`] blocking; returns `Ok(0` at
  end-of-stream, once the writer has closed and all buffered data has been
  drained), plus non-blocking `try_read` (returns `Err(Error::Eof)` at the
  same point) and a deadline-bound `read_timeout`.
- `Writer::close` marks the ring buffer closed (readable data already
  written is still drained normally); `Writer::close_storage` additionally
  releases the OS shared-memory segment and should be called once, by
  whichever side created it, after the other side is done.
- `backend::Storage` is 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 useful `Writer::new`/`Reader::new` backend 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`, plus `load_u32_at`/`store_u32_at` for
the head/tail/closed counters, defaulted to a plain `read_at`/`write_at`
pair so most backends need not override them). `create_shm`/`open_shm` use
`backend::ShmStorage`, backed directly by POSIX `shm_open`/`mmap`; the web
build's `createWriter`/`openReader` use `backend::SharedArrayBufferStorage`
(see [Web](#web)), which overrides `load_u32_at`/`store_u32_at` with real
`Atomics.load`/`store` -- the compile-time equivalent of Go's runtime
`backend.AtomicStorage` capability check. `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 --
exactly how the web backend was added.

**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_unlink`s) 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 implementation and, on the web, JavaScript's
`Int32Array`. Coordination goes through plain, 4-byte aligned loads and
stores by default, 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) -- but not over a browser
`SharedArrayBuffer` shared between the main thread and a Worker, where a
plain access is a data race under the JavaScript memory model. That's
exactly why `SharedArrayBufferStorage` doesn't take this shortcut and uses
real `Atomics` instead (see [Web](#web)).

**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

```sh
cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt
```

Or via the repo's [Mage](https://magefile.org) targets from the repo root:
`mage -l` lists them once added (see the main [README](../README.md)).

## License

MIT, see [LICENSE](../LICENSE).