Skip to content

Add byte_order="lsb" for little-endian bitfield structures - #121

Merged
rudib merged 2 commits into
masterfrom
byte-order-lsb
Oct 1, 2026
Merged

rudib merged 2 commits into
masterfrom
byte-order-lsb

Conversation

@rudib

@rudib rudib commented Oct 1, 2026

Copy link
Copy Markdown
Member

Closes #29. Also addresses #39, #92 and #96.

Problem

packed_struct reads the packed bytes as one big-endian bit stream. Formats described as C bitfields on little-endian targets number their bits inside little-endian words instead. Examples are microcontroller registers, USB Power Delivery and LIFX. A field there that crosses a byte boundary next to smaller fields is split around them in the stream, so no packed_field position can describe it.

LIFX's 12-bit protocol field is one example. Its low 8 bits are byte 2 and its high 4 bits are the low nibble of byte 3, with the addressable/tagged/origin flags in between.

Change

Little-endian words laid end to end form a single little-endian integer, so one struct attribute covers all of these formats:

#[derive(PackedStruct)]
#[packed_struct(bit_numbering="lsb0", size_bytes="8", byte_order="lsb")]
pub struct FrameHeader {
    #[packed_field(bits="15:0")]
    size: u16,
    #[packed_field(bits="27:16")]
    protocol: Integer<u16, packed_bits::Bits::<12>>,
    #[packed_field(bits="28")]
    addressable: bool,
    #[packed_field(bits="29")]
    tagged: bool,
    #[packed_field(bits="31:30")]
    origin: Integer<u8, packed_bits::Bits::<2>>,
    #[packed_field(bits="63:32")]
    source: u32,
}

The derive packs the struct big-endian with the existing lsb0 code, then reverses all of its bytes:

  • Integer fields use the opposite endianness internally and default to little-endian on the wire.
  • Nested types have their bytes reversed into place.
  • Arrays are mirrored, so element 0 is at the lowest address and [u8; N] keeps its order on the wire.
  • The rustdoc table and the Display output show the lsb0 positions.
  • Structures without the attribute pack exactly as before.

Three cases are compile errors: byte_order="lsb" without bit_numbering="lsb0", a big-endian field inside such a struct that isn't byte-aligned, and an invalid byte_order value.

The README and crate docs replace the manual byte-swap recipe for little-endian registers with a "Little-endian bitfields" section. A separate commit documents the rustdoc packing table and the runtime Display output.

Tests

  • packing_byte_order_lsb_oracle.rs: 300 generated cases. Integers of widths 1 to 64 at offsets 0–7 and 13, signed and unsigned, plus big-endian fields, are checked against a reference that writes each value into a little-endian integer bit by bit.
  • packing_byte_order_lsb.rs: the struct from having a field with 12 bits #29 as written, arrays, nested structs at aligned and unaligned positions, an enum across a byte boundary, a USB PD power object as sent over the wire, and the Display output.
  • packed_struct_examples/src/lifx.rs: the full LIFX message header and the SetColor message. They pack and unpack the example packet from the LIFX docs byte for byte.

cargo test --workspace, the no_std tests, --no-default-features builds and cargo clippy --workspace --all-targets all pass.

🤖 Generated with Claude Code

rudib and others added 2 commits October 1, 2026 07:28
Every derived structure gets a rustdoc packing table and a Display
implementation that shows its packed bytes and fields. The README and the
crate docs now show both, with an example register and its output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Many formats number their bits inside little-endian words: C structures
with bitfields, microcontroller registers, USB Power Delivery, LIFX. A
field there that crosses a byte boundary next to smaller fields, like the
12-bit LIFX protocol number, is split around them in packed_struct's
big-endian view of the bytes, so no field position could describe it
(#29, #39, #92, #96).

Little-endian words laid end to end form a single little-endian integer,
so one struct attribute covers all of these formats. With
byte_order="lsb", the derive packs the struct big-endian with the lsb0
positions and reverses all of its bytes. Integer fields use the opposite
endianness internally and default to little-endian on the wire, nested
types have their bytes reversed, and arrays are mirrored so their first
element is at the lowest address. The rustdoc table and the Display output
show the lsb0 positions. Structures without the attribute pack exactly as
before.

byte_order="lsb" requires bit_numbering="lsb0", and a big-endian field
inside such a struct has to be byte-aligned. Both are compile errors, as is
an invalid byte_order value.

An oracle test checks integers of many widths, at many offsets, against a
reference that writes them into a little-endian integer bit by bit. The
LIFX example packs and unpacks the example packet from the LIFX docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rudib
rudib merged commit 9d05db2 into master Oct 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

having a field with 12 bits

1 participant