|
| 1 | +--- |
| 2 | +author: "Stephen Gutekanst" |
| 3 | +title: "Packed structs in Zig make bit/flag sets trivial" |
| 4 | +date: "2022-08-29" |
| 5 | +draft: false |
| 6 | +categories: |
| 7 | +- zig |
| 8 | +- zigtips |
| 9 | +description: "As we've been building Mach engine, we've been using a neat little pattern in Zig that enables writing flag sets more nicely in Zig than in other languages. Here's a brief explainer." |
| 10 | +images: ["https://user-images.githubusercontent.com/3173176/184735519-cc78d19d-73e8-4914-8f3d-fc3a15d00bb7.png"] |
| 11 | +--- |
| 12 | + |
| 13 | +As we've been building [Mach engine](https://machengine.org/), we've been using a neat little pattern in Zig that enables writing flag sets more nicely in Zig than in other languages. |
| 14 | + |
| 15 | +## What is a flag set? |
| 16 | + |
| 17 | +We've been rewriting `mach/gpu` (WebGPU bindings for Zig) from scratch recently, so let's take a flag set from the WebGPU C API: |
| 18 | + |
| 19 | +```c |
| 20 | +typedef uint32_t WGPUFlags; |
| 21 | +typedef WGPUFlags WGPUColorWriteMaskFlags; |
| 22 | +``` |
| 23 | + |
| 24 | +Effectively, `WGPUColorWriteMaskFlags` here is a 32-bit unsigned integer where you can set specific bits in it to represent whether or not to write certain colors: |
| 25 | + |
| 26 | +```c |
| 27 | +typedef enum WGPUColorWriteMask { |
| 28 | + WGPUColorWriteMask_None = 0x00000000, |
| 29 | + WGPUColorWriteMask_Red = 0x00000001, |
| 30 | + WGPUColorWriteMask_Green = 0x00000002, |
| 31 | + WGPUColorWriteMask_Blue = 0x00000004, |
| 32 | + WGPUColorWriteMask_Alpha = 0x00000008, |
| 33 | + WGPUColorWriteMask_All = 0x0000000F, |
| 34 | + WGPUColorWriteMask_Force32 = 0x7FFFFFFF |
| 35 | +} WGPUColorWriteMask; |
| 36 | +``` |
| 37 | + |
| 38 | +Then to use it you'd use the various bit operations with those masks, e.g.: |
| 39 | + |
| 40 | +```c |
| 41 | +WGPUColorWriteMaskFlags mask = WGPUColorWriteMask_Red | WGPUColorWriteMask_Green; |
| 42 | +mask |= WGPUColorWriteMask_Blue; // set blue bit |
| 43 | +``` |
| 44 | + |
| 45 | +This all works, people have been doing it for years in C, C++, Java, Rust, and more. In Zig, we can do better. |
| 46 | + |
| 47 | +## Zig packed structs |
| 48 | + |
| 49 | +<img class="color-auto" style="max-height: 300px;" src="https://user-images.githubusercontent.com/3173176/184735519-cc78d19d-73e8-4914-8f3d-fc3a15d00bb7.png" /> |
| 50 | + |
| 51 | +Zig has `packed struct`s: these let us pack memory tightly, where a `bool` is actually a single bit (in most other languages, this is not true.) Zig also has arbitrary bit-width integers, like `u28`, `u1` and so on. |
| 52 | + |
| 53 | +We can write `WGPUColorWriteMaskFlags` from earlier in Zig using: |
| 54 | + |
| 55 | +```zig |
| 56 | +pub const ColorWriteMaskFlags = packed struct { |
| 57 | + red: bool = false, |
| 58 | + green: bool = false, |
| 59 | + blue: bool = false, |
| 60 | + alpha: bool = false, |
| 61 | +
|
| 62 | + _padding: u28 = 0, |
| 63 | +}; |
| 64 | +``` |
| 65 | + |
| 66 | +This is still just 32 bits of memory, and so can be passed to the same C APIs that expect a `WGPUColorWriteMaskFlags` - but interacting with it is much nicer: |
| 67 | + |
| 68 | +```zig |
| 69 | +var mask = ColorWriteMaskFlags{.red = true, .green = true}; |
| 70 | +mask.blue = true; // set blue bit |
| 71 | +``` |
| 72 | + |
| 73 | +In C you would need to write code like this: |
| 74 | + |
| 75 | +```c |
| 76 | +if (mask & WGPUColorWriteMask_Alpha) { |
| 77 | + // alpha is set.. |
| 78 | +} |
| 79 | +if (mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) { |
| 80 | + // alpha and blue are set.. |
| 81 | +} |
| 82 | +if ((mask & WGPUColorWriteMask_Green) == 0) { |
| 83 | + // green not set |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +In Zig it's just: |
| 88 | + |
| 89 | +```zig |
| 90 | +if (mask.alpha) { |
| 91 | + // alpha is set.. |
| 92 | +} |
| 93 | +if (mask.alpha and mask.blue) { |
| 94 | + // alpha is set.. |
| 95 | +} |
| 96 | +if (!mask.green) { |
| 97 | + // green not set |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +## Comptime validation |
| 102 | + |
| 103 | +Making sure that our `ColorWriteMaskFlags` ends up being the same size could be a bit tricky: what if we count the number of `bool` wrong? Or what if we accidently get the padding size wrong? Then it might not be the same size as a `uint32` anymore. |
| 104 | + |
| 105 | +Luckily, we can verify our expectations at comptime: |
| 106 | + |
| 107 | +```zig |
| 108 | +pub const ColorWriteMaskFlags = packed struct { |
| 109 | + red: bool = false, |
| 110 | + green: bool = false, |
| 111 | + blue: bool = false, |
| 112 | + alpha: bool = false, |
| 113 | +
|
| 114 | + _padding: u28 = 0, |
| 115 | +
|
| 116 | + comptime { |
| 117 | + std.debug.assert(@sizeOf(@This()) == @sizeOf(u32)); |
| 118 | + std.debug.assert(@bitSizeOf(@This()) == @bitSizeOf(u32)); |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +The Zig compiler will take care of running the `comptime` code block here for us when building, and it will verify that the byte size of `@This()` (the type we're inside of, the `ColorWriteMaskFlags` struct in this case) matches the `@sizeOf(u32)`. |
| 124 | + |
| 125 | +Similarly we could check the `@bitSizeOf` both types if we like. |
| 126 | + |
| 127 | +Note that [`@sizeOf`](https://ziglang.org/documentation/master/#sizeOf) may include the size of padding for more complex types, while [`@bitSizeOf`](https://ziglang.org/documentation/master/#bitSizeOf) returns the number of bits it takes to store `T` in memory _if the type were a field in a packed struct/union_. For flag sets like this, it doesn't matter and either will do. For more complex types, be sure to recall this. |
| 128 | + |
| 129 | + |
| 130 | +## Thanks for reading |
| 131 | + |
| 132 | +<img align="left" style="max-height: 150px;" src="https://user-images.githubusercontent.com/3173176/187348488-0b52e87d-3a48-421c-9402-be78e32b5a20.png"></img> |
| 133 | +Be sure to join the new [Mach engine Discord server](https://discord.gg/XNG3NZgCqp) where we're building the future of Zig game development. |
| 134 | +<br><br> |
| 135 | +You can also [sponsor my work](https://github.com/sponsors/slimsag) if you like what I'm doing! :) |
0 commit comments