Expand description
smol-bytes
§Introduction
smol-bytes provides byte buffers that store up to 62 bytes inline — no heap
allocation, and cloning an inline value is a plain 64-byte copy. Larger values
use bytes-backed heap storage with
reference-counted clones. Two strategies control what happens when a heap
value shrinks back under the inline threshold, and UTF-8 wrappers layer
String-like, boundary-checked APIs over the same storage.
This is a good fit when most values are small and cloned often — tokens in a lexer, keys and field names, protocol headers — and allocation pressure matters.
§Install
[dependencies]
smol-bytes = "0.1"Python (pip install smol-bytes) and JavaScript (npm install smol-bytes)
packages are also published; build-from-source instructions for each binding
are below.
§Quick start
The root Bytes type is an alias for shared::Bytes. Ordinary copies and
static values of at most 62 bytes start inline; imported or owner-backed
values and retained shared heap views can remain heap-backed below that
threshold.
use smol_bytes::Bytes;
let small = Bytes::from_static(b"identifier");
assert!(small.is_inline());
let cloned = small.clone(); // 64-byte copy, no allocation
assert_eq!(small, cloned);
let heap = Bytes::copy_from_slice(&[0_u8; 63]);
assert!(heap.is_heap());
assert_eq!(heap.len(), 63);§Storage strategies
shared::Bytes preserves heap storage once a value lives there, keeping
conversions with bytes::Bytes zero-copy. compact::Bytes copies a shrinking
view of 62 bytes or fewer back into inline storage to release the allocation.
use smol_bytes::{compact, shared, Buf};
let mut shared = shared::Bytes::from(vec![0_u8; 100]);
let mut compact = compact::Bytes::from(vec![0_u8; 100]);
shared.advance(70);
compact.advance(70);
assert_eq!(shared.len(), 30);
assert_eq!(compact.len(), 30);
assert!(shared.is_heap()); // stays shareable and zero-copy convertible
assert!(compact.is_inline()); // allocation released, contents inlinedRule of thumb: use shared (the default) for I/O and bytes interop; use
compact when memory footprint matters more than conversion speed.
§Types
| Type | Storage | Mutable | Purpose |
|---|---|---|---|
Buffer | Fixed inline bytes, up to 62 bytes | Yes | no_std fixed buffer |
Bytes / shared::Bytes | Inline or shared heap-backed bytes | No | Immutable shared view |
compact::Bytes | Inline or compacting heap-backed bytes | No | Immutable compacting view |
BytesMut | Inline, then growable bytes::BytesMut storage | Yes | Mutable byte buffer |
Utf8Buffer | Fixed inline, valid UTF-8 bytes | Yes | Small mutable UTF-8 value |
Utf8Bytes / compact::Utf8Bytes | Shared or compacting UTF-8 bytes | No | Immutable UTF-8 value |
Utf8BytesMut | Inline or growable valid UTF-8 bytes | Yes | Mutable UTF-8 value |
Every handle is 64 bytes. All byte types implement bytes::Buf, and the
mutable ones implement bytes::BufMut.
BytesMut::split_to and BytesMut::split_off return Ok(BytesMut) when the
output is growable heap storage and Err(Buffer) when the output is fixed
inline storage. The try_split_* variants add an outer bounds Result, so
their shape is Result<Result<BytesMut, Buffer>, OutOfBounds>.
Rust UTF-8 split and slice indices are byte offsets that must fall on
character boundaries; offenders panic, and the try_split_to,
try_split_off, and try_slice variants return errors instead. (The Python
bindings differ deliberately — see below.)
§bytes interop
Conversions with the bytes crate are zero-copy wherever the representation
allows:
bytes::Bytes -> shared::Bytesshares the allocation (Fromimpl).shared::Bytes -> bytes::Bytesreuses heap backing; inline values copy.compact::Bytes::from(bytes::Bytes)inlines payloads of at most 62 bytes and shares larger ones.BytesMut::freeze_shared/freeze_compactconvert without copying heap contents;Bytes::try_into_mutreclaims unique heap allocations.
§Features and MSRV
| Feature | Description |
|---|---|
std (default) | Standard-library support and the heap-backed types |
alloc | Heap-backed types without std |
serde | Serde support |
borsh | Borsh support |
arbitrary | arbitrary support for generated values |
quickcheck | QuickCheck support |
async-graphql | Bytes and String GraphQL scalars for Bytes and Utf8Bytes; implies std |
sqlx | sqlx Type/Encode/Decode for Bytes and Utf8Bytes; implies std |
pyo3 | Python bindings; implies std |
wasm | WebAssembly bindings; implies std |
With no features enabled the crate is no_std and provides the fixed
Buffer and Utf8Buffer types; alloc adds the heap-backed types without
std.
The sqlx bindings decode by borrowing from the row, so a value of at most 62
bytes is built inline with no heap allocation at all. That costs one thing:
PostgreSQL will not lend BYTEA out in a simple (unprepared) query, so code on
that path — raw_sql, or a SQL string handed straight to an Executor — has
to decode the byte types as Vec<u8> and convert. query, query_as and the
query! macros carry an argument list and are therefore prepared, so they are
unaffected, as are the UTF-8 types and every MySQL and SQLite path.
Rust 1.85 is the library MSRV, and the bytes dependency floor is 1.10.
Development-only test and benchmark dependencies can require a newer
compiler, and so do both optional integrations, to different floors:
async-graphql 7.2 declares Rust 1.89, and 7.2 is the floor because the 7.0
releases do not build against the 7.2 derive crate their own dependency range
admits; sqlx 0.9 declares Rust 1.94, and 0.9 is the floor because the impls are
written against the lifetime-free Database::ArgumentBuffer introduced there.
Enabling either raises the MSRV for the whole build.
§Verification
The test and CI story is deliberately heavier than the crate’s size:
- Unit, integration, doc, and property tests (proptest state-machine
comparisons against
Vec/String, plusquickcheckandarbitrarygenerators that preserve type invariants). - Miri over the full suite under both stacked borrows and tree borrows with strict provenance and symbolic alignment checks.
- Address, leak, memory, and thread sanitizers in CI.
- Deserialization is hardened: borsh reads length-prefixed payloads in bounded chunks instead of trusting the length prefix, and serde sequence hints are capped before preallocating.
§Python
Install the published package with pip install smol-bytes (Python 3.11+).
To build from a checkout instead, Python 3.11+, Rust, and maturin are
required:
python -m venv .venv
source .venv/bin/activate
python -m pip install maturin pytest
maturin develop --features pyo3 --manifest-path smol-bytes-py/Cargo.toml
python -m pytest tests/python -vThe root smol_bytes module exposes Buffer, BytesMut, Utf8Buffer,
Utf8Bytes, and Utf8BytesMut; smol_bytes.shared and smol_bytes.compact
expose the immutable Bytes and Utf8Bytes strategy types.
The UTF-8 classes are string-like from Python: len(), indexing, and the
truncate/split_to/split_off/slice methods all work in Unicode
characters, while byte_len() and the explicitly byte-oriented Buf-style
methods (advance, get_*) work in bytes.
from smol_bytes import Utf8Bytes
from smol_bytes.shared import Bytes
raw = Bytes.from_bytes(b"abc")
assert raw.is_inline()
assert bytes(raw) == b"abc"
text = Utf8Bytes.from_str("café")
assert len(text) == 4 # Unicode characters
assert text.byte_len() == 5 # UTF-8 bytes
assert str(text) == "café"
assert str(text.split_to(3)) == "caf"Binding behavior worth knowing:
- Methods that allocate proportionally to caller data raise
MemoryErroron absurd or failing requests instead of aborting the interpreter, like CPython containers. memoryview(...)over the shared and compactBytesclasses exports a snapshot copy, not a live view.- Slice assignment on the mutable classes requires matching lengths, and contiguous assignments take a direct copy fast path.
§JavaScript / WebAssembly
Install the published package with npm install smol-bytes.
To build from a checkout instead, install Node.js 20, wasm-pack 0.13.1, and
the wasm32-unknown-unknown target. The generated package is pinned to
wasm-bindgen 0.2.126 for reproducibility:
rustup target add wasm32-unknown-unknown
cd js
npm ci
npm run build
npm testThe Wasm build entry point is:
wasm-pack build smol-bytes-wasm --target bundler --out-dir ../js/pkg --out-name smol_bytes -- --features wasmThe root export contains the core, mutable, and shared UTF-8 types; the
smol-bytes/shared and smol-bytes/compact exports provide the strategy
types. Byte conversions at the Wasm boundary return copies, offsets are byte
offsets, and fallible operations throw catchable errors rather than trapping
the instance.
import { Utf8Bytes } from "smol-bytes";
import { Bytes as CompactBytes } from "smol-bytes/compact";
const raw = CompactBytes.fromBytes(new Uint8Array([1, 2, 3]));
console.assert(raw.isInline());
console.assert(raw.toBytes()[2] === 3);
const text = Utf8Bytes.fromString("café");
console.assert(text.len() === 5); // byte length
console.assert(text.toString() === "café");§Performance characteristics
Structural properties of the representations:
- Values of at most 62 bytes construct inline with no backing allocation.
- Every handle is 64 bytes;
Option<Bytes>is the same size asBytes. - Cloning an inline value copies 64 bytes; cloning a heap-backed immutable
value bumps a reference count; cloning
BytesMut/Utf8BytesMutcopies contents. - Shared heap-backed conversions to and from
bytes::Bytesreuse the backing allocation. - Compact storage copies at most 62 bytes when inlining a shrinking view.
Run the benchmark suite when investigating a change:
cargo bench
cargo bench --bench clone
cargo bench --bench split_to§Development
These commands match the main CI checks:
cargo fmt --all -- --check
cargo clippy --workspace --no-default-features --features std,alloc,serde,borsh,arbitrary,quickcheck --all-targets -- -D warnings
cargo test --workspace --no-default-features --features std,serde,borsh,arbitrary,quickcheck
cargo test --package smol-bytes --no-default-features --features alloc,quickcheck
cargo check --package smol-bytes --lib --no-default-features
cargo doc --package smol-bytes --no-depsSee CONTRIBUTING.md for contribution guidance.
§License
smol-bytes is available under either the MIT license or the Apache License,
Version 2.0, at your option. See LICENSE-MIT and
LICENSE-APACHE.
Copyright (c) 2026 Al Liu.
Re-exports§
pub use error::*;
Modules§
- buf
allocorstd - Utilities for working with buffers.
- compact
allocorstd - The Compact strategy: Aggressively inlines data to minimize memory usage.
- error
- Error types for byte buffer operations.
- shared
allocorstd - The Shared strategy: Preserves heap allocations for fast
Bytesconversions.
Structs§
- Buffer
- A fixed-size buffer for inline storage.
- Bytes
Mut allocorstd - Growable mutable byte buffer with inline/heap storage.
- Utf8
Buffer - UTF-8 validated wrapper around
Bufferwith a String-like interface. - Utf8
Bytes Mut allocorstd - Growable mutable UTF-8 string with inline/heap storage.
Constants§
- INLINE_
CAP - Number of bytes that can be stored inline.
Traits§
- Buf
allocorstd - Read bytes from a buffer.
- BufMut
allocorstd - A trait for values that provide sequential write access to bytes.
- Utf8Buf
- Extension trait for UTF-8 validated buffer types.
- Utf8
BufMut - Extension trait for mutable UTF-8 validated buffer types.
Type Aliases§
- Bytes
allocorstd - A space-efficient byte buffer that shares heap allocations with
bytes::Bytes. - Utf8
Bytes allocorstd - A shared, immutable UTF-8 string type alias using the
Sharedstrategy.