Skip to content

Repository files navigation

smol-bytes

High-performance, clone-efficient byte buffers optimized for small data.

github LoC Build codecov

docs.rs crates.io crates.io license

Discord

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 inlined

Rule 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::Bytes shares the allocation (From impl).
  • shared::Bytes -> bytes::Bytes reuses 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_compact convert without copying heap contents; Bytes::try_into_mut reclaims 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, plus quickcheck and arbitrary generators 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 -v

The 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 MemoryError on absurd or failing requests instead of aborting the interpreter, like CPython containers.
  • memoryview(...) over the shared and compact Bytes classes 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 test

The Wasm build entry point is:

wasm-pack build smol-bytes-wasm --target bundler --out-dir ../js/pkg --out-name smol_bytes -- --features wasm

The 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 as Bytes.
  • Cloning an inline value copies 64 bytes; cloning a heap-backed immutable value bumps a reference count; cloning BytesMut/Utf8BytesMut copies contents.
  • Shared heap-backed conversions to and from bytes::Bytes reuse 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-deps

See 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.

About

High-performance, clone-efficient byte buffers optimized for small data.

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages

Generated from al8n/template-rs