Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`BStackVec<'a, T: Copy, A: BStackSliceAllocator>`** (`alloc` + `set` features): A typed, growable vector backed by a `BStack` allocation, mirroring the core `Vec<T>` API. The 16-byte block header stores `len` and `capacity` as little-endian `u64` values; elements follow immediately. The header is re-read from disk on every call, so the metadata is recoverable after a crash by reconstructing the handle via `BStackVec::from_raw_block`. Key design points:
- **Growth**: when `push` would exceed capacity, the block is reallocated to `max(cap * 2, 4)` elements via the allocator's `realloc`. New element space is zero-initialised by `BStack::extend`.
- **Zeroing on removal**: `pop` decrements `len` before zeroing the vacated slot; `truncate` writes the new `len` before zeroing removed slots in a single `BStackSlice::zero_range` call. Deallocation zeroing is delegated to the allocator.
- **API**: `new`, `with_capacity`, `from_slice`, `unsafe from_raw_block`, `len`, `capacity`, `is_empty`, `get`, `read_vec`, `as_slice`, `push`, `pop`, `truncate`, `clear`, `reserve`, `resize`, `iter`, `raw_block`, `into_raw_block`.
- **Iterator**: `BStackVecIter<'b, 'a, T, A>` borrows the vec immutably for its lifetime (preventing concurrent mutation), snapshots `len` at construction, and yields `io::Result<T>` per element read from disk on demand.

### Changed

- **`FirstFitBStackAllocator` internal reads converted from `get` to `get_into` with stack-allocated buffers** (`alloc` + `set` features): Three internal reads — the 32-byte allocator header on `new`, the 8-byte `free_head` field at the start of `find_large_enough_block`, and the 8-byte block size during `realloc`'s same-block fast path — now use fixed-size stack arrays and `get_into` instead of `get`. This eliminates three small heap allocations per call to those paths, with no change to observable behaviour.
Expand Down Expand Up @@ -202,4 +210,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-process safety via advisory `flock` on Unix
- File format with 16-byte header containing magic number and committed length
- Durability guarantees with `durable_sync` (fdatasync on Unix)
- Optional `set` feature for in-place payload mutation
- Optional `set` feature for in-place payload mutation
28 changes: 28 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ windows-sys = { version = "0.52", features = [
"Win32_System_IO",
] }

[[example]]
name = "bstack_vec"
required-features = ["alloc", "set"]

[[example]]
name = "alloc_basic"
required-features = ["alloc", "set"]

[[example]]
name = "alloc_typed"
required-features = ["alloc", "set"]

[[example]]
name = "custom_alloc"
required-features = ["alloc", "set"]

[[example]]
name = "hashmap"
required-features = ["set"]

[[example]]
name = "atomic_ops"
required-features = ["atomic"]

[[example]]
name = "atomic_race"
required-features = ["atomic"]

[dev-dependencies]
rand = "0.10.1"

Expand Down
3 changes: 2 additions & 1 deletion PLANNED.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ The `FirstFitBStackAllocator` could benefit from atomic operations to improve pe

---

=======

## Adding `SlabBStackAllocator` for fixed-block slab allocation

**Feature flag:** `alloc` + `set`
Expand Down Expand Up @@ -212,4 +214,3 @@ When capacity is exceeded, `BStackVec` would use the `BStack`'s `realloc` to gro
- **Error handling:** Should methods return `Result` for all operations, or should some (e.g., `len()`, `capacity()`) be infallible? Returning `Result` allows for better error propagation but may be more verbose for simple accessors.
- **Initialization of new elements:** When growing the vector, should new elements be zero-initialized, left uninitialized (i.e. `MaybeUninit`), or should we require `T` to be `Default` and call `default()`? Zero-initialization is safer and guranteed for newly allocated space, but may be unnecessary overhead for some types.
- **Zeroing on deallocation:** Should empty parts of the vector be zeroed on deallocation for security, or should this be left to the caller? Zeroing adds overhead but can prevent data leakage.

