Skip to content

0.12.0

Latest

Choose a tag to compare

@rudib rudib released this 01 Oct 05:39
· 2 commits to master since this release
9d05db2

This is the first release since 0.10.1. The 0.11.0 version number was used on master in the
meantime but never published to crates.io, so everything below is relative to 0.10.1.

The release contains several silent wire-format fixes: some struct definitions that compiled
under 0.10.1 packed and unpacked their fields at the wrong bits. They still compile, but the bytes
they produce and accept are now different (and correct). If you store packed data or exchange it
with other systems, read Wire-format fixes before upgrading.

Upgrade checklist

  1. Search your code for exclusive byte ranges, bytes="x..y". Every one of them changes its layout,
    see below.
  2. Check for nested structs, reserved fields or other fields of 32 bytes or more that don't start
    and end on a byte boundary.
  3. Fix any new compile errors about field positions and sizes that disagree. Each one points at a
    field whose layout was previously wrong or ambiguous.
  4. Upgrade the toolchain to Rust 1.85 or newer.

Wire-format fixes

These change what pack() produces and what unpack() expects for the affected structs, with no
compile error or warning.

Exclusive byte ranges, bytes="x..y", were one byte short ([#107])

The byte range was converted to bits as x*8 ..= (y-1)*8 - 1 instead of x*8 ..= y*8 - 1, so
bytes="0..2" covered only byte 0, and bytes="1..3" only byte 1. Bit ranges (bits="x..y") and
inclusive byte ranges (bytes="x..=y") were not affected.

Whenever the type still fit into the shorter range, this compiled and silently truncated the field:

#[derive(PackedStruct)]
#[packed_struct(endian="msb", bit_numbering="msb0", size_bytes="4")]
pub struct Reg {
    #[packed_field(bytes="0..4")]
    value: u32,
}
0.10.1 0.12.0
Reg { value: 0x12345678 }.pack() [0x34, 0x56, 0x78, 0x00], a 24-bit field [0x12, 0x34, 0x56, 0x78]
Reg::unpack(&[0x12, 0x34, 0x56, 0x78]) value: 0x123456, the last byte ignored value: 0x12345678

The same happened to a u64 at bytes="0..8" (packed as 56 bits), and to array fields, whose
elements shrank to fit: [u8; 4] at bytes="0..4" was packed as four 6-bit elements into the first
3 bytes. Fields that didn't fit into the shorter range, like a u16 at bytes="0..2", failed to
compile.

Who is affected: every field positioned with bytes="x..y".

Migration: no code change is needed to get the intended layout. If you need to keep reading data
packed by 0.10.1, describe the old layout explicitly: bytes="x..y" used to cover the bytes
x..=y-2, so bytes="0..4" on a u32 becomes bytes="0..=2" on an Integer<u32, Bits<24>>.

Fields of 32 bytes or more at unaligned positions lost bits

For fields that are not byte-aligned (their start bit or width is not a multiple of 8), the derive
builds a per-byte mask from a running bit count that was cast to u8. For fields of 32 bytes or
more the count wrapped around, so the mask for every 32nd byte of the field was truncated and some
of its bits were silently dropped, both on pack() and on unpack(). For example, a 40-byte nested
struct of all ones at bit offset 4 packed with 4 bits of its 32nd byte cleared, and didn't
round-trip.

Who is affected: fields that are at least 256 bits wide and don't start and end on a byte
boundary. In practice these are nested PackedStruct fields, large ReservedZero/ReservedOne
fields and similar wide types placed after a bit field. Integer fields are at most 64 bits and are
not affected.

Migration: none. Data that went through the old code lost those bits, and there is nothing to
restore.

Breaking changes

  • Field positions and sizes must agree. A field whose position covers a different number of
    bits than its declared size is now a compile error, instead of one of them silently winning:

    #[packed_field(bits="0..=7", size_bits="4")]  // error: The field's position covers 8 bits, but its size is 4 bits.
    a: Integer<u8, packed_bits::Bits::<4>>,
  • Array fields must split evenly into their elements. An array whose bit width is not a
    multiple of its length is now a compile error. Previously the leftover bits were silently ignored
    (e.g. [u8; 3] in 8 bits became three 2-bit elements plus 2 unused bits), and an array with more
    elements than bits made the derive panic with a division by zero.

    #[packed_field(bits="0..=7")]  // error: The array's 8 bits can't be evenly split into 3 elements.
    a: [u8; 3],
  • Minimum supported Rust version is now 1.85 (was 1.51). All crates use edition 2024 and
    resolver 3 ([#116]).

  • Dependencies: syn 3.0 (via 2.0, [#101]), quote 1.0.47, proc-macro2 1.0.107,
    serde 1.0.229. The serde_derive dependency was replaced by serde's derive feature.

  • The std feature no longer enables the serde dependency. It used to pull in serde/std
    unconditionally; it is now serde?/std and only applies together with use_serde.

  • bitvec is no longer a dependency ([#114], [#117]). It pulled in radium, which doesn't build
    on targets without AtomicU64 (e.g. ESP32-S3, xtensa-esp32s3-none-elf). The two bit shifts it
    was used for in LsbInteger are now implemented in the crate; the packed output is unchanged and
    is verified against the previous implementation.

Added

  • Little-endian bitfields, byte_order="lsb" ([#29], [#39], [#92], [#96]). Many formats number
    their bits inside little-endian words: C structures with bitfields, microcontroller registers,
    USB Power Delivery, and network protocols like LIFX. A field that crosses a byte boundary there,
    such as a 12-bit field next to 4 bits of flags, isn't contiguous in packed_struct's big-endian
    view of the bytes, so it couldn't be described at all. With
    #[packed_struct(bit_numbering="lsb0", size_bytes="N", byte_order="lsb")], the structure is
    packed as a single little-endian integer and the bit positions are the ones from the
    specification:

    #[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")]  // byte 2 and the low nibble of byte 3
        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,
    }

    Integer fields default to little-endian. Array elements start at the lowest address. The rustdoc
    table and the Display output show the LSB0 positions. The default, byte_order="msb", generates
    the same code as before.

  • LIFX example. packed_struct_examples/src/lifx.rs describes the LIFX LAN message header and
    the SetColor message, and is tested against the example packet from the LIFX documentation.

  • Integers narrower than their native type in every native type that can hold them.
    Integer<T, Bits<N>> was only implemented when N needed exactly as many bytes as T, so a
    u32 in 12 bits, a u16 in 4 bits, or [u32; 4] with element_size_bits="12" didn't compile.
    All widths up to the size of the native type are now supported, including sign extension for the
    signed types.

  • PackingError implements core::error::Error in no_std builds too ([#115]).

  • fmt::Binary for Integer<T, B> ([#105]).

Fixed

  • Derived code builds in edition 2024 crates. The derive emitted { &<temp> }.pack(), which
    fails to borrow-check under edition 2024's tail-expression temporary scope rules.
  • Array fields no longer blow up compile times ([#110], [#102], [#118]). Parsing, pack/unpack,
    the Debug formatter and the rustdoc table used to be unrolled per element, so large arrays took
    very long to compile, and a [u16; 20000] field overflowed rustc's stack. The derive now emits one
    code template per distinct bit alignment and loops over the elements, so the generated code no
    longer grows with the array length. The rustdoc table shows one row per array field.
  • #[derive(PrimitiveEnum)] infers a type that holds every discriminant. Enums with large
    negative discriminants (-3000000000 was inferred as i32) or with mixed signs (-1 and 200
    as i8) failed to compile. The type is now the smallest integer that holds both the smallest and
    the largest discriminant, and an enum that no integer type can hold is reported as a compile
    error. Enums that compiled before keep their type.
  • Bits<N> is generated up to the full byte width. The widest type was one bit short: 255 bits
    by default, and 511 or 2047 with byte_types_64 or byte_types_256, so a reserved field of
    exactly 32, 64 or 256 bytes, like ReservedZero<Bits<256>>, failed to compile.
  • Unpacking a dynamically sized tuple from a too short slice returns
    PackingError::BufferSizeMismatch instead of underflowing the length calculation (a panic in debug
    builds).
  • The alloc feature builds on stable Rust. It used the nightly-only #![feature(alloc)], and the
    no_std prelude was missing an import.

Documentation

  • How lsb0 numbers bits ([#92]). lsb0 bit 0 is the least significant bit of the last
    byte, and endian only orders the bytes inside each field, so bit_numbering="lsb0", endian="lsb" does not describe a little-endian register. The docs now include an example for
    little-endian registers.

Internal

  • packed_struct::__private is a new hidden module with support code for the derive
    (try_array_from_fn). It is not public API and can change in any release.
  • Bit-level oracle tests: a reference implementation writes each field bit by bit, and the generated
    code is checked against it for every integer width at many bit offsets, MSB and LSB, signed and
    unsigned, for arrays of unaligned integers, and for large unaligned fields.
  • Shared package metadata and dependency versions live in the workspace manifest.
  • CI: actions/checkout@v5, explicit toolchains, feature-combination checks, an MSRV job, and
    s390x-unknown-linux-gnu replaces mips64 (tier 3, no prebuilt std) as the big-endian target.