Expand description
A persistent, fsync-durable binary stack backed by a single file.
§Overview
BStack treats a file as a flat byte buffer that grows and shrinks from
the tail. Every mutating operation — push,
extend, pop, discard, (with the set
feature) set, zero, and
repeat, (with the atomic feature)
replace, and (with both set and atomic)
process — calls a durable sync before returning,
so the data survives a process crash or an unclean system shutdown.
Read-only operations — peek,
peek_into, get, and
get_into — never modify the file and on Unix and
Windows can run concurrently with each other.
pop_into is the buffer-passing counterpart of pop,
carrying the same durability and atomicity guarantees.
discard is like pop but discards the removed bytes
without reading or returning them, avoiding any allocation or copy.
The crate depends on libc (Unix) and windows-sys (Windows) for
platform-specific syscalls, and uses no unsafe code beyond the required
FFI calls and the two lifetime-extending reborrow helpers behind
bstack_unsafe_reborrow! / bstack_unsafe_reborrow_mut! (see the
reborrow module).
§File format
Every file begins with a fixed 32-byte header, then the concatenated payload (push 0, push 1, …):
bytes field
───── ─────
0 .. 8 magic[8]
8 .. 16 clen — committed payload length (u64 LE)
16 .. 24 wip_ptr — write-in-progress journal target (u64 LE; 0 when idle)
24 .. 32 wip_aux — write-in-progress journal mode (u64 LE)
32 .. payload — push 0, push 1, … concatenatedmagic— 8 bytes:BSTK+ major(1 B) + minor(1 B) + patch(1 B) + reserved(1 B). This version writesBSTK\x00\x04\x04\x00(0.4.4).openaccepts any file whose first 6 bytes matchBSTK\x00\x04(any 0.4.x) and rejects anything with a different major or minor.clen— little-endianu64recording the committed payload length. It is updated atomically with eachpushorpopand is used for crash recovery on the nextopen.wip_ptr/wip_aux— two little-endianu64fields holding the write-in-progress journal that makes in-place mutations crash-atomic.wip_ptris the physical offset an interrupted in-place write must be replayed into (0in the steady state);wip_auxnames the journal mode (Set— verbatim replay of the staged tail;Repeat— repeat a staged pattern;Copy— replay a disjoint copy from its still-intact source, of which only the coordinate is staged;SpliceGrow/SpliceShrink— a length-changing tail replace, whose new committed length recovery derives from the file size and the recorded direction). Recovery interprets them onopen— see Crash recovery. Legacy 0.1.x files (16-byte header) are upgraded in place byBStack::migrate.
All user-visible offsets are logical (0-based from the start of the payload region, i.e. from file byte 32).
§Crash recovery
On open, recovery first checks the write-in-progress journal
(wip_ptr); if disarmed, it reconciles the committed length against the file
size:
| Condition | Cause | Recovery |
|---|---|---|
wip_ptr != 0, wip_aux = Set | an in-place set/swap/cas/copy/cross_exchange crashed mid-commit | replay the staged tail verbatim into [wip_ptr, …), disarm, truncate to 32 + clen |
wip_ptr != 0, wip_aux = Repeat | a zero/repeat crashed mid-fill | write count copies of the staged pattern into [wip_ptr, …), disarm, truncate |
wip_ptr != 0, wip_aux = Copy | a disjoint copy crashed mid-copy | replay move_chunked(src → wip_ptr) from the untouched source (the tail stages only [src | n]), disarm, truncate |
wip_ptr != 0, wip_aux = SpliceGrow/SpliceShrink | a length-changing atrunc/splice/splice_into/replace crashed mid-replace | derive clen' from the file size and direction, replay the staged new tail into [wip_ptr, …), commit clen' while disarming, truncate |
wip_ptr != 0, wip_aux unrecognized | a mode armed by a newer build | roll back: disarm, truncate to 32 + clen |
wip_ptr == 0, wip_aux = MultiWrite | a set_batched/inplace_gen multi-write batch crashed after all blocks were staged | replay each staged [s | e | data] block into [s, e), disarm, truncate to 32 + clen (a corrupt tail rolls back, applying nothing) |
wip_ptr == 0, file_size − 32 > clen | partial tail write (push, or a crashed journal or multi-write stage) before the header update | truncate to 32 + clen |
wip_ptr == 0, file_size − 32 < clen | partial truncation (pop crashed before the header update) | set clen = file_size − 32 |
Each replay is idempotent — the staged tail is immutable and disjoint from
its target — so a crash during recovery itself is safe to re-run. After
recovery a durable_sync ensures the repaired state is on stable storage
before any caller can observe or modify the file.
§Deferred replay
A BStack stays usable after a write fails, with no reopen: the next write
repairs the file before doing anything else, and reads refuse until it has.
A write that fails after its first mutating I/O can leave the states above —
an armed journal, or a stale tail past the committed length — on a handle
that stays open. The failed call returns its error unchanged and the repair
is deferred to the next write, through an in-memory replay_needed flag
under the BStack rwlock. While it is set:
- The next write replays first, silently, before validating its own arguments — a stale tail would inflate the payload size every mutator derives from the file end — then proceeds normally. The flag clears only once the replay succeeds; a failing replay returns its own error.
- Reads fail with
InterruptedWrite, until a write replays orrecoveris called to replay on its own. This covers every read that consults the file or the cached committed length —len, and a zero-lengthget(n, n)too, since it still checksnagainst the payload size. Unaffected are the calls that return before taking the lock (peek_into/get_intoon an empty buffer,get_batched/get_batched_intoon no entries) and lock-free reads of the immutable locked prefix.
§Durability
In-place same-length writes — set, zero,
repeat, swap,
swap_into, cas, copy,
cross_exchange, process,
set_batched, inplace_gen, and
the crds family — leave the payload length unchanged and are each
crash-atomic, committing by one of three strategies (recovered on the next
open; see Crash recovery):
- Aligned-block write — when the target lies within one power-fail-atomic
block, a single
write+durable_syncis already all-or-nothing; no journal is armed. - Write-in-progress journal — otherwise: stage a backup past
clen→durable_sync→ armwip_ptr→durable_sync→ write in place →durable_sync→ clearwip_ptr→durable_sync→ftruncatethe backup.zero/repeatstage only[count | pattern];cross_exchangestages one region and commits at a single atomicwip_ptrflip; moves and fills stream through a bounded buffer (O(1) memory). - Multi-write journal —
set_batchedandinplace_gencommit several non-overlapping in-place writes as one unit: stage every[s | e | data]block pastclen→durable_sync→ arm theMultiWritesentinel (wip_ptrstays0, so it never collides with a single-region journal) →durable_sync→ replay each block in place →durable_sync→ disarm →ftruncate. A batch that reduces to one write falls back to the single-write strategies above.
Below, commit denotes whichever of those two strategies applies to the bytes being written; anything before it is read/compare/callback work under the lock.
| Operation | Syscall sequence |
|---|---|
push | lseek(END) → write(data) → lseek(8) → write(clen) → durable_sync |
extend | lseek(END) → set_len(new_end) → lseek(8) → write(clen) → durable_sync |
extend_sparse, extend_sparse_batched | set_len(new_end) → write each buffer into the grown region (gaps left zero) → lseek(8) → write(clen) → durable_sync |
pop, pop_into | lseek → read → ftruncate → lseek(8) → write(clen) → durable_sync |
discard | ftruncate → lseek(8) → write(clen) → durable_sync |
set (feature) | commit data |
zero, repeat (feature) | commit the repeated pattern (the journal stages only [count | pattern]) |
atrunc (feature: atomic) | dispatch on the tail-replace shape: pure truncation → ftruncate → commit clen; pure append → set_len(new_end) → write(buf) → durable_sync → commit clen; same-length → commit buf in place; length change → splice journal (stage the new tail past the payload → arm SpliceGrow/SpliceShrink → replay into place → atomically commit clen' + disarm → truncate, a durable_sync at each barrier) |
splice, splice_into (feature: atomic) | lseek(tail) → read(n) → (then as atrunc) |
try_extend (feature: atomic) | lseek(END) — conditional push sequence if size matches |
try_discard (feature: atomic) | lseek(END) — conditional discard sequence if size matches |
try_extend_zeros (feature: atomic) | lseek(END) — conditional extend(n) sequence if size matches |
try_extend_sparse, try_extend_sparse_batched (feature: atomic) | lseek(END) — conditional extend_sparse / extend_sparse_batched sequence if size matches |
swap, swap_into (features: set+atomic) | read old bytes → commit buf |
cas (features: set+atomic) | read → compare — conditional commit of new |
process (features: set+atomic) | read(start..end) → (callback) → commit the buffer |
process_gen (features: set+atomic) | closure-driven reads, ending in at most one mutating step: Write commits; Swap uses the exchange journal (as cross_exchange); Push/Pop/Discard/Atrunc/Splice/Sparse behave as their standalone forms |
set_batched (features: set+atomic) | validate + reject overlap → multi-write journal: stage every [s | e | data] block past clen → arm the MultiWrite sentinel (wip_ptr stays 0) → replay each block in place → disarm → ftruncate (a durable_sync at each barrier); a lone effective write takes the ordinary single-write commit |
inplace_gen (features: set+atomic) | closure-driven reads (each overlaid with the batch-so-far edits) interleaved with accumulated Writes (later overrides earlier on overlap); on None the pending edits commit together via the multi-write journal (as set_batched) |
replace (feature: atomic) | lseek(tail) → read(n) → (callback) → (then as atrunc) |
cross_exchange (features: set+atomic) | read(a), read(b) → exchange journal: stage a → arm at a → write b→a → flip wip_ptr to b → write a→b → disarm → ftruncate (a durable_sync at each barrier) |
copy (features: set+atomic) | same-location → no-op; single-block dest → commit; overlapping → stream source→tail→dest (Set journal); disjoint → copy journal (stage only [src | n] → arm Copy → stream source→dest → disarm; recovery replays from the untouched source) |
eq_crds, ne_crds (features: set+atomic) | read(a) → compare — conditional commit of b_buf |
masked_eq_crds, masked_ne_crds (features: set+atomic) | read(a) → mask+compare — conditional commit of b_buf |
peek, peek_into, get, get_into, get_batched, get_batched_into, get_batched_gen | pread(2) on Unix; ReadFile+OVERLAPPED on Windows; lseek → read elsewhere (no sync — read-only) |
durable_sync on macOS issues fcntl(F_FULLFSYNC), which flushes the
drive’s hardware write cache. Plain fdatasync is not sufficient on macOS
because the kernel may acknowledge it before the drive controller has
committed the data. If F_FULLFSYNC is not supported by the device the
implementation falls back to sync_data (fdatasync).
durable_sync on other Unix calls sync_data (fdatasync), which is
sufficient on Linux and BSD.
durable_sync on Windows calls sync_data, which maps to
FlushFileBuffers. This flushes the kernel write-back cache and waits for
the drive to acknowledge, providing equivalent durability to fdatasync.
The debug-only debug-no-sync Cargo feature skips durable_sync entirely
(writes still happen, just unsynced) for faster fault-injection test
iteration. Not for production use.
§Multi-process safety
On Unix, open acquires an exclusive advisory flock
on the file (LOCK_EX | LOCK_NB). If another process already holds the
lock, open returns immediately with io::ErrorKind::WouldBlock rather
than blocking indefinitely. The lock is released automatically when the
BStack is dropped (the underlying file descriptor is closed).
On Windows, open acquires an exclusive LockFileEx
lock (LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY) covering the
entire file range. If another process already holds the lock, open
returns immediately with io::ErrorKind::WouldBlock
(ERROR_LOCK_VIOLATION). The lock is released when the BStack is
dropped (the underlying file handle is closed).
Note: Both
flock(Unix) andLockFileEx(Windows) are advisory and per-process. They prevent well-behaved concurrent opens across processes but do not protect against processes that bypass the lock or against raw writes to the file.
§Correct usage
bstack files must only be opened through this crate or a compatible
implementation that understands the file format, the header protocol, and
the locking semantics. Reading or writing the underlying file with raw
tools or syscalls while a BStack instance is live — or manually editing
the header fields — can silently corrupt the committed-length sentinel or
bypass the advisory lock.
The authors make no guarantees about the behaviour of this crate — including freedom from data loss or logical corruption — when the file has been accessed outside of this crate’s controlled interface.
§Thread safety
BStack wraps the file in a std::sync::RwLock. The committed payload
length is also cached in memory and kept in sync with the on-disk header
by every write-lock-held operation, so len and
is_empty can be answered under the read lock without
any File::metadata syscall.
| Operation | Lock (Unix / Windows) | Lock (other) |
|---|---|---|
push, extend, extend_sparse, extend_sparse_batched, pop, pop_into, discard | write | write |
set, zero, repeat (feature) | write | write |
atrunc, splice, splice_into, try_extend, try_extend_zeros, try_extend_sparse, try_extend_sparse_batched (feature: atomic) | write | write |
try_discard(s, n > 0) (feature: atomic) | write | write |
try_discard(s, 0) (feature: atomic) | read | read |
get_batched, get_batched_into, get_batched_gen (feature: atomic) | read | write |
swap, swap_into, cas (features: set+atomic) | write | write |
cross_exchange, copy, process, process_gen, set_batched, inplace_gen (features: set+atomic) | write | write |
eq_crds, ne_crds, masked_eq_crds, masked_ne_crds (features: set+atomic) | write | write |
replace (feature: atomic) | write | write |
peek, peek_into, get, get_into | read | write |
len | read | read |
On Unix and Windows, peek, peek_into, get, and get_into use a
cursor-safe positional read (pread(2) on Unix; ReadFile with
OVERLAPPED on Windows) that does not modify the file-position cursor.
This allows multiple concurrent calls to any of these methods to run in
parallel while any ongoing push, pop, or pop_into still serialises
all writers via the write lock. For get and
get_into, reads that lie entirely within the
locked region bypass the rwlock — see that
section for the concurrency model.
On other platforms a seek is required, so peek, peek_into, get, and
get_into fall back to the write lock and all reads serialise.
Unlike get_batched_gen, which only ever takes
the read lock (Unix/Windows), process_gen and
inplace_gen always take the write lock — even
for sequences that turn out to be read-only and end in None — because the
closure may decide, only after seeing earlier reads, to mutate; the lock
therefore has to be acquired before the first read so the whole sequence
runs as one indivisible step.
§Locked region (lock_up_to)
BStack maintains an in-memory monotonically growing partition
boundary named the locked region. Bytes in [0, locked_len()) are
declared permanently immutable for the lifetime of the open file.
The locked length starts at 0 on every open and is
not persisted to disk — the file format is unchanged. Callers extend
the boundary by calling lock_up_to (or open and
lock in one step with open_locked_up_to).
It can only grow; attempts to shrink it return
io::ErrorKind::InvalidInput.
Opening with open_cached (or
open_locked_up_to_cached) enables
an in-memory mirror of the locked region: each lock_up_to call reads the
newly locked bytes from disk into a heap buffer, and subsequent reads whose
range falls entirely within the cached region are served with no syscall.
§Effects
-
get/get_intofast-path reads. Whengetorget_intoare called with a range that lies entirely within the locked region, the internalRwLockis bypassed.- On non-cached stacks (Unix/Windows), reads are lock-free and use
pread(2)(Unix) orReadFile+OVERLAPPED(Windows). - On cached stacks (all platforms), reads are served from the
in-memory buffer under a
Mutex(so RwLock-free, but not lock-free). Thefstatsize check is skipped on this path — the locked length is a sufficient upper bound.
- On non-cached stacks (Unix/Windows), reads are lock-free and use
-
Write protection.
set,zero,repeat,swap,swap_into,cas,process,cross_exchange,copy(destination only),eq_crds,ne_crds,masked_eq_crds, andmasked_ne_crds(region B) returnio::ErrorKind::InvalidInputwhen their write target range overlaps the locked region.atrunc,splice,splice_into, andreplacereturn the same error when the operation would modify bytes inside it. -
Shrink protection.
pop,pop_into,discard, andtry_discardreturnio::ErrorKind::InvalidInputwhen they would shrink the payload below the locked length.
Callers that never invoke lock_up_to see no behavioural change — every
read and write path adds only a single uncontended AtomicU64::load and
a comparison.
§Concurrency model
lock_up_to(n) acquires the exclusive write lock before publishing the
new boundary with a Release store. Locked-region fast-path readers
Acquire-load locked before each call. Two consequences follow:
-
A stale load is always safe. If a reader sees an older (smaller)
lockedvalue, it falls through to the rwlock path; if it sees a newer value, the entire range it now reads is by definition immutable. -
Locked-region checks on writers are evaluated under the write lock, so they cannot race against a concurrent
lock_up_toextending the boundary across the write target.
On cached stacks the cache Mutex is acquired and fully populated
before locked is advanced with the Release store. A reader that
Acquire-loads locked and then locks the cache Mutex therefore always
sees a buffer whose valid range covers at least [0, locked).
§Typical use
use bstack::BStack;
// A fixed 64-byte metadata block at the head of the file, read by many
// threads but never modified after first write.
let stack = BStack::open_locked_up_to("meta.bin", 64)?;
assert_eq!(stack.locked_len(), 64);
// Reads of the metadata bypass the rwlock on Unix and Windows.
let header = stack.get(0, 64)?;On cached stacks this locked-region fast path is available on all
platforms (served from the cache under a Mutex).
§Standard I/O adapters
§Writing
BStack implements std::io::Write (and so does &BStack, mirroring
[std::io::Write for &File]). Each call to write is forwarded to
push, so every write is atomically appended and durably
synced before returning. flush is a no-op.
use std::io::Write;
use bstack::BStack;
let mut stack = BStack::open("log.bin")?;
stack.write_all(b"hello")?;
stack.write_all(b"world")?;§Reading
BStackReader wraps a &BStack with a cursor and implements
std::io::Read and std::io::Seek. Use BStack::reader or
BStack::reader_at to construct one.
use std::io::{Read, Seek, SeekFrom};
use bstack::BStack;
let stack = BStack::open("log.bin")?;
stack.push(b"hello world")?;
let mut reader = stack.reader();
let mut buf = [0u8; 5];
reader.read_exact(&mut buf)?; // b"hello"
reader.seek(SeekFrom::Start(6))?;
reader.read_exact(&mut buf)?; // b"world"§Trait implementations
§BStack
| Trait | Semantics |
|---|---|
Debug | Shows version (semver string from the magic header, e.g. "0.4.4") and len (Option<u64>, None on I/O failure). |
PartialEq / Eq | Pointer identity. Two values are equal iff they are the same instance. No two distinct BStack values in one process can refer to the same file. |
Hash | Hashes the instance address — consistent with pointer-identity PartialEq. |
§BStackReader
| Trait | Semantics |
|---|---|
PartialEq / Eq | Equal when both the BStack pointer (identity) and the cursor offset match. |
Hash | Hashes (BStack pointer, offset) — consistent with PartialEq. |
PartialOrd / Ord | Ordered by BStack instance address, then by cursor offset. Groups all readers over the same stack and within that group orders by position. |
§Feature flags
-
set— In-place overwrite of existing payload bytes without changing the file size (BStack::set,BStack::zero,BStack::repeat). -
alloc— Region-based sub-allocation over aBStackpayload. Adds the allocator traits, handle types (BStackRange,BStackOwnedSlice,BStackSlice,BStackChunk),LinearBStackAllocator, andDebugCheckingAllocator. Combined withset, also enablesBStackSliceWriter,FirstFitBStackAllocator,GhostTreeBstackAllocator,SlabBStackAllocator,CheckedSlabBStackAllocator,SegregatedBStackAllocator(experimental), andBStackByteVec. -
atomic— Compound read-modify-write operations that hold the write lock across what would otherwise be separate calls. Combined withset, also enables atomic swap, CAS, in-place batch writes, and cross-region operations.
Enable with:
[dependencies]
bstack = { version = "0.4", features = ["set"] }
# or
bstack = { version = "0.4", features = ["alloc"] }
# or both
bstack = { version = "0.4", features = ["alloc", "set"] }§Allocator (alloc feature)
The alloc feature adds a region-management layer on top of BStack.
§Key types
-
BStackAllocator— trait for types that own aBStackand manage contiguous byte regions within its payload. Requiresstack(),into_stack(),alloc(), andrealloc(); provides a default no-opdealloc()and delegation helperslen()/is_empty(). -
BStackBulkAllocator— extension trait forBStackAllocatorthat adds atomic bulk operations. Both methods are required with no default; on error the backing store is left unchanged unless a crash occur. -
BStackUninitAllocator— opt-in extension trait forBStackAllocatorwhosealloc_uninit/realloc_uninitskip zero-initialising newly allocated or grown bytes. The returned bytes are unspecified (leftover from a prior allocation) but always valid to read, saving the zero-fill write for callers that overwrite the region before reading it. Existing bytes are preserved exactly asrealloc. Implementing it is optional and signals that the allocator actually has a cheaper uninitialised path. Implemented bySlabBStackAllocator,GhostTreeBstackAllocator,CheckedSlabBStackAllocator,SegregatedBStackAllocatorandFirstFitBStackAllocator, and forwarded byDebugCheckingAllocatorwhen its inner allocator implements it.LinearBStackAllocatordeliberately does not: a bump allocator only ever hands out freshly extended tail, whose zeroes cost no write I/O. -
BStackAllocError<'a, A>— error returned byrealloc/dealloc. Carries the failingsourceplushandle: Option<A::Allocated<'a>>, the surviving allocation handed back to the caller so a failed resize/free is not a silent leak.BStackBulkAllocErroris itsdealloc_bulkcounterpart, returning aVecof the handles it did not free. -
BStackRange— raw(offset, len)pair;Copy, no pointer, no I/O. Serialises to/from[u8; 16]for persistent bookkeeping. -
BStackOwnedSlice<'a, A>— ownership handle returned byalloc/realloc. Non-Copy, non-Clone; owns the allocation lifetime'a. Exposesas_slice()/as_slice_mut()to obtain a borrowed view, and also provides convenienceread*/write*/zero*methods that delegate via those views. Passed by value toreallocanddealloc; Drop is a no-op. -
BStackSlice<'a>— borrowed I/O view over a region. Non-Copy; obtained fromBStackOwnedSlice::as_slice[_mut]()or directly fromBStackSlice::from_raw_parts. Exposesread,read_into,read_range_into,subslice,subslice_range,reader,reader_at, and (with thesetfeature)write,write_range,zero,zero_range. -
BStackSliceReader<'a>— cursor-based reader over aBStackSlice, implementingio::Readandio::Seekin the slice’s coordinate space. -
LinearBStackAllocator— reference bump allocator that appends regions sequentially.reallocis O(1) for the tail allocation and returnsUnsupportedfor non-tail slices.deallocreclaims the tail viaBStack::discard(orBStack::try_discardwithatomic); non-tail deallocations are a no-op. Every operation maps to exactly oneBStackcall and is crash-safe by inheritance.Sendin all configurations; alsoSyncwith theatomicfeature. ImplementsBStackAllocatorandBStackBulkAllocator. -
FirstFitBStackAllocator— A persistent first-fit free-list allocator that reuses freed regions to prevent unbounded file growth. Requires bothallocandsetfeatures.Sendin all configurations; alsoSyncwith theatomicfeature, where an internalMutexserializes free-list mutation and stack extension. -
GhostTreeBstackAllocator— A pure-AVL general-purpose allocator with zero-overhead live allocations. Free blocks store their AVL node inline, and the tree is keyed on(size, address)for best-fit allocation. Provides O(log n) allocation and deallocation with crash recovery through tree rebalancing on mount. Requires bothallocandsetfeatures.Sendin all configurations;Send + Syncwith theatomicfeature, where an internalMutexserialises AVL tree mutations. -
SlabBStackAllocator— Fixed-block slab allocator. All blocks are exactlyblock_sizebytes with no per-block header or footer; freed blocks are tracked via an intrusive singly-linked free list stored in the first 8 bytes of each free block. O(1) alloc and dealloc. UseSlabBStackAllocator::newto initialise an empty stack andSlabBStackAllocator::opento reopen an existing one. Requires bothallocandsetfeatures; withatomicadditionally implementsBStackBulkAllocator(alloc_bulk/dealloc_bulk). -
CheckedSlabBStackAllocator— Crash-recoverable variant ofSlabBStackAllocator. Prefixes every block with an 8-byte overhead field (zero when free, high bit set with a block count when in use) so leaked blocks are recoverable by a linear scan and double-frees are caught at runtime before the free list can be corrupted. Constructor takesdata_size(usable bytes per block, ≥ 8); the on-diskblock_sizeisdata_size + 8. UseCheckedSlabBStackAllocator::newto initialise an empty stack andCheckedSlabBStackAllocator::opento reopen one (openrunsrecoverautomatically). Requires bothallocandsetfeatures; withatomicadditionally implementsBStackBulkAllocator(alloc_bulk/dealloc_bulk). -
SegregatedBStackAllocator— experimental segregated (binned) free-list allocator. GeneralisesCheckedSlabBStackAllocatorfrom one block size to 33 size classes sharing a single arena: 16 linear classes (16‥256 B, step 16), 16 geometric classes (320‥4096 B, 4 per octave), and one shared oversized bucket. Each class is an independent intrusive free list; the class is computed from the request with register arithmetic (no tables), giving O(1) classed alloc/dealloc. Every block carries the same 8-byte overhead tag as the checked slab, so leaked blocks are reclaimable by a linear scan and double-frees are caught. A singlenewconstructor initialises an empty stack or reopens one (running recovery automatically). Requires bothallocandset;Sendin all configurations,Send + Syncwithatomic(no allocator-level lock — free-list splices rideBStack::process_gen/BStack::inplace_gen), where it additionally implementsBStackBulkAllocator(alloc_bulk/dealloc_bulk, work bounded by the classes touched, with oversized requests matched largest-first against the oversized free list). Experimental: the on-disk format and API may change, some resize paths differ between theatomicand non-atomicbuilds, and the deep in-use-leak GC is not yet implemented (the free-neighbour coalescer,coalesce, now is —atomiconly). -
DebugCheckingAllocator<A>— transparent debug wrapper. Wraps any allocator whoseAllocatedtype isBStackOwnedSliceand whoseErrorisio::Error. Tracks allocated and freed regions in memory and panics on overlapping allocations, double-frees, partial-frees, and multi-span frees. When the inner allocator reports a lost handle (handle: NoneinBStackAllocError), the region is removed from tracking entirely — its fate is unknown, so neither “live” nor “freed” would be correct. Intended for tests and debugging; O(n) per-operation overhead. Requiresalloconly. -
BStackByteVec<'a, A>— a growable byte (u8) vector backed by aBStackallocation (requiresalloc+set). Mirrors the coreVec<u8>API:new,with_capacity,from_slice,push,extend_from_slice,pop,get,set,read_bytes,as_slice,truncate,clear,fill,reserve,reserve_exact,resize,shrink_to,shrink_to_fit, anditer. With theatomicfeature it also gains the crash-atomic byte moversinsert,remove,swap_remove,extend_from_within, and the cross-sliceextend_from_bstack_slice,copy_into_bstack_slice,append_from_owned, andmove_tail_into. The block stores a 16-byte header (len,cap) followed by the byte data; the header is re-read on every call for crash recoverability.pushdoubles capacity (minimum 4);popdecrementslenthen zeros the vacated slot;truncatewriteslenthen zeros all removed slots.
§Lifetime model
BStackOwnedSlice<'a, A> borrows the allocator for 'a.
The borrow checker statically prevents calling
BStackAllocator::into_stack — which consumes the allocator by value —
while any owned slice is still in scope. BStackSlice<'a> views obtained
via as_slice[_mut]() have a shorter lifetime tied to the borrow of the
owned slice, preventing them from outliving the handle that owns the region.
What lifetimes cannot express is that a handle goes back to the allocator
that issued it: two allocators of the same type are the same type, so
a2.dealloc(h1) compiles. That is not a soundness problem — handles are
(offset, len) coordinates into a file, not pointers — but it would corrupt
a2’s bookkeeping, so every allocator rejects a foreign handle at run time
with io::ErrorKind::InvalidInput, returning the handle intact and its own
metadata untouched. BStackOwnedSlice::is_from is the check, for custom
allocators that need it.
§Quick example
use bstack::{BStack, BStackAllocator, LinearBStackAllocator};
# fn main() -> std::io::Result<()> {
let alloc = LinearBStackAllocator::new(BStack::open("data.bstack")?);
let mut slice = alloc.alloc(128)?; // reserve 128 zero bytes
let data = slice.read()?; // read them back
alloc.dealloc(slice)?; // release (tail, so O(1))
let stack = alloc.into_stack(); // reclaim the BStack
# Ok(())
# }§Examples
use bstack::BStack;
let stack = BStack::open("log.bin")?;
// push returns the logical byte offset where the payload starts.
let off0 = stack.push(b"hello")?; // 0
let off1 = stack.push(b"world")?; // 5
assert_eq!(stack.len()?, 10);
// peek reads from a logical offset to the end without removing anything.
assert_eq!(stack.peek(off1)?, b"world");
// get reads an arbitrary half-open logical byte range.
assert_eq!(stack.get(3, 8)?, b"lowor");
// pop removes bytes from the tail and returns them.
assert_eq!(stack.pop(5)?, b"world");
assert_eq!(stack.len()?, 5);§Fault injection (fault-injection feature)
This build has the dev/test-only fault-injection feature active, so
BStack I/O can be made to fail on demand. Implement FaultPolicy and arm
it with BStack::with_fault_policy (at construction) or
BStack::set_fault_policy (arm, re-arm, or disarm mid-test); every I/O
method then consults the policy once, after validating its arguments. This
exercises error-handling and rollback paths that a successful sequence of calls
can never reach. The whole mechanism is gated on all(debug_assertions, feature = "fault-injection"), so a --release build carries none of it and its
performance is unaffected. See the fault module for details.
Re-exports§
pub use fault::FaultPolicy;pub use fault::FaultState;
Modules§
- fault
- Deterministic I/O-fault injection at the
BStackAPI level. - reborrow
- Lifetime-extending reborrows for generator call sites.
Macros§
- bstack_
unsafe_ reborrow - Extends a shared borrow so it can escape a
process_gen/inplace_gengenerator closure. - bstack_
unsafe_ reborrow_ mut - Extends a mutable borrow so it can escape a
process_gen/inplace_gengenerator closure.
Structs§
- BStack
- A persistent, fsync-durable binary stack backed by a single file.
- BStack
Alloc Error - Error returned by
BStackAllocator::reallocandBStackAllocator::deallocwhen the operation fails. - BStack
Bulk Alloc Error - Error returned by
BStackBulkAllocator::dealloc_bulkwhen the bulk free fails, carrying back the handles that were not freed. - BStack
Byte Vec - A growable byte vector backed by a
crate::BStackallocation. - BStack
Byte VecIter - An iterator over the bytes of a
BStackByteVec. - BStack
Chunk - A fixed-size-record view over a
BStackSlice— a slice with a stride. - BStack
Chunk Iter - A lazy, zero-I/O iterator over the chunks of a
BStackChunk. - BStack
Join Error - Error returned by
BStackOwnedSlice::try_joinandtry_join_inplacewhen the concatenation fails. - BStack
Owned Slice - An owned allocation handle for a region managed by a
BStackAllocator. - BStack
Range - A raw
(offset, len)coordinate pair with no backing reference. - BStack
Reader - A cursor-based reader over a
BStackpayload. - BStack
Slice - A borrowed, non-owning view of a contiguous region within a
BStackpayload. - BStack
Slice Error - Error returned by
BStackOwnedSlice::try_subsliceandtry_subslice_inplacewhen the narrowing fails. - BStack
Slice Reader - A cursor-based reader over a
BStackSlice. - BStack
Slice Writer - A cursor-based writer over a
BStackSlice. - Checked
SlabB Stack Allocator - A crash-recoverable fixed-block slab allocator implementing
BStackAllocatoron top of aBStack. - Debug
Checking Allocator - Debug-only allocator wrapper that validates allocations and deallocations.
- First
FitB Stack Allocator - A persistent first-fit free-list allocator implementing
BStackAllocatoron top of aBStack. - Ghost
Tree Bstack Allocator - A pure-AVL general-purpose allocator built on top of a
BStack. - Interrupted
Write - The error a read carries while an interrupted write is pending replay.
- LinearB
Stack Allocator - A simple bump allocator that owns a
BStackand allocates regions sequentially by appending to the tail. - SegregatedB
Stack Allocator - A segregated free-list allocator implementing
BStackAllocatoron top of aBStack. - SlabB
Stack Allocator - A fixed-block slab allocator implementing
BStackAllocatoron top of aBStack.
Enums§
- BStack
GenOp - A request to read from, or write to, a region of the payload.
Traits§
- BStack
Allocator - A trait for types that own a
BStackand manage contiguous byte regions within its payload. - BStack
Atomic Guarded Slice - Marker trait for
BStackGuardedSliceimplementations that guarantee atomicity and crash safety. - BStack
Atomic Guarded Slice Subview - Marker trait for
BStackGuardedSliceSubviewimplementations that also satisfyBStackAtomicGuardedSlice’s atomicity and crash-safety contract. - BStack
Bulk Allocator - Extension trait for allocators that support batching multiple allocations and deallocations in a single operation.
- BStack
Guarded Slice - A
BStackSliceabstraction with lifecycle hooks for transparent I/O interception. - BStack
Guarded Slice Subview - Extension trait for
BStackGuardedSliceimplementations that can produce a narrowed sub-view while preserving the full hook scope of the parent. - BStack
InPlace Resize Allocator - Extension trait for allocators that can resize a region at either edge without relocating its retained bytes.
- BStack
Owned Slice Allocator - Convenience supertrait for the common case of a
BStackAllocatorwhose handle type isBStackOwnedSliceand whose error type isio::Error. - BStack
Uninit Allocator - Extension trait for allocators that can skip zero-initialisation of newly allocated or grown regions.