bitcram 0.3.0

A small, derive-based bit packing library for compact integer representations
Documentation
  • Coverage
  • 92.31%
    12 out of 13 items documented4 out of 4 items with examples
  • Size
  • Source code size: 39.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 2.69 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 8m 10s Average build duration of successful builds.
  • all releases: 5m 12s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • malt03/bitcram
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • malt03

bitcram

Crates.io Version docs.rs Test

A small, derive-based bit packing library for Rust. Pack structured data into compact integer buffers with sub-byte field granularity.

Features

  • #[packable(B)] attribute macro for automatic Packable implementations
  • Buffer types: u8, u16, u32, u64, u128
  • Supports structs (unit, tuple, named) and enums with mixed variant kinds
  • Generics with type parameters (bounds auto-applied)
  • Debug-mode safety checks with zero release-build overhead
  • Performance comparable to established bit-packing crates

Quick Start

Encode a mini-chess move (5×6 board, 6 piece types, with promotion) into 16 bits:

use bitcram::{Packable, packable};

/// A coordinate on a 5×6 board.
///
/// 30 distinct (x, y) pairs fit in 5 bits. A naive "3 bits for x + 3 bits for y"
/// layout would use 6 bits — implementing `Packable` directly lets us collapse
/// the two fields into a single 5-bit index and reclaim that bit.
#[derive(Debug, PartialEq, Eq)]
struct Coord {
    x: u8, // 0..5
    y: u8, // 0..6
}

impl Packable<u16> for Coord {
    const SIZE: u32 = 5;
    fn pack(&self) -> u16 {
        (self.y * 5 + self.x) as u16
    }
    fn unpack(buffer: u16) -> Self {
        let i = buffer as u8;
        Self { x: i % 5, y: i / 5 }
    }
}

#[packable(u16)]
#[derive(Debug, PartialEq, Eq)]
enum Piece {
    King, Queen, Rook, Bishop, Knight, Pawn,
} // 6 variants → 3 bits

#[packable(u16)]
#[derive(Debug, PartialEq, Eq)]
enum Promotion {
    None, Queen, Rook, Bishop, Knight,
} // 5 variants → 3 bits

#[packable(u16)]
#[derive(Debug, PartialEq, Eq)]
struct Move {
    from: Coord,          // 5 bits
    to: Coord,            // 5 bits
    piece: Piece,         // 3 bits
    promotion: Promotion, // 3 bits
} // 16 bits total — fits exactly in u16

let m = Move {
    from: Coord { x: 0, y: 1 },
    to: Coord { x: 0, y: 3 },
    piece: Piece::Pawn,
    promotion: Promotion::None,
};
let packed: u16 = m.pack(); // 2 bytes
assert_eq!(Move::unpack(packed), m);

Each move serializes to 2 bytes. A naive byte-aligned encoding of the same data would take 4 bytes or more.

Supported types

#[packable(B)] can derive Packable<B> for:

  • Unit structs (struct Foo;) — SIZE = 0
  • Tuple structs (struct Foo(X, Y);)
  • Named structs (struct Foo { x: X, y: Y })
  • Enums with any combination of variant kinds:
    • Unit variants (Bar)
    • Empty tuple variants (Bar())
    • Tuple variants (Bar(X, Y))
    • Named variants (Bar { x: X, y: Y })
  • Generic typesPackable<B> bounds are auto-applied to type parameters

Multiple buffer types can be specified to generate one impl per type:

#[packable(u16, u32, u64)]
struct Foo { #[bits(5)] x: u8, #[bits(5)] y: u8 }

Built-in Packable implementations are provided for:

Type SIZE
bool 1
Option<T> 1 + T::SIZE
(T1, T2, ..., Tn) (up to 12-tuples) T1::SIZE + T2::SIZE + ... + Tn::SIZE
[T; N] N * T::SIZE

For primitive types or custom encodings (like Coord above), implement Packable<B> manually — or annotate fields with #[bits(N)] to specify the bit width directly:

use bitcram::{Packable, packable};

#[packable(u16)]
#[derive(Debug, PartialEq, Eq)]
struct Foo {
    #[bits(5)] x: u8,
    #[bits(6)] y: u8,
    #[bits(5)] z: u8,
}

let foo = Foo { x: 31, y: 63, z: 31 };
assert_eq!(Foo::unpack(foo.pack()), foo);

Without #[bits], the field type must implement Packable<B> — there is no automatic width inference from primitive types. This makes the requirement explicit at the cost of a small annotation. The field type must be cast-compatible with the buffer type in both directions; all primitive unsigned integers (u8, u16, u32, u64, u128) qualify.

The field type must be cast-compatible with the buffer type in both directions (*field as B and B as FieldType). All primitive unsigned integers (u8, u16, u32, u64, u128) qualify.

Conventions

The library trades runtime checks for performance, with a few rules the caller is expected to follow:

  • Packable::pack() must return a value that fits in SIZE bits. Debug builds verify this with assert!; release builds silently mask oversized values to prevent buffer corruption.
  • Total packed size must not exceed B::BITS. Debug builds track cumulative bits; release builds rely on this convention.
  • A single raw_pack/raw_unpack call must use size < B::BITS. Compose multiple smaller calls if you need to fill the buffer exactly. (Shifting by the full type width is undefined behavior in Rust.)
  • Empty enums are silently skipped by the derive macro. This allows incremental development without compile errors on placeholder types.
  • The Unpacker trusts its input. Decoding malformed data may produce garbage but will not panic on its own.

Benchmarks

Compared against similar crates packing 4 fields (4-bit + 4-bit + 8-bit + 16-bit = 32 bits total):

Crate Pack Unpack Round-trip Output size
bitcram 934 ps 1.61 ns 2.00 ns 4 bytes
modular-bitfield 967 ps 1.54 ns 1.99 ns 4 bytes
bitfield-struct 970 ps 1.55 ns 2.00 ns 4 bytes
bincode 2.x 3.82 ns 4.33 ns 8.09 ns ~6 bytes
postcard 6.32 ns 2.98 ns 9.16 ns ~6 bytes

The three bit-packers are essentially equivalent. Byte-level serializers are ~4× slower on round-trip and produce ~50% larger output for this kind of small, fixed-shape data.

Numbers were measured on a single machine and will vary by hardware; reproduce locally with:

cargo bench -p bitcram_bench

Workspace structure

  • bitcram/ — runtime crate (Packable, Buffer, Packer, Unpacker)
  • bitcram_derive/ — proc-macro crate (#[packable])
  • bitcram_bench/ — comparison benchmarks (not published)

Requirements

  • Rust 1.85+ (edition 2024)

License

Licensed under either of MIT or Apache License 2.0 at your option.