56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ bstack = { version = "0.2", features = ["set"] }

### `alloc`

Enables the region-management layer on top of `BStack`: `BStackAllocator`, `BStackBulkAllocator`, `BStackSlice`, `BStackSliceReader`, `LinearBStackAllocator`, `FirstFitBStackAllocator`, `GhostTreeBstackAllocator`, `ManualAllocator`, and the `BStackSliceAllocator` supertrait.
Enables the region-management layer on top of `BStack`: `BStackAllocator`, `BStackBulkAllocator`, `BStackSlice`, `BStackSliceReader`, `LinearBStackAllocator`, `FirstFitBStackAllocator`, `GhostTreeBstackAllocator`, `ManualAllocator`, and the `BStackSliceAllocator` supertrait. Combined with `set`, also enables `BStackSliceWriter`, `BStackVec`, and `BStackVecIter`.

```toml
[dependencies]
Expand Down Expand Up @@ -611,6 +611,60 @@ assert_eq!(c.start(), a.start());
let stack = alloc.into_stack();
```

### `BStackVec<'a, T, A>` (`alloc + set` features)

A typed, growable vector backed by a `BStack` allocation, mirroring the core `Vec<T>` API.

```toml
[dependencies]
bstack = { version = "0.2", features = ["alloc", "set"] }
```

#### Memory layout

```
┌──────────────────────┬──────────────────────┬────────────────────────┐
│ len (8 B, LE u64) │ cap (8 B, LE u64) │ elements: [T; cap] │
└──────────────────────┴──────────────────────┴────────────────────────┘
byte 0 byte 8 byte 16
```

The header is re-read from disk on every call, so the `(len, cap)` metadata is
recoverable after a crash by reconstructing the handle from the raw block via
`BStackVec::from_raw_block`.

#### Key behaviour

- **Growth**: `push` reallocates to `max(cap × 2, 4)` elements when `len == cap`. New element space is zero-initialised by `BStack::extend`.
- **Readback helper**: `read_vec` loads all logical elements into a Rust `Vec<T>`.
- **Zeroing on removal**: `pop` decrements `len` before zeroing the vacated slot; `truncate` writes the new `len` before zeroing removed slots in a single `BStackSlice::zero_range` call. Deallocation zeroing is delegated to the allocator.
- **Iterator**: `BStackVecIter` borrows the vec immutably for its lifetime (preventing concurrent mutation) and yields `io::Result<T>` per element, reading from disk on demand.

#### Example

```rust
use bstack::{BStack, BStackVec, LinearBStackAllocator};

let alloc = LinearBStackAllocator::new(BStack::open("vec.bstack")?);

let mut v: BStackVec<u64, _> = BStackVec::new(&alloc)?;
v.push(1)?;
v.push(2)?;
v.push(3)?;

assert_eq!(v.len()?, 3);
assert_eq!(v.get(1)?, Some(2));
assert_eq!(v.pop()?, Some(3));

for item in v.iter()? {
println!("{}", item?);
}

alloc.dealloc(v.into_raw_block())?;
```

---

### Lifetime model

`BStackSlice<'a, A>` borrows the **allocator** for `'a`, not the `BStack`
Expand Down
179 changes: 179 additions & 0 deletions examples/bstack_vec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//! Persistent order log using [`BStackVec`].
//!
//! Demonstrates:
//!
//! * Storing a typed, multi-field struct (`Order`) in a `BStackVec` backed by
//! a `LinearBStackAllocator`.
//! * Iterating, querying, and mutating the live vec.
//! * Serialising the raw block handle, closing the file, reopening it, and
//! reconstructing the vec — the reopen / crash-recovery path.

use bstack::{BStack, BStackAllocator, BStackVec, LinearBStackAllocator};
use std::io;
use std::path::PathBuf;

