3 releases (breaking)
Uses new Rust 2024
| new 0.3.0 | Jan 7, 2026 |
|---|---|
| 0.2.0 | Jan 7, 2026 |
| 0.1.0 | Jan 7, 2026 |
#745 in Data structures
30KB
203 lines
nexus-bits
Overview
nexus-bits provides derive macros for packing and unpacking integers as bit fields. Unlike other bitfield libraries that generate structs containing integers, nexus-bits generates newtypes that are the integer—ideal for wire protocols, database IDs, and trading systems where the packed integer is the canonical representation.
Installation
[dependencies]
nexus-bits = "0.3"
Features
- Structs: Flat bit-packed storage with builder pattern
- Enums: Tagged unions with discriminant and per-variant fields
- IntEnum: Simple integer-backed enums
- Compile-time validation (overlaps, bounds)
- Runtime overflow detection via
Result - Zero-cost
#[repr(transparent)]newtypes - Supports
u8,u16,u32,u64,u128and signed variants
Usage
Structs
Pack multiple fields into a single integer:
use nexus_bits::bit_storage;
#[bit_storage(repr = u64)]
pub struct SnowflakeId {
#[field(start = 0, len = 12)]
sequence: u16,
#[field(start = 12, len = 10)]
worker: u16,
#[field(start = 22, len = 42)]
timestamp: u64,
}
// Build with validation
let id = SnowflakeId::builder()
.sequence(100)
.worker(5)
.timestamp(1234567890)
.build()?;
// Accessors
assert_eq!(id.sequence(), 100);
assert_eq!(id.worker(), 5);
assert_eq!(id.timestamp(), 1234567890);
// Wire conversion
let raw: u64 = id.raw();
let parsed = SnowflakeId::from_raw(raw);
Flags
Single-bit boolean fields:
use nexus_bits::bit_storage;
#[bit_storage(repr = u8)]
pub struct OrderFlags {
#[flag(0)]
is_buy: bool,
#[flag(1)]
is_hidden: bool,
#[flag(2)]
is_post_only: bool,
#[field(start = 4, len = 4)]
priority: u8,
}
let flags = OrderFlags::builder()
.is_buy(true)
.is_hidden(false)
.is_post_only(true)
.priority(7)
.build()?;
assert!(flags.is_buy());
assert!(!flags.is_hidden());
assert!(flags.is_post_only());
assert_eq!(flags.priority(), 7);
IntEnum
Integer-backed enums for use in bit fields:
use nexus_bits::IntEnum;
#[derive(IntEnum, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Side {
Buy = 0,
Sell = 1,
}
#[derive(IntEnum, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TimeInForce {
Day = 0,
Gtc = 1,
Ioc = 2,
Fok = 3,
}
// Use in bit_storage
#[bit_storage(repr = u32)]
pub struct OrderInfo {
#[field(start = 0, len = 1)]
side: Side,
#[field(start = 1, len = 2)]
tif: TimeInForce,
#[field(start = 3, len = 16)]
quantity: u16,
}
let order = OrderInfo::builder()
.side(Side::Buy)
.tif(TimeInForce::Ioc)
.quantity(100)
.build()?;
// IntEnum accessors return Result (discriminant might be invalid from wire)
assert_eq!(order.side()?, Side::Buy);
assert_eq!(order.tif()?, TimeInForce::Ioc);
assert_eq!(order.quantity(), 100); // Primitives are infallible
Tagged Enums
Different interpretations of the same bits based on a discriminant:
use nexus_bits::{bit_storage, IntEnum};
#[derive(IntEnum, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Exchange { Nasdaq = 0, Nyse = 1, Cboe = 2 }
#[bit_storage(repr = i64, discriminant(start = 0, len = 4))]
pub enum InstrumentId {
#[variant(0)]
Equity {
#[field(start = 4, len = 8)]
exchange: Exchange,
#[field(start = 12, len = 20)]
symbol: u32,
},
#[variant(1)]
Future {
#[field(start = 4, len = 8)]
exchange: Exchange,
#[field(start = 12, len = 16)]
underlying: u16,
#[field(start = 28, len = 16)]
expiry: u16,
},
#[variant(2)]
Option {
#[field(start = 4, len = 8)]
exchange: Exchange,
#[field(start = 12, len = 16)]
underlying: u16,
#[field(start = 28, len = 16)]
expiry: u16,
#[field(start = 44, len = 16)]
strike: u16,
#[flag(60)]
is_call: bool,
},
}
// Build a variant
let equity = InstrumentId::equity()
.exchange(Exchange::Nasdaq)
.symbol(12345)
.build()?;
// Variant accessors are infallible (pre-validated at build time)
assert_eq!(equity.exchange(), Exchange::Nasdaq);
assert_eq!(equity.symbol(), 12345);
// Convert to wire type
let wire: InstrumentId = equity.into();
let raw: i64 = wire.raw();
// Parse from wire and dispatch by kind
let parsed = InstrumentId::from_raw(raw);
// Check variant
assert!(parsed.is_equity());
assert!(!parsed.is_future());
// Match on kind
match parsed.kind()? {
InstrumentIdKind::Equity => {
let e = parsed.as_equity()?;
println!("Equity symbol: {}", e.symbol());
}
InstrumentIdKind::Future => {
let f = parsed.as_future()?;
println!("Future expiry: {}", f.expiry());
}
InstrumentIdKind::Option => {
let o = parsed.as_option()?;
println!("Option strike: {}", o.strike());
}
}
Generated Types
For Structs
Given #[bit_storage(repr = u64)] struct Foo { ... }:
| Type | Description |
|---|---|
Foo |
#[repr(transparent)] newtype with from_raw(), raw(), field accessors |
FooBuilder |
Builder with setters and build() -> Result<Foo, FieldOverflow<u64>> |
For Enums
Given #[bit_storage(repr = i64, discriminant(...))] enum Foo { Bar { ... }, Baz { ... } }:
| Type | Description |
|---|---|
Foo |
Parent wire type with from_raw(), raw(), kind(), is_*(), as_*() |
FooBar |
Validated variant type with infallible accessors |
FooBaz |
Validated variant type with infallible accessors |
FooBarBuilder |
Builder with build() and build_parent() |
FooBazBuilder |
Builder with build() and build_parent() |
FooKind |
Discriminant enum (FooKind::Bar, FooKind::Baz) |
Error Types
use nexus_bits::{FieldOverflow, UnknownDiscriminant, Overflow};
// Returned by builders when a value exceeds field capacity
let err: FieldOverflow<u64> = FieldOverflow {
field: "sequence",
overflow: Overflow { value: 5000, max: 4095 },
};
// Returned by kind() / as_*() for invalid discriminant or IntEnum
let err: UnknownDiscriminant<u64> = UnknownDiscriminant {
field: "__discriminant",
value: 0x1234567890,
};
Compile-Time Validation
The macro rejects invalid configurations at compile time:
| Error | Example | Message |
|---|---|---|
| Overlapping fields | Two fields both use bits 0-7 | "field 'b' overlaps with 'a'" |
| Field exceeds repr | 16-bit field at bit 60 in u64 | "field exceeds 64 bits (start 60 + len 16 = 76)" |
| Flag out of bounds | #[flag(64)] in u64 |
"flag bit 64 exceeds 64 bits" |
| Zero-length field | len = 0 |
"len must be > 0" |
| Discriminant overflow | 4-bit discriminant with #[variant(20)] |
"variant discriminant 20 exceeds max 15" |
| Duplicate discriminant | Two variants with #[variant(0)] |
"duplicate discriminant 0: already used by 'Foo'" |
| Field overlaps discriminant | Field at bits 0-7, discriminant at bits 0-3 | "field 'x' overlaps with discriminant" |
Gaps between fields are allowed (reserved bits, padding).
Comparison with Existing Libraries
| Feature | nexus-bits | modular-bitfield | bitfield-struct | packed_struct |
|---|---|---|---|---|
| Flat structs | ✅ | ✅ | ✅ | ✅ |
| Tagged enums | ✅ | ❌ | ❌ | Partial |
| Validated variant types | ✅ | N/A | N/A | ❌ |
| IntEnum in fields | ✅ | ❌ | ❌ | ✅ |
| Builder pattern | ✅ | ❌ | ❌ | ❌ |
| Overflow detection | Result |
Silent truncation | Silent truncation | Varies |
| Zero-cost newtype | ✅ | ❌ (generates struct) | ❌ | ❌ |
When to use nexus-bits
Wire protocols / message formats: The integer IS the data. You receive an i64 instrument ID over the wire and need to interpret its bits differently based on a discriminant.
// nexus-bits: the i64 is your type
let id = InstrumentId::from_raw(wire_value);
match id.kind()? { ... }
// Other libraries: wrapper around storage
let id = InstrumentId::from_bytes(&wire_value.to_le_bytes());
Trading systems: Packing order flags, instrument IDs, snowflake IDs where:
- Stable integer representation matters for databases/serialization
- Sub-microsecond parsing overhead matters
- Tagged unions distinguish asset classes, order types, etc.
ID generation: Snowflake-style IDs where you pack timestamp, worker, sequence into a single integer and need both packing and unpacking.
When to use alternatives
modular-bitfield: Hardware registers, memory-mapped I/O where you're manipulating a struct in place and don't need tagged unions.
bitvec: Arbitrary-length bit arrays, bit-level slicing, when you need more than 128 bits.
packed_struct: Byte-oriented serialization with endianness control, protocol buffers style packing.
Design Philosophy
Most bitfield libraries generate a struct that contains an integer:
// modular-bitfield style
#[bitfield]
struct Flags {
a: B4,
b: B4,
}
let f = Flags::new().with_a(1).with_b(2);
let raw: u8 = f.into_bytes()[0]; // Extract the integer
nexus-bits generates a newtype that is the integer:
// nexus-bits style
#[bit_storage(repr = u8)]
struct Flags {
#[field(start = 0, len = 4)] a: u8,
#[field(start = 4, len = 4)] b: u8,
}
let f = Flags::builder().a(1).b(2).build()?;
let raw: u8 = f.raw(); // It's already the integer
let f2 = Flags::from_raw(raw); // Zero-cost conversion
This matters when your domain thinks in integers—database columns, wire protocols, hash keys—rather than structured data that happens to be packed.
Minimum Supported Rust Version
This crate requires Rust 1.70 or later.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Related Crates
- nexus-queue - High-performance SPSC queue
- nexus-channel - Lock-free SPSC channel
- nexus-slot - Single-value container
- nexus-slab - Pre-allocated object pool
Dependencies
~79KB