In embedded systems, binary file formats, and network protocols, space is at a premium. Instead of using a whole int for a boolean flag, programmers often pack multiple small values into a single byte. For example, a single 8-bit value might store a 3-bit version number, a 1-bit status flag, and a 4-bit command code.
In C, this is handled via Bitfields. In standard Perl, you would have to use complex bitwise math (>>, &, |). In Affix, you can define these fields directly in your Struct and let the JIT engine handle the math for you.
The Recipe
We will build a parser for a hypothetical hardware "Control Register."
use v5.40;
use Affix qw[:all];
# Define the Packed Structure
# C equivalent:
# struct ControlRegister {
# uint8_t enabled : 1;
# uint8_t mode : 3;
# uint8_t reserved : 2;
# uint8_t error : 1;
# uint8_t interrupt: 1;
# };
typedef ControlRegister => Struct [
enabled => UInt8 | 1, # 1 bit
mode => UInt8 | 3, # 3 bits
reserved => UInt8 | 2, # 2 bits
error => UInt8 | 1, # 1 bit
interrupt => UInt8 | 1 # 1 bit (Total = 8 bits / 1 byte)
];
# Simulate hardware input
# Raw byte: 0b10001011
# Interrupt: 1
# Error: 0
# Reserved: 00
# Mode: 101 (5)
# Enabled: 1
my $ptr = Affix::malloc( sizeof UChar );
memcpy( $ptr, chr 0b10001011, sizeof UChar ); # Direct byte-level access
# 'Map' the structure onto the raw byte
# We use cast to interpret the scalar memory as our struct
my $reg = cast( $ptr, ControlRegister() );
# Access fields naturally
say 'Register Analysis:';
say ' Enabled: ' . ( $reg->{enabled} ? 'YES' : 'NO' );
say ' Mode: ' . $reg->{mode};
say ' Error: ' . ( $reg->{error} ? 'YES' : 'NO' );
say ' Interrupt: ' . ( $reg->{interrupt} ? 'YES' : 'NO' );
# Modify fields
say 'Disabling hardware and changing mode...';
$reg->{enabled} = 0;
$reg->{mode} = 2;
# Inspect the raw byte again
my $new_val = unpack( 'C', $ptr );
say sprintf( 'New raw value: 0b%08b', $new_val );
How It Works
-
1. The
|Operator When used inside aStructdefinition, the|operator specifies the bit-width of a field.mode => UInt8 | 3This tells Affix: "The field
modeis part of an unsigned 8-bit integer, but only occupies 3 bits." -
2. Automatic Masking and Shifting When you write
$reg->{mode} = 5, Affix does the following under the hood:- Reads the current byte from memory.
- Clears the 3 bits belonging to
modeusing a bit-mask (0xF1). - Shifts the value
5to the correct bit-position. - Bitwise-ORs the result and writes the byte back to memory. You get all the convenience of a high-level hash with the efficiency of hand-tuned C bitwise math.
-
3. Zero-Copy Views By using
caston a string (or amalloc'd pointer), we create a Live View. Modifying the hash keys modifies the underlying memory immediately. This is exactly how you would write a device driver that interacts with memory-mapped hardware registers.
Kitchen Reminders
-
Endianness Bitfield ordering is platform-dependent. Most compilers (GCC, Clang, MSVC) pack bits from the least significant bit (LSB) to the most significant bit (MSB) on x86_64, but this can vary on ARM or PowerPC. Always check your hardware manual!
-
Type Matching Ensure the base type (e.g.,
UInt8) matches the total width of the bits you are packing. If you pack 12 bits into aUInt8, Affix will correctly overflow into the next byte, but your C compiler might do something different. -
Reserved Fields Even if you don't use certain bits, you should include them in your struct definition (like the
reservedfield above) to ensure the subsequent fields are shifted to the correct bit offsets.
Comments