borrowed-buf 0.1.1

A small, fast, no_std alternative to the nightly-only core::io::BorrowedBuf
Documentation
  • Coverage
  • 100%
    29 out of 29 items documented13 out of 28 items with examples
  • Size
  • Source code size: 77.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 262.7 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 1s Average build duration of successful builds.
  • all releases: 1s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • M-Compressors/borrowed-buf
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • Mnwa

borrowed-buf

Crates.io Docs.rs CI MSRV

A small, fast, no_std port of the nightly-only core::io::BorrowedBuf and BorrowedCursor, usable on stable Rust (MSRV 1.89).

The API mirrors core::io on nightly (including the borrowed_buf_init methods), so switching to the standard library later is a matter of changing the import, unless you use one of the few additions.

It lets you read into uninitialized memory without zeroing it first, while tracking how much of the buffer is filled and whether the rest is initialized:

[                capacity                ]
[ filled | unfilled (may be initialized) ]
  • No dependencies, #![no_std]; the default std feature adds std::io helpers.
  • Generic over the element type: BorrowedBuf<'a, T> for any T: Copy.
  • ensure_init initializes the buffer at most once, so a reused buffer is zeroed only once.
  • Every test runs under Miri (Stacked Borrows and Tree Borrows) in CI.

Why

Most APIs that fill a buffer (Read::read, decoders, codecs) take &mut [T], which must already be initialized. That leaves two options:

  • Zero it first. [0; N] or vec![0; n] costs a full memset for every fresh buffer, often more than the read itself.
  • Use MaybeUninit<T> by hand. Then you track which prefix is written, call assume_init only on that prefix, and never create a &[T] over uninitialized memory. Get any step wrong and it's silent undefined behavior.

BorrowedBuf is a safe wrapper around &mut [MaybeUninit<T>] that does this bookkeeping for you:

  • filled(), into_filled() and into_filled_mut() expose only initialized data, as plain &[T] / &mut [T], so your code never calls assume_init.
  • append() copies straight into uninitialized memory, with no zeroing; append_fill() does the same for n copies of one value.
  • ensure_init() initializes the unfilled part at most once per buffer, and hands back a &mut [T] for APIs like Read::read.
  • A BorrowedCursor is append-only: a callee can't read, overwrite or de-initialize what the caller already filled.
  • with_unfilled_buf() hands a callee a fresh BorrowedBuf over the unfilled part, so it can inspect what it wrote.
  • Writing in place is still possible through a few unsafe methods (as_mut, advance, set_init), each with one documented precondition, so the unsafe surface is small and easy to audit.

Example

use borrowed_buf::BorrowedBuf;
use core::mem::MaybeUninit;

let mut storage = [MaybeUninit::<u8>::uninit(); 16];
let mut buf: BorrowedBuf<'_, u8> = BorrowedBuf::from(&mut storage[..]);

// Hand out a cursor: the callee can only append, never touch what's already filled.
let mut cursor = buf.unfilled();
cursor.append(b"hello");

// Write straight into the unfilled region, then record the progress.
// SAFETY: only initialized bytes are written, and the first one is written before advancing.
unsafe {
    cursor.as_mut()[0].write(b'!');
    cursor.advance(1);
}

assert_eq!(buf.filled(), b"hello!");
assert!(!buf.is_init());

Any Copy element type works, e.g. decoding samples straight into uninitialized memory:

use borrowed_buf::{BorrowedBuf, BorrowedCursor};
use core::mem::MaybeUninit;

fn decode(input: &[u8], mut out: BorrowedCursor<'_, i16>) {
    let n = out.capacity().min(input.len() / 2);
    // SAFETY: only initialized values are written.
    let slots = unsafe { out.as_mut() };
    for (slot, pair) in slots.iter_mut().zip(input.chunks_exact(2)).take(n) {
        slot.write(i16::from_le_bytes([pair[0], pair[1]]));
    }
    // SAFETY: the first `n` elements of the cursor were just written.
    unsafe { out.advance(n) };
}

let mut storage = [MaybeUninit::<i16>::uninit(); 4];
let mut samples = BorrowedBuf::<i16>::from(&mut storage[..]);
decode(&[1, 0, 0xff, 0xff], samples.unfilled());
assert_eq!(samples.filled(), &[1, -1]);

Reading from std::io::Read (with the std feature):

# #[cfg(feature = "std")] {
use borrowed_buf::{io, BorrowedBuf};
use core::mem::MaybeUninit;

let mut reader: &[u8] = b"some bytes";
let mut storage = [MaybeUninit::<u8>::uninit(); 4];
let mut buf = BorrowedBuf::<u8>::from(&mut storage[..]);

io::read_buf_exact(&mut reader, buf.unfilled()).unwrap();
assert_eq!(buf.filled(), b"some");
# }

Note that BorrowedBuf::from(&mut [MaybeUninit<T>]) can't always infer T, since MaybeUninit<T> is itself Copy and From<&mut [T]> would also apply. Name the element type (BorrowedBuf::<u8>::from(..)) when the compiler asks for it; core::io behaves the same.

Differences from core::io

The BorrowedBuf and BorrowedCursor APIs are a superset of nightly's. Two methods are additions, so code that uses them won't switch to core::io by changing the import alone:

  • BorrowedCursor::append_fill(n, value) appends n copies of value, like append but without needing a source slice (e.g. for run-length or LZ77 distance-1 runs).
  • BorrowedBuf::truncate(len) shortens the filled region, like Vec::truncate; the elements cut off stay initialized.

The rest of the additions live in the io module, because std::io::Read can't be extended with new methods on stable:

core::io / std::io (nightly) borrowed-buf
Read::read_buf(cursor) io::read_buf(&mut reader, cursor)
Read::read_buf_exact(cursor) io::read_buf_exact(&mut reader, cursor)
impl Write for BorrowedCursor<'_, u8> impl Write for BorrowedCursor<'_, u8>

License

MIT