// ── Order type ────────────────────────────────────────────────────────────────

/// A single order record.
///
/// All fields are fixed-width and the struct is `repr(C)` so the on-disk
/// layout is stable across compilations. Fixed-point money values use
/// integer cents (2 implied decimal places).
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
struct Order {
/// Unix timestamp (seconds since epoch).
timestamp: u64,
/// Price per item in hundredths of the currency unit (e.g. 1999 = $19.99).
price_cents: i64,
/// Shipping fee in hundredths of the currency unit.
shipping_cents: i64,
/// Number of items ordered (negative for returns).
amount: i32,
/// Fulfilment class: 0 = standard, 1 = express, 2 = overnight.
order_type: u8,
_pad: [u8; 3],
}

impl Order {
fn new(
timestamp: u64,
amount: i32,
price_cents: i64,
shipping_cents: i64,
order_type: u8,
) -> Self {
Self {
timestamp,
amount,
price_cents,
shipping_cents,
order_type,
_pad: [0; 3],
}
}

fn total_cents(&self) -> i64 {
self.price_cents * self.amount as i64 + self.shipping_cents
}

fn type_name(&self) -> &'static str {
match self.order_type {
0 => "standard",
1 => "express",
2 => "overnight",
_ => "unknown",
}
}
}

// ── helpers ───────────────────────────────────────────────────────────────────

fn print_orders(v: &BStackVec<Order, LinearBStackAllocator>) -> io::Result<()> {
let len = v.len()?;
println!(" {} order(s):", len);
for item in v.iter()? {
let o = item?;
println!(
" ts={:<12} {:>3} x {:>7} + {:>5} ship [{:>9}] total={:>9}¢",
o.timestamp,
o.amount,
format!("{}¢", o.price_cents),
format!("{}¢", o.shipping_cents),
o.type_name(),
o.total_cents(),
);
}
Ok(())
}

// ── main ──────────────────────────────────────────────────────────────────────

