summaryrefslogtreecommitdiff
path: root/exercises/114_packed.zig
blob: 74ec7a17620082bed18712540860d5b128d79834 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//
// We've already learned plenty about bit manipulation using bitwise operations
// in exercises 100 and 101 and in quiz 113. 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 individual 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_independence: 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