Skip to main content

Crate bitcram

Crate bitcram 

Source
Expand description

§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:

TypeSIZE
bool1
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):

CratePackUnpackRound-tripOutput size
bitcram934 ps1.61 ns2.00 ns4 bytes
modular-bitfield967 ps1.54 ns1.99 ns4 bytes
bitfield-struct970 ps1.55 ns2.00 ns4 bytes
bincode 2.x3.82 ns4.33 ns8.09 ns~6 bytes
postcard6.32 ns2.98 ns9.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.

Structs§

Packer
Sequentially packs values into a single buffer.
Unpacker
Sequentially unpacks values from a buffer.

Traits§

Buffer
An unsigned integer type that can serve as a bit-packing buffer.
Packable
A type that can be packed into and unpacked from a Buffer of type B.

Attribute Macros§

packable
Derives bitcram::Packable<B> for a struct or enum.