fn main() -> io::Result<()> {
let path = PathBuf::from("orders_example.bstack");

// ── Session 1: create and populate ───────────────────────────────────────

println!("=== Session 1: creating order log ===");

let block_bytes: [u8; 16] = {
let alloc = LinearBStackAllocator::new(BStack::open(&path)?);
let mut orders: BStackVec<Order, _> = BStackVec::new(&alloc)?;

orders.push(Order::new(1_748_000_000, 2, 1999, 499, 0))?; // 2× $19.99, $4.99 ship
orders.push(Order::new(1_748_000_120, 1, 8999, 0, 1))?; // 1× $89.99, free express
orders.push(Order::new(1_748_000_300, 5, 599, 999, 0))?; // 5× $5.99, $9.99 ship
orders.push(Order::new(1_748_001_000, -1, 1999, 0, 0))?; // 1 return, $19.99 refund

println!("After initial push:");
print_orders(&orders)?;
println!(" capacity={}", orders.capacity()?);

// Pop the return record, inspect it, then push a replacement.
let ret = orders.pop()?.expect("expected a record");
assert_eq!(ret.amount, -1);
println!("\nPopped return record: {:?}", ret);

// Replace with an overnight order.
orders.push(Order::new(1_748_001_500, 3, 2499, 1499, 2))?;

println!("\nAfter replacement:");
print_orders(&orders)?;

// Serialise the raw block handle (offset + len as 16 bytes).
let bytes: [u8; 16] = orders.into_raw_block().into();
bytes
// `alloc` (and the BStack) are dropped here, closing the file.
};

println!("\nFile closed. Block handle: {:?}", block_bytes);

// ── Session 2: reopen and recover ────────────────────────────────────────

println!("\n=== Session 2: reopen and recover ===");

{
let alloc = LinearBStackAllocator::new(BStack::open(&path)?);

// Reconstruct the BStackSlice from the serialised bytes.
// SAFETY: `block_bytes` was produced by `BStackSlice::to_bytes` in
// Session 1 on the same file; the offset and length are valid.
let block = unsafe { bstack::BStackSlice::from_bytes(&alloc, block_bytes) };

// Reconstruct the BStackVec.
// SAFETY: the block was created by `BStackVec<Order, _>::new` with the
// same element type; the header layout matches.
let orders: BStackVec<Order, _> = unsafe { BStackVec::from_raw_block(block) };

println!("Recovered {} order(s):", orders.len()?);
print_orders(&orders)?;

// Verify a specific record survives the round-trip.
let second = orders.get(1)?.expect("expected record at index 1");
assert_eq!(second.price_cents, 8999);
assert_eq!(second.order_type, 1);
println!("\nVerified: record[1] = {:?}", second);

// Demonstrate resize: pad to 8 slots with a zero-value placeholder.
let placeholder = Order::new(0, 0, 0, 0, 0);
let mut orders = orders;
// We must hold the mutable vec; reconstruct a mutable binding.
// (In real code the vec would be mut from the start.)
orders.resize(8, placeholder)?;
println!("\nAfter resize to 8 (placeholder-filled):");
print_orders(&orders)?;

// Truncate back to the real data.
orders.truncate(4)?;
println!("\nAfter truncate to 4:");
print_orders(&orders)?;

// Clean up via dealloc.
orders.dealloc()?;
}

// Remove the example file.
std::fs::remove_file(&path)?;
println!("\nDone.");
Ok(())
}
22 changes: 15 additions & 7 deletions src/alloc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
//! strict total order. All memory is kept zeroed: the BStack zeroes on
//! extension, and the allocator zeroes on free.
//!
//! * [`BStackVec<T>`](BStackVec) — a typed, growable vector backed by a
//! [`BStack`] allocation (requires both `alloc` **and** `set`). Mirrors the
//! core [`Vec<T>`] API: `push`, `pop`, `get`, `truncate`, `reserve`,
//! `resize`, `iter`, and more. The 16-byte block header stores `len` and
//! `capacity` as little-endian `u64`s so the state is recoverable after a
//! crash. `push` reallocates at `max(cap × 2, 4)`; `pop` zeros the vacated
//! slot; `truncate` zeros removed slots in a single
//! [`zero_range`](BStackSlice::zero_range) call.
//!
//! # Lifetime model
//!
//! `BStackSlice<'a, A>` borrows the **allocator** `A` for `'a`, not the
Expand Down Expand Up @@ -69,13 +78,8 @@
//! bstack = { version = "0.1", features = ["alloc"] }
//! ```
//!
//! In-place slice writes ([`BStackSliceWriter`]) additionally require `set`:
//!
//! ```toml
//! bstack = { version = "0.1", features = ["alloc", "set"] }
//! ```
//!
//! [`FirstFitBStackAllocator`] requires **both** `alloc` and `set`:
//! In-place slice writes ([`BStackSliceWriter`]), [`FirstFitBStackAllocator`],
//! and [`BStackVec`] additionally require `set`:
//!
//! ```toml
//! bstack = { version = "0.1", features = ["alloc", "set"] }
Expand Down Expand Up @@ -442,6 +446,8 @@ pub mod ghost_tree;
pub mod guarded;
pub mod linear;
pub mod manual;
#[cfg(feature = "set")]
pub mod vec;

pub use debug_checking::{DebugCheckingAllocator, DebugHandle};
#[cfg(feature = "set")]
Expand All @@ -454,3 +460,5 @@ pub use guarded::{BStackAtomicGuardedSlice, BStackAtomicGuardedSliceSubview};
pub use guarded::{BStackGuardedSlice, BStackGuardedSliceSubview};
pub use linear::LinearBStackAllocator;
pub use manual::ManualAllocator;
#[cfg(feature = "set")]
pub use vec::{BStackVec, BStackVecIter};
Loading