NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #2817 most downloaded on crates.io
Binary-level structure packing and unpacking generator
Last release 7 days ago
01 Oct 2026
Ships unpredictably
gaps range from 2 weeks to 3.9 years
Nearly every release is documented
notes for 14 of 15 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
15 releases · first in 2018
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 b
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.
bytes="x..y". Every one of them changes its layout,These change what pack() produces and what unpack() expects for the affected structs, with no
compile error or warning.
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>>.
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.
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.
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]).
{ &<temp> }.pack(), whichDebug formatter and the rustdoc table used to be unrolled per element, so large arrays took[u16; 20000] field overflowed rustc's stack. The derive now emits one#[derive(PrimitiveEnum)] infers a type that holds every discriminant. Enums with large-3000000000 was inferred as i32) or with mixed signs (-1 and 200i8) failed to compile. The type is now the smallest integer that holds both the smallest andBits<N> is generated up to the full byte width. The widest type was one bit short: 255 bitsbyte_types_64 or byte_types_256, so a reserved field ofReservedZero<Bits<256>>, failed to compile.PackingError::BufferSizeMismatch instead of underflowing the length calculation (a panic in debugalloc feature builds on stable Rust. It used the nightly-only #![feature(alloc)], and theno_std prelude was missing an import.lsb0 numbers bits ([#92]). lsb0 bit 0 is the least significant bit of the lastendian 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 forpacked_struct::__private is a new hidden module with support code for the derivetry_array_from_fn). It is not public API and can change in any release.actions/checkout@v5, explicit toolchains, feature-combination checks, an MSRV job, ands390x-unknown-linux-gnu replaces mips64 (tier 3, no prebuilt std) as the big-endian target.One column per quarter.
Clippy lint the generated code in tests by @rudib in #84
bitvec to 1.0 ([#95]).First release with partial support for const generics in byte-width types. Now requires at least Rust 1.51.
Bits1, Bits2, ... and Bytes1, Bytes2, ...
marker types were replaced by Bits<N> and Bytes<N> ([#69]). Minimum Rust version 1.51.bitvec.Fixed a Rust compiler warning with unused mutable slices
Fixed the values for PrimitiveEnum 's implicitly enumerated discriminators
Bundled the derive macros with the main crate, so no dependency to the codegen crate is needed anymore
packed_struct, a separate packed_struct_codegen dependency
is no longer needed ([#66]).Updated to "1.0" releases of syn and quote packages
syn and quote packages (#53)PackedStruct trait is not generic anymore, the size of the array has been moved into an associated type. Thus a single struct can only be serialized into a particular output sizepack() trait method now returns a Result type for potential failures in packingbyte_types_64 and byte_types_256use_serdeInteger wrapper now implements Eq and Hash traits (#55)pack() returns a Result, the byte array is an
associated type, and slice indexing is checked everywhere, so malformed input returns a
PackingError instead of panicking ([#44], [#52]).syn/quote 1.0 ([#54]).use_serde feature ([#57]).Hash and Eq for the built-in type wrappers ([#58]).byte_types_64 and byte_types_256 features for types above 32 bytes, which aren't generated by
default ([#56]).Big-endian platforms fix
Big-endian platforms fix
unpack_from_slice checks that the slice is long enough ([#37]).release 0.3.0 with new enum api
release 0.3.0 with new enum api
u8 can be used in structures ([#31]).EnumCatchAll for enum fields that need to keep unknown values ([#26]).The alloc feature for no_std + alloc builds ([#24]).
alloc feature for no_std + alloc builds ([#24]).- Basic no_std support ([#23]).
no_std support ([#23]).Reserved fields, always packed as zeroes or ones ([#19], [#21]).
Debug/Display formatting of packed structs and integers ([#8], [#20]).Breaking: .. in bit positions is exclusive, like in Rust. Use : or ..= for inclusive ranges ([#12]).
.. in bit positions is exclusive, like in Rust. Use : or ..= for inclusive
ranges ([#12]).rust-lang/cargo#4242
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →