mirror of
https://codeberg.org/ziglings/exercises.git
synced 2026-03-28 21:34:52 +00:00
111/112: Add exercises for packed structs/unions
The first exercise introduces the `packed` keyword as an alternative for bitwise operations. Its main goals are establishing a solid understanding of field order and conveying the fact that packed containers are basically integers. It introduces the concept of container layouts and briefly explains the default `auto` layout before introducing the `packed` layout (but doesn't touch `extern` at all). The exercise also presents a real-world use case for packed containers, namely LZ4 frame descriptors. Furthermore it covers equality comparisons between packed containers. The second exercise talks about switch statements with packed containers and goes into some more detail on packed unions.
This commit is contained in:
parent
973ec41097
commit
16a794fbee
|
|
@ -1308,6 +1308,14 @@ const exercises = [_]Exercise{
|
|||
\\& 1110 // (bitmask)
|
||||
\\= 0110
|
||||
},
|
||||
.{
|
||||
.main_file = "111_packed.zig",
|
||||
.output = "",
|
||||
},
|
||||
.{
|
||||
.main_file = "112_packed2.zig",
|
||||
.output = "",
|
||||
},
|
||||
.{
|
||||
.main_file = "999_the_end.zig",
|
||||
.output =
|
||||
|
|
|
|||
175
exercises/111_packed.zig
Normal file
175
exercises/111_packed.zig
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
//
|
||||
// We've already learned plenty about bit manipulation using bitwise operations
|
||||
// in exercices 097 and 098 and in quiz 110. The techniques we already know work
|
||||
// just fine, but creating masks and shifting individual bits around can become
|
||||
// quite tedious and unwieldy pretty quickly.
|
||||
// What if there was a better, a more convenient way to control invidivual bits?
|
||||
//
|
||||
// Luckily, Zig has a keyword for exactly this purpose:
|
||||
//
|
||||
// packed
|
||||
//
|
||||
// It doesn't do anything on its own, to unlock its potential (and to get our
|
||||
// program to compile) we have to attach it either to a struct or to a union
|
||||
// declaration:
|
||||
//
|
||||
// const Foo = packed struct { ... };
|
||||
// const Bar = packed union { ... };
|
||||
//
|
||||
// Now, what does this keyword even do?
|
||||
// To answer this question we first have to talk about *container layouts*.
|
||||
//
|
||||
// Plain structs and unions use the `auto` layout; it gives no guarantees about
|
||||
// their size or the order of the fields they contain, both are fully up to the
|
||||
// compiler (though both size and field order *are* guaranteed to be the same
|
||||
// across any single compilation unit).
|
||||
//
|
||||
// Attaching the `packed` keyword to a container makes it use `packed` layout:
|
||||
// Suddenly, all of its fields are *packed* together tightly without any padding
|
||||
// in between and their order is guaranteed to be the same as the one specified
|
||||
// in our source code. For structs, the size of the container is guaranteed to
|
||||
// be the sum of the (bit-)sizes of all of its fields. For unions, all fields
|
||||
// have to have the exact same (bit-)size (no padding allowed!); the union itself
|
||||
// is also guaranteed to be exactly of this size.
|
||||
//
|
||||
// If you're familiar with C, you might have already heard of structure packing
|
||||
// in a different context: arranging fields in a way that minimizes the amount
|
||||
// of alignment padding between them (or having the compiler do it for you).
|
||||
// This is *not* what Zig's `packed` keyword is for!
|
||||
//
|
||||
// Try to make the comptime assertions below pass:
|
||||
|
||||
const PackedStruct = packed struct {
|
||||
a: u2,
|
||||
b: u?,
|
||||
};
|
||||
|
||||
comptime {
|
||||
assert(@bitSizeOf(PackedStruct) == 6);
|
||||
}
|
||||
|
||||
const PackedUnion = packed union {
|
||||
a: bool,
|
||||
b: u?,
|
||||
};
|
||||
|
||||
comptime {
|
||||
assert(@bitSizeOf(PackedUnion) == 1);
|
||||
}
|
||||
|
||||
// Now, how can we use this new knowledge to manipulate some bits?
|
||||
//
|
||||
// As you might have already guessed, `packed` containers are very useful for
|
||||
// representing bitflags or other tightly packed collections of bit-sized values
|
||||
// often found in file headers and network protocols.
|
||||
//
|
||||
// Let's take a look at a real-life example:
|
||||
// The LZ4 compression format (†) specifies a frame format to describe compressed
|
||||
// data. Each LZ4 frame has a descriptor, and each descriptor contains a 'FLG'
|
||||
// byte that specifies the contents of its frame:
|
||||
|
||||
/// | BitNb | 7-6 | 5 | 4 | 3 | 2 | 1 | 0 |
|
||||
/// | ------- |-------|-------|----------|------|----------|--------|------|
|
||||
/// |FieldName|Version|B.Indep|B.Checksum|C.Size|C.Checksum|Reserved|DictID|
|
||||
///
|
||||
const FLG = packed struct(u8) {
|
||||
dict_id: bool,
|
||||
reserved: u1 = 0,
|
||||
content_checksum: bool,
|
||||
content_size: bool,
|
||||
block_checksum: bool,
|
||||
block_indepencence: bool,
|
||||
version: u2,
|
||||
};
|
||||
|
||||
// Wait, what's with the `(u8)` after the `struct` keyword? What do integers have
|
||||
// to do with all of this?
|
||||
// Well, this is a good opportunity to come clear about something:
|
||||
// packed structs and packed unions aren't actually structs or unions at all...
|
||||
// They are merely integers in disguise! For all intents and purposes, their
|
||||
// fields are just convenient names for ranges of their underlying bits. To make
|
||||
// it easier to enforce size requirements for packed containers, Zig allows us
|
||||
// to specify a *backing integer* for them, just like for enums.
|
||||
//
|
||||
// In the case of `FLG`, we want our struct to occupy exactly a single byte, so
|
||||
// we specify `u8` as the backing integer. It's safe to convert between a packed
|
||||
// container and its backing integer using the builtin `@bitCast`.
|
||||
// The LZ4 spec also mandates that reserved bits must always be zero, so it's
|
||||
// good practice to set `0` as a default value for `reserved`.
|
||||
//
|
||||
// The fields of a packed struct start at the least significant bit of its backing
|
||||
// integer and end at its most significant bit. This is the case no matter what
|
||||
// endianness our target has.
|
||||
//
|
||||
// Try to silence the complaints below:
|
||||
|
||||
const Bits = packed struct(u4) {
|
||||
a: u1 = 0,
|
||||
b: u1 = 0,
|
||||
c: u1 = 0,
|
||||
d: u1 = 0,
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b1000));
|
||||
const my_bits: Bits = .{};
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0001));
|
||||
const my_bits: Bits = .{};
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0010));
|
||||
const my_bits: Bits = .{};
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0011));
|
||||
const my_bits: Bits = .{};
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b1101));
|
||||
const my_bits: Bits = .{};
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
}
|
||||
|
||||
// As we can see, equality comparisons (`==` and `!=`) work for packed structs.
|
||||
// They also work for packed unions. However, since packed containers are not
|
||||
// naturally ordered, we can't use any other comparison operators on them.
|
||||
//
|
||||
// It's also possible to use packed containers in `switch` statements, which we
|
||||
// will cover in the next exercise!
|
||||
//
|
||||
// Since packed containers make very strong guarantees about their memory layout,
|
||||
// only a handful of types are eligible to be part of them.
|
||||
// The following types are allowed as field types:
|
||||
//
|
||||
// - integers
|
||||
// - floats
|
||||
// - bool
|
||||
// - void
|
||||
// - enums with explicit backing integers
|
||||
// - packed unions
|
||||
// - packed structs
|
||||
//
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
fn complain(my_bits: Bits, expected: Bits, src_loc: std.builtin.SourceLocation) void {
|
||||
std.debug.print(
|
||||
"That's not quite right! You've got 0b{b:0>4}, but we want 0b{b:0>4} in line {d}.\n",
|
||||
.{ @as(u4, @bitCast(my_bits)), @as(u4, @bitCast(expected)), src_loc.line },
|
||||
);
|
||||
}
|
||||
|
||||
// (†) https://github.com/lz4/lz4/blob/5c4c1fb2354133e1f3b087a341576985f8114bd5/doc/lz4_Frame_format.md#frame-descriptor
|
||||
78
exercises/112_packed2.zig
Normal file
78
exercises/112_packed2.zig
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// We've already learned about switch statements in exercises 030, 031 and 108.
|
||||
// They also work with packed containers:
|
||||
|
||||
const S = packed struct(u2) {
|
||||
a: bool,
|
||||
b: i1,
|
||||
};
|
||||
|
||||
// Try to make it compile without adding an `else` prong!
|
||||
|
||||
comptime {
|
||||
const s: S = .{ .a = true, .b = -1 };
|
||||
switch (s) {
|
||||
.{ .a = true, .b = -1 } => {}, // ok!
|
||||
.{ .a = true, .b = ??? },
|
||||
.{ .a = ???, .b = 0 },
|
||||
.{ .a = ???, .b = ??? },
|
||||
=> @compileError("We don't want to end up here!"),
|
||||
}
|
||||
}
|
||||
|
||||
// As we can see, switching on packed structs is pretty straightforward.
|
||||
// When switching on packed unions however, we'll realize that a packed
|
||||
// union never keeps track of its active tag, not even in debug mode! This
|
||||
// means that packed unions compare solely by their bit pattern (again, just
|
||||
// like integers).
|
||||
|
||||
const U = packed union(u2) {
|
||||
a: u2,
|
||||
b: i2,
|
||||
};
|
||||
|
||||
// Find and remove the duplicate case!
|
||||
|
||||
comptime {
|
||||
const u: U = .{ .a = 3 };
|
||||
switch (u) {
|
||||
.{ .a = 3 } => {}, // ok!
|
||||
.{ .a = 2 },
|
||||
.{ .b = 1 },
|
||||
.{ .b = -1 },
|
||||
.{ .a = 0 },
|
||||
=> @compileError("We don't want to end up here!"),
|
||||
}
|
||||
}
|
||||
|
||||
// Since packed unions don't have the concept of an active tag, it's always legal
|
||||
// to access any of their fields. This can be useful to view the same data from
|
||||
// different perspectives seamlessly.
|
||||
//
|
||||
// Try to make the float below negative:
|
||||
|
||||
/// IEEE 754 half precision float
|
||||
const Float = packed union(u16) {
|
||||
value: f16,
|
||||
bits: packed struct(u16) {
|
||||
mantissa: u10,
|
||||
exponent: u5,
|
||||
sign: u1,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
// Reminder: if the sign bit of a float is set, the number is negative!
|
||||
|
||||
var number: Float = .{ .value = 2.34 };
|
||||
number.bits.??? = ???;
|
||||
if (number.value != -2.34) {
|
||||
std.debug.print("Make it negative!\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// This concludes our introduction to packed containers. The next time you need
|
||||
// control over individual bits, keep them in mind as a potent alternative!
|
||||
//
|
||||
|
||||
const std = @import("std");
|
||||
57
patches/patches/111_packed.patch
Normal file
57
patches/patches/111_packed.patch
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
--- exercises/111_packed.zig 2026-03-13 11:18:44
|
||||
+++ answers/111_packed.zig 2026-03-13 11:18:57
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
const PackedStruct = packed struct {
|
||||
a: u2,
|
||||
- b: u?,
|
||||
+ b: u4,
|
||||
};
|
||||
|
||||
comptime {
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
const PackedUnion = packed union {
|
||||
a: bool,
|
||||
- b: u?,
|
||||
+ b: u1,
|
||||
};
|
||||
|
||||
comptime {
|
||||
@@ -113,31 +113,31 @@
|
||||
pub fn main() void {
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b1000));
|
||||
- const my_bits: Bits = .{};
|
||||
+ const my_bits: Bits = .{ .d = 1 };
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0001));
|
||||
- const my_bits: Bits = .{};
|
||||
+ const my_bits: Bits = .{ .a = 1 };
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0010));
|
||||
- const my_bits: Bits = .{};
|
||||
+ const my_bits: Bits = .{ .b = 1 };
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b0011));
|
||||
- const my_bits: Bits = .{};
|
||||
+ const my_bits: Bits = .{ .a = 1, .b = 1 };
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
|
||||
{
|
||||
const expected: Bits = @bitCast(@as(u4, 0b1101));
|
||||
- const my_bits: Bits = .{};
|
||||
+ const my_bits: Bits = .{ .a = 1, .c = 1, .d = 1 };
|
||||
if (my_bits != expected) complain(my_bits, expected, @src());
|
||||
}
|
||||
}
|
||||
32
patches/patches/112_packed2.patch
Normal file
32
patches/patches/112_packed2.patch
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
--- exercises/112_packed2.zig 2026-03-13 11:14:08
|
||||
+++ answers/112_packed2.zig 2026-03-13 11:14:16
|
||||
@@ -13,9 +13,9 @@
|
||||
const s: S = .{ .a = true, .b = -1 };
|
||||
switch (s) {
|
||||
.{ .a = true, .b = -1 } => {}, // ok!
|
||||
- .{ .a = true, .b = ??? },
|
||||
- .{ .a = ???, .b = 0 },
|
||||
- .{ .a = ???, .b = ??? },
|
||||
+ .{ .a = true, .b = 0 },
|
||||
+ .{ .a = false, .b = 0 },
|
||||
+ .{ .a = false, .b = -1 },
|
||||
=> @compileError("We don't want to end up here!"),
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@
|
||||
.{ .a = 3 } => {}, // ok!
|
||||
.{ .a = 2 },
|
||||
.{ .b = 1 },
|
||||
- .{ .b = -1 },
|
||||
.{ .a = 0 },
|
||||
=> @compileError("We don't want to end up here!"),
|
||||
}
|
||||
@@ -65,7 +64,7 @@
|
||||
// Reminder: if the sign bit of a float is set, the number is negative!
|
||||
|
||||
var number: Float = .{ .value = 2.34 };
|
||||
- number.bits.??? = ???;
|
||||
+ number.bits.sign = 1;
|
||||
if (number.value != -2.34) {
|
||||
std.debug.print("Make it negative!\n", .{});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user