mirror of
https://codeberg.org/ziglings/exercises.git
synced 2026-03-28 21:34:52 +00:00
Merge pull request 'Add exercises for packed structs/unions' (#374) from justusk/ziglings:packed into main
Reviewed-on: https://codeberg.org/ziglings/exercises/pulls/374
This commit is contained in:
commit
e6d93d731a
10
build.zig
10
build.zig
|
|
@ -265,7 +265,7 @@ pub fn build(b: *Build) !void {
|
|||
|
||||
const progress_file_size = try progress_file.length(io);
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
var gpa = std.heap.DebugAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
const contents = try allocator.alloc(u8, progress_file_size);
|
||||
|
|
@ -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