openmymind.netProgramming blog exploring Zig, Elixir, Go, Testing, Design and Performance
"https://www.openmymind.net/atom.xml" rel="self"/>
2024-03-15T00:00:00Zhttps://www.openmymind.net/Karl SeguinZig: Use The Heap to Know A Value's Address2024-03-15T00:00:00Z/Zig-Use-The-Heap-To-Know-A-Values-Address/<p>You'll often find yourself wanting to know the address of a newly created value which gets returned from a function. This can happen for a number of reasons, but the most common is creating bidirectional references. For example, if we create a pool of objects, we often want those objects to reference the pool. You might end up with something like:</p><pre><code>
fn init(allocator: Allocator, count: usize) !Pool {
var conns = try allocator.alloc(count, Conn);
errdefer allocator.free(conns);
var pool = Pool{
.conns = conns;
}
for (0..count) |i| {
// This won't work
conns[i] = Conn.open(&pool);
}
return pool;
}
fn deinit(self: *const Pool, allocator: Allocator) void {
allocator.free(self.conns);
}</code></pre>
<p>The above skeleton is almost certainly not what you want and would segfault if used as part of a larger program. When we call <code>Call.open(&pool)</code>, we're passing the address of the <code>pool</code> value on the stack. When we return <code>pool</code>, we're returning a copy. Thus, whoever called <code>init</code> gets a copy which will have its own address.</p>
<p>Taking the address of a stack variable and using it beyond the lifetime of the stack is a common error. This error manifests itself in different scenarios but when the problem you're trying to solve is <em>I need to know the address of X (<code>pool</code> in the above case)</em>, you have two options.</p>
<p>The first is to make this the callers problem. Our <code>init</code> would be changed to take a <code>*Pool</code>:</p><pre><code>
fn init(allocator: Allocator, count: usize, pool: *Pool) !void {
var conns = try allocator.alloc(count, Conn);
errdefer allocator.free(conns);
for (0..count) |i| {
// This won't work
conns[i] = Conn.open(pool);
}
pool.conns = conns;
}</code></pre>
<p>You see this pattern a lot in C libraries and you'll see it now and again in Zig code too.</p>
<p>Your other, probably more common, option is to put <code>pool</code> on the heap:</p><pre><code>
fn init(allocator: Allocator, count: usize) !*Pool {
const pool = try allocator.create(Pool);
errdefer allocator.destroy(pool);
var conns = try allocator.alloc(count, Conn);
errdefer allocator.free(conns);
pool.* = .{
.conns = conns,
};
for (0..count) |i| {
// This won't work
conns[i] = Conn.open(pool);
}
return pool;
}
fn deinit(self: *Pool, allocator: Allocator) void {
allocator.free(self.conns);
// cannot use self after this!
allocator.destroy(self);
}</code></pre>
<p>Notice that our function returns a <code>*Pool</code> instead of <code>Pool</code>. Also notice that to create our array of <code>conns</code>, we're using <code>allocator.alloc</code>, but to create <code>pool</code>, we're using <code>allocator.create</code>. And, related, to free <code>conns</code> we're using <code>allocator.free</code>, but to free <code>pool</code>, we're using <code>allocator.destroy</code>. This tripped me up when I started to learn Zig, but you get used to it. <code>alloc/free</code> is to create and free an array of items, whereas <code>create/destroy</code> is to create and free a single item.</p>
<p>Generally speaking, it's obvious whether a value can stay on the stack or has to be pushed onto the heap. The point of this post is to say: it's ok to create a value on the heap. You will find yourself in situations where you need to know the address of a value but don't know its final resting place (because you're returning the value to the caller and that, as far as your function is concerned, is its "owner"). When that happens, a reasonable (and in many cases only) option is to make the "final resting place", the heap. Just make sure to <code>destroy</code> the created item when its no longer needed.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Being Mindful of Memory Allocations2024-02-20T00:00:00Z/Being-Mindful-of-Memory-Allocations/<p>One consequence of programming without a garbage collector is that you're generally more aware of every memory allocation your program makes. Ideally, this increased awareness results in more prudent memory use. Developers often lament that while hardware has gotten faster, computers feel more sluggish. I don't know how true this is, and if it is true, I don't know how big a part memory allocation plays.</p>
<p>What I do know is that my time in Zig has fundamentally shaped how I look at code and hopefully has impacted my non-Zig coding. As an example, I recently wrote a <a href="https://github.com/karlseguin/pg.zig">PostgreSQL client in Zig</a>. In PostgreSQL, the flow for sending a parameterized query usually involves sending the SQL to PostgreSQL along with a request to "describe" the statement. Part of the response from PostgreSQL is a message that contains the 32 bit integer object id (oid) of each parameter. Libraries can use this information to figure out how to serialize the values given to them by the application.</p>
<p>If we look at <a href="https://github.com/jackc/pgconn/blob/e82f7d1fadf5970c308d0502d196783e72467178/pgconn.go#L849">Go's pgx handling of this</a>, we find:</p><pre><code>
case *pgproto3.ParameterDescription:
psd.ParamOIDs = make([]uint32, len(msg.ParameterOIDs))
copy(psd.ParamOIDs, msg.ParameterOIDs)</code></pre>
<p>Your average query might have 2-4 parameters, so we're talking about a very small amount of memory. But this is just one of many allocations made when querying, all of which has GC overhead.</p>
<p>In pg.zig, every connection has a <code>param_oids: []i32</code> field, the size of which is configurable. The code to handle the parameter description looks like:</p><pre><code>
var param_oids = conn.param_oids;
if (param_count > param_oids.len) {
param_oids = try allocator.alloc(i32, param_count);
}
var pos: usize = 0;
for (0..param_count) |i| {
const end = pos + 4;
param_oids[i] = std.mem.readInt(i32, data[0..end][0..4], .big);
pos = end;
}</code></pre>
<p>In short, if the number of parameters fits in our pre-allocated <code>conn.param_oids</code>, we'll use that, else we'll allocate a new larger <code>[]i32</code> to fit the number of parameters. If we do allocate a larger <code>param_oids</code> we'll have to clean it up once our query is done. In this specific case, it's easily handled by the fact that <code>allocator</code> comes from an <code>ArenaAllocator</code> that exists until the query result is freed (when the <code>ArenaAllocator</code> is freed, any allocation made with it are also freed). We could take this a step further and continue re-using the larger <code>param_oids</code>, but that would probably be a surprising behavior.</p>
<p>I've made a few contributions to pgx in the past and consider it a solid library. I used it as a reference when implementing pg.zig and I'm sure it has a number of optimizations that I haven't done or thought of. I don't know the current codebase well enough to say if it's possible to re-use a pre-allocated <code>paramOids</code>. But it's easy to skip these optimizations, especially when languages and runtimes (e.g. garbage collector) hide so much.</p>
<p>For better or worse, I think the age of mindful allocation is behind us. Re-using memory is hard. While some might point to extra edge cases or security consideration, I think the overwhelming driver is developer laziness - which many have called a good thing. One of the things I like about Zig and its lack of an established ecosystem is that I can fret about unnecessarily allocating 64 bytes of data if I want to.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Compile-Time Configuration For Zig Libraries2024-02-05T00:00:00Z/Compile-Time-Configuration-For-Zig-Libraries/<p>If you're writing a Zig library, you might find yourself wishing to expose a compile-time configuration option to application developers. One of the reasons you might want to do this for performance reasons, preferring to do something at compile-time versus runtime. Consider this example using my <a href="https://github.com/karlseguin/pg.zig">PostgreSQL library</a>:</p><pre><code>
var result = try pool.query("select id, name from users where power > $1", .{9000});
defer result.deinit();
while (try result.next()) |row| {
const id = row.get(i32, 0);
const name = row.get([]u8, 1);
// ...
}</code></pre>
<p>When I wrote the library, I was unsure what level of runtime check I should add to <code>row.get</code>. My instinct was to add none. As the developer, you know that column 0 is an <code>integer not null</code> and column 1 is a <code>text not null</code>. Is it <em>really</em> worth adding an additional type and nullability check? But I wasn't sure if that was the right assumption to make.</p>
<p>One option would be to have two different <code>get</code> methods, one safe and one not. But that's a more confusing API and isn't always straightforward to implement without duplication (not all checks can be done at the top of the function as a guard clause). Another option would be to use <code>std.debug.assert</code>, whose behavior is controlled by Zig's optimization flag (e.g. <code>ReleaseFast</code>). But this is a blunt tool affecting all code. I myself want to run some code in <code>RelaseSafe</code>, but not have these specific PostgreSQL checks.</p>
<p>Zig offers two solutions. The first is to use global declarations in the root source file. The "root source file" is the program's main entry point (where the <code>pub main() void</code> function is located). By convention this is the application's "main.zig", but a developer can use any file name. The point is that it's controlled by the application developer, but can be accessed by a library developer using <code>@import("root")</code>. In our library code, we could do:</p><pre><code>
const root = @import("root");
const assert_enabled = if (@hasDecl(root, "pg_assert")) root.pg_assert else true;
pub fn assert(ok: bool) void {
if (comptime assert_enabled) {
std.debug.assert(ok);
} else {
unreachable;
}
}</code></pre>
<p>Having our own <code>assert</code> function based on our own comptime configuration allows the application to run the whole of the application <code>ReleaseSafe</code> while excluding our library's assertion. To do so, the application developer merely needs to add the following to their <code>main.zig</code>:</p><pre><code>
pub const pg_assert = false;</code></pre>
<p>Zig's standard library uses this approach in a few places. For example, you might have done something like this to change the standard library's log level:</p><pre><code>
pub const std_options = struct {
pub const log_level = .debug;
};</code></pre>
<p>There's a second option available through Zig's build system. As a library developer, the code is almost the same. Rather than importing "root", we import "config":</p><pre><code>
const config = @import("config");
const assert_enabled = if (@hasDecl(config, "assert")) config.pg_assert else true;
// ...</code></pre>
<p>Application developers must define the options in their <code>build.zig</code>:</p><pre><code>
const pg = b.dependency("pg", dep_opts).module("pg");
const options = b.addOptions();
options.addOption(bool, "assert", true);
pg.addOptions("config", options);
// add the pg module to their program as they normally would</code></pre>
<p>I believe the build-approach is newer and might be intended as the way forward. It is better scoped, i.e. it doesn't rely on library using distinct variable names like "pg_assert", and setting compile-time flags in the build script feels more cohesive than a bunch of globals in the root source file. Whichever approach you take, remember that compile-time configuration is less flexible for application developers since they now have to provide configuration at compile-time that, maybe in some cases, they rather do at runtime. So try to use this sparingly, if at all.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Why is Simple Code So Often Bad?2024-01-30T00:00:00Z/Why-Is-Simple-Code-So-Often-Bad/
<p>I recently wrote a <a href="https://github.com/karlseguin/metrics.zig">Prometheus client library for Zig</a>. I had to write a custom hash map context (what Zig calls the <code>eql</code> and <code>hash</code> functions) which led to my last couple blog posts exploring Zig's hash maps in more details. The last topic covered was the <code>getOrPut</code> method which returns a pointer to the key and value arrays of where the entry is or should be. <code>getOrPut</code> is great because you can do an upsert operation with a single lookup (but, like all things in GC-free language, don't hold on to that reference else you risk a dangling pointer).</p>
<p>While writing the library, I used the Go client as a reference. I also looked through a Node.js implementation. There I saw:</p><pre><code>
updateExemplar(exemplarLabels, value, hash) {
if (!isObject(this.hashMap[hash].exemplar)) {
this.hashMap[hash].exemplar = new Exemplar();
}
this.hashMap[hash].exemplar.validateExemplarLabelSet(exemplarLabels);
this.hashMap[hash].exemplar.labelSet = exemplarLabels;
this.hashMap[hash].exemplar.value = value ? value : 1;
this.hashMap[hash].exemplar.timestamp = nowTimestamp();
}</code></pre>
<p>To me, this code is problematic for two reasons. While I know nothing about V8 optimization, I assume 5 or 6 hash lookups is going to be slower than 1 or 2. The other issue is that, as it's written, you need to parse each line to understand that a single object is being mutated. A local would solve both issues:</p><pre><code>
updateExemplar(exemplarLabels, value, hash) {
let exemplar = this.hashMap[hash].exemplar;
if (!isObject(exemplar)) {
exemplar = new Exemplar();
this.hashMap[hash].exemplar = exemplar;
}
exemplar.validateExemplarLabelSet(exemplarLabels);
exemplar.labelSet = exemplarLabels;
exemplar.value = value ? value : 1;
exemplar.timestamp = nowTimestamp();
}</code></pre>
<p>The original code isn't <em>awful</em>. But it <strong>is</strong> a simple function and it seems to almost go out of its way to be inefficient to run, read and maintain. It honestly makes me wonder how it happens. I wonder how often I've done something like it, or worse, without realizing. I just finished writing a multipart/form-data parser, and it's <em>awful</em>. I don't mean to pick on this one example, even though I know I am. It just happens to be the most recent example I've seen. The real issue is how commonplace this is.</p>
<p>I'm skeptical about our ability to improve, and wonder how close we are to hitting a limit on the complexity of software that can be written and maintained?</p>
<p><a href="#new_comment">Leave a comment</a></p>
Zig's HashMap - Part 22024-01-22T00:00:00Z/Zigs-HashMap-Part-2/
<p>In <a href="https://www.openmymind.net/Zigs-HashMap-Part-1/">part 1</a> we explored how the six HashMap variants relate to each other and what each offered to developers. We largely focused on defining and initializing HashMaps for various data type and utilizing custom <code>hash</code> and <code>eql</code> functions for types not supported by the <code>StringHashMap</code> or <code>AutoHashMap</code>. In this part we'll focus on keys and values, how they're stored, exposed and our responsibility with respect to their lifetime.</p>
<p>Loosely speaking, Zig's hash map are implemented using two slices: one for keys and one for values. The hash code returned from the <code>hash</code> function is used to find the ideal index of an entry within these arrays. To start simply, if we had this code:</p><pre><code>
var lookup = std.StringHashMap(i32).init(allocator);
defer lookup.deinit();
try lookup.put("Goku", 9001);
try lookup.put("Paul", 1234);</code></pre>
<p>We could visualize our hash map like so:</p><pre><code>
keys: values:
-------- --------
| Paul | | 1234 | @mod(hash("Paul"), 5) == 0
-------- --------
| | | |
-------- --------
| | | |
-------- --------
| Goku | | 9001 | @mod(hash("Goku"), 5) == 3
-------- --------
| | | |
-------- --------</code></pre>
<p>When we <code>hash</code> our keys and apply a modulo operation with the length of our array (5 above), we get the ideal index for the entry. I say "ideal" because our <code>hash</code> function can return the same hash code for two different keys; the chance of such collisions increase dramatically when we map the hash code to one of 5 available slots via <code>@mod</code>. But if we ignore possible collisions, this is a reasonable view of our hash map.</p>
<p>Once our hash map fills up to a certain point (in part 1, we briefly talked about the fill factor and mentioned that Zig defaults to 80%), it needs to grow to accommodate new values and to maintain the constant-time performance of lookups. Growing a hash map is like growing a dynamic array, we allocate a new array and copy the values from the original to the new (a simple algorithm would be making it 2x larger). For this to work with a hash map though, we can't simply copy the keys and values to the same index of the new slices. We need to re-calculate their index. Why? Because the location of an entry has to be consistent and predictable. We can't insert a key-value pair using one algorithm, e.g. <code>@mod(hash("Goku"), 5)</code>, and expect to find it using a different one, e.g. <code>@mod(hash("Goku"), 10)</code> (notice the 5 changed to 10, because our array grew).</p>
<p>This basic visualization is going to serve as a foundation for much of this post. Further, the fact that entries can be moved from one backing array to another (i.e. when the hash map fills up and needs to grow) is also something we'll keep revisiting.</p>
<h3>Values</h3>
<p>If we extend the above snippet and call <code>lookup.get("Paul")</code>, the return value will be <code>1234</code>. The return type of <code>get</code> is a <code>?i32</code>, or more generically, <code>?V</code>. The optional return type allows <code>get</code> to inform us that the key wasn't found. If you've looked through Zig's documentation, you've probably noticed another similar method: <code>getPtr(key)</code> with a slightly different return type: <code>?*V</code>.</p>
<p>Since it's difficult to appreciate the difference between the two methods when talking about primitive types like our <code>i32</code>, consider this version which swaps our <code>i32</code> value for a <code>User</code>:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(User).init(allocator);
defer lookup.deinit();
try lookup.put("Goku", .{
.id = 9000,
.name = "Goku",
.super = false,
});
var user = lookup.get("Goku").?;
user.super = true;
std.debug.print("{any}\n", .{lookup.get("Goku").?.super});
}
const User = struct {
id: i32,
name: []const u8,
super: bool,
};</code></pre>
<p>Even though we set <code>user.super = true</code>, the value of the <code>User</code> in <code>lookup</code> is still <code>false</code>. This is because, in Zig, assignment are done by copy. If we keep the code as-is, but change <code>lookup.get</code> to <code>lookup.getPtr</code>, it'll work. We're still doing an assignment, thus still copying a value, but the value we're copying is the address of the <code>User</code> in our hash map, not the user itself.</p>
<p><code>getPtr</code> allows us to get a reference to the value within the hash map. As we see above, this has behavioral significance; we can directly modify the value stored in our hash map. It also has a performance implication as copying large values can be expensive. But consider our above visualization and remember that, as the hash table fills up, values can be relocated. With that in mind, can you explain why this code crashes?:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// change the type, just to make it easier to write this snippet
// the same would happen with our above StringHashMap(User)
var lookup = std.AutoHashMap(usize, usize).init(allocator);
defer lookup.deinit();
try lookup.put(100, 100);
const first = lookup.getPtr(100).?;
for (0..50) |i| {
try lookup.put(i, i);
}
first.* = 200;
}</code></pre>
<p>If the <code>first.* = 200;</code> syntax is a confusing, we're dereferencing the pointer and writing a value to it. Our pointer is the address of an index in our values array, so what this syntax does is write directly into our array. It crashes because our inserting-loop has forced the hash table to grow, causing the underlying key and values to be re-allocated and all keys and values to be moved. The pointer returned by <code>getPtr</code> is no longer valid. At the time of this writing, the default hash map size is 8 with a fill factor of 80%. If we looped <code>0..5</code> the code would work, but one more iteration (<code>0..6</code>) causes the growth and thus our crash. With typical usage, this issue isn't normally a problem; you won't hold a reference to an entry while modifying the hash map. But understanding that it can happen and understanding why it happens will help us better utilize other hash map features that return value and key pointers.</p>
<p>Going back to our <code>User</code> example, what if we changed the type of <code>lookup</code> from <code>std.StringHashMap(User)</code> to <code>std.StringHashMap(*User)</code>? The biggest impact will be with respect to the lifetime of our values. With our original <code>std.StringHashMap(User)</code>, we could say that the lookup <em>owns</em> the values - the users we insert are embedded with the hash map's value array. This makes lifetime management easy, when we <code>deinit</code> our <code>lookup</code> the backing keys and values arrays are freed.</p>
<aside><p>Our <code>User</code> has a <code>name: []const u8</code> field. Our examples use a string literal, which statically exist throughout the lifetime of the program. If our name was dynamically allocated though, we would have to free it explicitly. We'll cover that as we explore pointer values in more detail.</p></aside>
<p>Using a <code>*User</code> breaks that ownership. Our hash map stores pointers, but it doesn't own what they point to. Despite the call to <code>lookup.deinit</code>, this code leaks the user:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(*User).init(allocator);
defer lookup.deinit();
const goku = try allocator.create(User);
goku.* = .{
.id = 9000,
.name = "Goku",
.super = false,
};
try lookup.put("Goku", goku);
}
const User = struct {
id: i32,
name: []const u8,
super: bool,
};</code></pre>
<p>Let's visualize it:</p><pre><code>
lookup
===============================
║ keys: values: ║
║ -------- ------------- ║
║ | Goku* | | 1024d4000 | ----> -------------
║ -------- ------------- ║ | 9000 |
║ | | | | ║ -------------
║ -------- ------------- ║ | 1047300e4 |---> -----------------
=============================== ------------- | G | o | k | u |
| 4 | -----------------
-------------
| false |
-------------</code></pre>
<aside><p>We'll talk about keys in the next section, for now we use "Goku" for simplicity.</p></aside>
<p>The double-lined box is our lookup, and represents the memory it owns and is responsible for. References we put in our hash map will point to values outside that box. This has a number of implications. Most importantly though, it means the lifetime of the values is detached from the lifetime of the hash map, and calling <code>lookup.deinit</code> won't free them.</p>
<p>There is a common case where we want to use pointers <strong>and</strong> associate the lifetime of the value with the hash map. Recall our crashing program, when a pointer to our hash map value became invalid. As I said, that isn't normally a problem, but in more advanced scenarios, you might want to have different parts of code referencing a value that also exists in a hash map. Let's re-examine the above visualization and think about what happens if our hash map grows and relocates the keys and values arrays:</p><pre><code>
lookup
===============================
║ keys: values: ║
║ -------- ------------- ║
║ | | | | ║
║ -------- ------------- ║
║ -------- ------------- ║
║ | | | | ║
║ -------- ------------- ║
║ -------- ------------- ║
║ | Goku* | | 1024d4000 | ----> -------------
║ -------- ------------- ║ | 9000 |
║ | | | | ║ -------------
║ -------- ------------- ║ | 1047300e4 |---> -----------------
=============================== ------------- | G | o | k | u |
| 4 | -----------------
-------------
| false |
-------------</code></pre>
<p>The two arrays have grown, been reallocated and our entry indexes have been re-calculated, but our actual <code>User</code> still resides at the same place in the heap (memory location 1047300e4). Just like <code>deinit</code> doesn't alter anything outside our double-lined boxes, other changes such as growth, won't alter them.</p>
<p>Generally speaking, it'll be obvious if you should be storing values or pointer to values. This is largely because methods like <code>getPtr</code> make it possible to efficiently retrieve and modify values directly from our hash map. Either way, we can have our performance cake, so performance isn't the main consideration. What does matter is whether values need to outlive the hash map and/or whether references to values need to exist (and thus remain valid) while the hash map undergoes changes.</p>
<p>In cases where where the hash map and the referenced values should be linked, we need to iterate through the values and clean them up before we call <code>lookup.deinit</code>:</p><pre><code>
defer {
var it = lookup.valueIterator();
while (it.next()) |value_ptr| {
allocator.destroy(value_ptr.*);
}
lookup.deinit();
}</code></pre>
<p>If the dereferencing (<code>value_ptr.*</code>) doesn't seem right, go back to the visualization. Our <code>valueIterator</code> is giving us a pointer to the value in the array, and the value in the array is a <code>*User</code>. Thus, <code>value_ptr</code> is a <code>**User</code>.</p>
<p>Whether we're storing a <code>User</code> or a <code>*User</code>, any allocated fields within the value are always our responsibility. In a real application, your user's <code>name</code> wouldn't be string literals, they would be dynamically allocated. In that case, our <code>while</code> loop above would have to change to:</p><pre><code>
while (it.next()) |value_ptr| {
const user = value_ptr.*;
allocator.free(user.name);
allocator.destroy(user);
}</code></pre>
<p>Even if our value is a <code>User</code>, its fields are our responsibility (it's a bit silly to think <code>lookup.deinit</code> would know how/what needs to be freed anyways):</p><pre><code>
while (it.next()) |value_ptr|
allocator.free(value_ptr.name);
}</code></pre>
<p>In this last case, since we're storing <code>User</code>, our <code>value_ptr</code> is a <code>*User</code> (a pointer to a <code>User</code>, not a pointer to a pointer to a <code>User</code> as before).</p>
<h3>Keys</h3>
<p>We could start and end this section by saying: everything we said about values applies equally to keys. That is 100% true, but it's somehow less intuitive. Most developers quickly understand that a heap-allocated <code>User</code> stored within a hash map has its own lifetime and need to be explicitly managed/freed. But for some reason, it isn't quite so obvious with keys.</p>
<p>Like values, if our keys are primitive types we don't have to do anything special. A key such as an integer is stored directly within our hash map's key array and thus has its lifetime and memory tied to the hash map. This is a very common case. But another common case is using string keys with the <code>std.StringHashMap</code>. This often trips up developers new to Zig, but you need to guarantee that string keys are valid for as long as they're used by the hash map. And, if they're dynamically allocated, you need to make sure they're freed when no longer used. This means doing for keys exactly what we did for values.</p>
<p>Let's visualize our hash map again, but this time properly represent a string key:</p><pre><code>
lookup
===================================
║ keys: values: ║
║ ------------- ------------ ║
║ | 1047300e4 | | 1024d4000 | ----> -------------
║ ------------- ------------- ║ | 9000 |
║ | | | | ║ -------------
║ ------------- ------------- ║ | 1047300e4 |---> -----------------
=================================== ------------- | G | o | k | u |
| 4 | -----------------
-------------
| false |
-------------</code></pre>
<p>In this example, our key is actually the <code>user.name</code>. Having the key be part of the value is pretty common. Here's what that might look like:</p><pre><code>
const user = try allocator.create(User);
user.* = . {
.id = 9000,
.super = false,
// simulate a name that comes from a dynamic source, like a DB
.name = try allocator.dupe(u8, "Goku"),
};
try lookup.put(user.name, user);</code></pre>
<p>In this case, our previous cleanup code is sufficient since we were already freeing <code>user.name</code> which is our key:</p><pre><code>
defer {
var it = lookup.iterator();
while (it.next()) |value_ptr| {
const user = value_ptr.*;
allocator.free(user.name);
allocator.destroy(user);
}
lookup.deinit();
}</code></pre>
<p>But for cases where the key isn't part of the value, we need to iterate and free the keys. In many cases, you'll want to iterate both the keys and values and free both. We can simulate this by freeing the name as referenced by the key instead of the user:</p><pre><code>
defer {
var it = lookup.iterator();
while (it.next()) |kv| {
// This..
allocator.free(kv.key_ptr.*);
// Is the same as the following, but only because user.name is our key
// allocator.free(user.name);
allocator.destroy(kv.value_ptr.*);
}
lookup.deinit();
}</code></pre>
<p>Instead of <code>iteratorValue()</code> we're using <code>iterator()</code> to get access to both a <code>key_ptr</code> and <code>value_ptr</code>.</p>
<p>The last thing to consider is how to remove values from our lookup. This code leaks both the name/key and the heap-allocated user <code>User</code> despite using our improved cleanup logic:</p><pre><code>
var lookup = std.StringHashMap(*User).init(allocator);
defer {
var it = lookup.iterator();
while (it.next()) |kv| {
allocator.free(kv.key_ptr.*);
allocator.destroy(kv.value_ptr.*);
}
lookup.deinit();
}
const user = try allocator.create(User);
user.* = . {
.id = 9000,
.super = false,
// simulate a name that comes from a dynamic source, like a DB
.name = try allocator.dupe(u8, "Goku"),
};
try lookup.put(user.name, user);
// We added this!
_ = lookup.remove(user.name);</code></pre>
<p>The last line removes the entry from our hash map, so our cleanup routine no longer iterates over it and don't free the <code>name</code> or <code>user</code>. We need to use <code>fetchRemove</code> instead of <code>remove</code> to get the key and value that was removed:</p><pre><code>
if (lookup.fetchRemove(user.name)) |kv| {
allocator.free(kv.key);
allocator.destroy(kv.value);
}</code></pre>
<p><code>fetchRemove</code> doesn't return key and value pointers, it returns the actual key and value. That doesn't change how we use it, but it should be obvious why the key and value are returned as opposed to a pointer to the key and value. With the items removed from the hash map, there is no valid pointer to the keys and values within the hash map - they've been removed.</p>
<p>All of this assumes that your values and keys need to be freed/invalidated when removed from the hash map. There can be cases where the lifetimes of your values (and more rarely your keys) are completely unrelated to their presence within the hash map. In those cases, you need to free the memory when it makes sense for your application to do so. There's no general pattern/guidance that applies.</p>
<p>For most cases, when dealing with non primitive keys or values, the takeway is that when you call <code>deinit</code> on your hash map, any allocations you made for your keys and/or values won't be magically freed; you'll need to do that yourself.</p>
<h3>getOrPut</h3>
<p>While there are a number of implications to what we've talked about so far, for me, one of the best benefits that comes from exposing key and value pointers directly is the <code>getOrPut</code> method.</p>
<p>If I asked you to store a named counters in a map in Go, or most languages, you'd end up with something like:</p><pre><code>
count, exists := counters[name]
if exists == false {
counters[name] = 1
} else {
counters[name] = count + 1;
}</code></pre>
<p>This code requires two lookups. While we've been trained not to look beyond the fact that hash map access is O(1), the reality is that fewer operations are faster than more, calculating the hash code isn't the cheapest operation (and its performance varies depending on the key type and length), and collisions add overhead to the entire process. The <code>getOrPut</code> method solves this problem by returning a value pointer and a boolean indicating whether or not the value was found.</p>
<p>In other words, with <code>getOrPut</code> we either get a pointer to the found value, or we get a pointer to where the item should go. We also get a boolean indicating which of the two scenarios e have. sThis allows the above kind of upsert to be written with a single lookup:</p><pre><code>
const gop = try counters.getOrPut(name);
if (gop.found_existing) {
gop.value_ptr.* += 1;
} else {
gop.value_ptr.* = 1;
}</code></pre>
<p>Of course, like any other value or key pointer, <code>value_ptr</code> should only be considered valid so long as no changes to the hash map is made. This, by the way, also applied to the iterated keys and values we get from <code>iterator()</code>, <code>valueIterator</code> and <code>keyIterator()</code>, for the same reason.</p>
<h3>Conclusion</h3>
<p>Hopefully you're now more comfortable with using <code>std.HashMap</code>, <code>std.AutoHashMap</code> and <code>std.StringHashMap</code> and, if appropriate, their unmanaged variants. While you might never have to provide your own context (<code>hash</code> and <code>eql</code> function), it's good to know that it's an option. For day to day programming, I find it immensely useful to visualize data, particularly when pointers are used and levels of indirection are added. Whenever I'm dealing with a <code>value_ptr</code> or <code>key_ptr</code>, I think of those two slices and the difference between the value or key and the address of the value or key in those slices.</p>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/Zigs-HashMap-Part-1/">part 1</a>
<span></span>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Zig's HashMap - Part 12024-01-16T00:00:00Z/Zigs-HashMap-Part-1/
<aside><p>This blog posts assumes that you're comfortable with <a href="https://www.openmymind.net/learning_zig/generics/">Zig's implementation of Generics</a>.</p></aside>
<p>Like most hash map implementations, Zig's <code>std.HashMap</code> relies on 2 functions, <code>hash(key: K) u64</code> and <code>eql(key_a: K, key_b: K) bool</code>. The <code>hash</code> function takes a key and returns an unsigned 64 bit integer, known as the <em>hash code</em>. The same key always returns the same hash code. The <code>hash</code> function <em>can</em> produce the same hash code for two different keys which is one reason we also need <code>eql</code>: to determine if two keys are the same.</p>
<p>This is all standard stuff, but Zig's implementation has a few specific details worth exploring. This is particularly true given the number of hash map types in the standard library as well as the fact that the documentation seems incomplete and confusing. Specifically, there are six hash map variants: <code>std.HashMap</code>, <code>std.HashMapUnmanaged</code>, <code>std.AutoHashMap</code>, <code>std.AutoHashMapUnmanaged</code>, <code>std.StringHashMap</code>, <code>std.StringHashMapUnmanaged</code>. One of those, <code>std.HashMapUnmanaged</code>, contains the bulk of the implementation. The other five are thin wrappers: they are composed of an <code>std.HashMapUnmanaged</code>. The documentation for those five variants is challenging because of this composition, which is not, in my opinion, handled well by the document generator.</p>
<aside><p>There's also a completely different <code>ArrayHashMap</code> which has different properties (e.g. preserving insertion order), which we won't be covering.</p></aside>
<p>If we look at the <code>put</code> method of <code>std.HashMap</code>, we'll see a pattern that's often repeated:</p><pre><code>
pub fn put(self: *Self, key: K, value: V) Allocator.Error!void {
return self.unmanaged.putContext(self.allocator, key, value, self.ctx);
}</code></pre>
<p>As I said, most of the heavy lifting is done by <code>std.HashMapUnmanaged</code>, which the other variants wrap in a field named "unmanaged".</p>
<h3>Unmanaged</h3>
<p>"Unmanaged" types are sprinkled throughout the standard library. It's a naming convention which indicates that the type in question doesn't maintain an allocator. Any method that requires allocations takes an explicit allocator as a parameter. To see this in practice, consider this linked list:</p><pre><code>
pub fn LinkedList(comptime T: type) type {
return struct {
head: ?*Node = null,
allocator: Allocator,
const Self = @This();
pub fn init(allocator: Allocator) Self {
return .{
.allocator = allocator,
};
}
pub fn deinit(self: Self) void {
var node = self.head;
while (node) |n| {
node = n.next;
self.allocator.destroy(n);
}
}
pub fn append(self: *Self, value: T) !void {
const node = try self.allocator.create(Node);
node.value = value;
const h = self.head orelse {
node.next = null;
self.head = node;
return;
};
node.next = h;
self.head = node;
}
pub const Node = struct {
value: T,
next: ?*Node,
};
};
}</code></pre>
<p>Our <code>init</code> function takes and stores an <code>std.mem.Allocator</code>. This allocator is then used as needed in <code>append</code> and <code>deinit</code>. This is a common pattern in Zig. The "unmanaged" version of the above code is only slightly different:</p><pre><code>
pub fn LinkedListUnmanaged(comptime T: type) type {
return struct {
head: ?*Node = null,
const Self = @This();
pub fn deinit(self: Self, allocator: Allocator) void {
var node = self.head;
while (node) |n| {
node = n.next;
allocator.destroy(n);
}
}
pub fn append(self: *Self, allocator: Allocator, value: T) !void {
const node = try allocator.create(Node);
// .. same as above
}
// Node is the same as above
pub const Node = struct {...}
};
}</code></pre>
<p>We no longer have an <code>allocator</code> field. The <code>append</code> and <code>deinit</code> functions both take an extra parameter: <code>allocator</code>. Because we no longer need to store the allocator, we're able to initialize a <code>LinkedListUnmanaged(T)</code> exclusively with default values (i.e. <code>head: ?*Node = null</code>) and are able to remove the <code>init</code> function altogether. This isn't a requirement of unamanged types, but it is common. To create a <code>LinkedListUnmanaged(i32)</code>, you'd do:</p><pre><code>
var ll = LinkedListUnmanaged(i32){};</code></pre>
<p>It's a bit cryptic, but it's standard Zig. <code>LinkedListUnmanaged(i32)</code> returns a type, so the above is no different than doing: <code>var user = User{}</code> and relying on the default field values of <code>User</code>.</p>
<p>You might be wondering <em>what's the point of unamaged types?</em>. But before we answer that, let's consider how easy it is to provide both managed and unmanaged versions of our <code>LinkedList</code>. We keep our <code>LinkedListUnmanaged</code> as-is, and change our <code>LinkedList</code> to wrap it:</p><pre><code>
pub fn LinkedList(comptime T: type) type {
return struct {
allocator: Allocator,
unmanaged: LinkedListUnmanaged(T),
const Self = @This();
pub fn init(allocator: Allocator) Self {
return .{
.unmanaged = .{},
.allocator = allocator,
};
}
pub fn deinit(self: Self) void {
self.unmanaged.deinit(self.allocator);
}
pub fn append(self: *Self, value: T) !void {
return self.unmanaged.append(self.allocator, value);
}
pub const Node = LinkedListUnmanaged(T).Node;
};
}</code></pre>
<p>This simple composition is, as we save above, the same as how the various hash map types wrap an <code>std.HashMapUnmanaged</code>.</p>
<p>There are a few benefits to unmanaged types. The most important is that they're more explicit. Rather than knowing that a type, like <code>LinkList(T)</code>, will probably need to allocate memory at some point, the explicit API of the unmanaged variant identifies the specific methods that allocate/deallocate. This can help reduce surprises and gives greater control to the caller. A secondary benefit of unamanged types is that they save a few bytes of memory by not having a reference to the allocator. Some applications might need to store thousands or even millions of these structures, in which case it can add up.</p>
<p>In the name of simplicity, the rest of this post won't mentioned unamanged. Anything we see about the <code>StringHashMap</code> or <code>AutoHashMap</code> or <code>HashMap</code> applies equally to their *Unmanaged variant.</p>
<h3>HashMap vs AutoHashMap</h3>
<p><code>std.HashMap</code> is a generic type which takes two type parameters: the type of the key and the type of the value. And, as we saw, hash maps require two functions: <code>hash</code> and <code>eql</code>. Combined, these functions are called the "context". Both functions operate on the key, and there isn't a single <code>hash</code> or <code>eql</code> function that'll work for all types. For example, for integer keys, <code>eql</code> is going to be <code>a_key == b_key</code> where as for <code>[]const u8</code> keys we want <code>std.mem.eql(u8, a_key, b_key)</code>.</p>
<p>When we use <code>std.HashMap</code> we need to provide the context (the two functions). We'll look at this shortly, but for now we can rely on <code>std.AutoHashMap</code> which automatically generate these functions for us. It might surprise you to know that <code>AutoHashMap</code> can even generate a context for more complex keys. The following works: </p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var h = std.AutoHashMap(User, i32).init(allocator);
try h.put(User{.id = 3, .state = .active}, 9001);
defer h.deinit();
}
const User = struct{
id: i32,
state: State,
const State = enum {
active,
pending,
};
};</code></pre>
<p>But there's a limit to <code>AutoHashMap</code>'s capabilities. Change our <code>User</code> struct to:</p><pre><code>
const User = struct{
id: i32,
state: State,
login_ids: []i32,
...
};</code></pre>
<p>And we get the following compilation error: </p>
<blockquote><code>std.hash.autoHash</code> does not allow slices as well as unions and structs containing slices here (<code>User</code>) because the intent is unclear. Consider using <code>std.hash.autoHashStrat</code> or providing your own hash function instead.</blockquote>
<p>The reality is that there are types with ambiguous equality rules. Slices, like our <code>login_ids</code> above, are a good example. Should two slices pointing to different memory, but with the same length and same content be considered equal? It's application-specific. Similarly, I was surprised to find out that <code>AutoHashMap</code> won't allow floats (either directly or as part of a struct):</p><pre><code>
var h = std.AutoHashMap(f32, i32).init(allocator);
defer h.deinit();
try h.put(1.1, 9001);</code></pre>
<p>The above gives a compilation error: <em>unable to hash type <code>f32</code></em>. It turns out that <a href="https://readafterwrite.wordpress.com/2017/03/23/how-to-hash-floating-point-numbers/">hashing floats isn't straightforward</a>. It isn't that floats and slices can't be hashed or compared. It's that whatever implementation Zig picks would likely surprise some developers and that unexpectedness could result in misuse which could lead to bugs. In the end, if <code>AutoHashMap</code> can handle your key type, then use it. If not, use a <code>HashMap</code> and provide your own context (which we'll look at shortly).</p>
<h3>AutoHashMap vs StringHashMap</h3>
<p>You'd be forgiven for thinking that <code>StringHashMap(V)</code> is an alias for <code>AutoHashMap([]const u8, V)</code>. But, as we just saw, <code>AutoHashMap</code> doesn't support slice keys. We can confirm this. Trying to run:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var h = std.AutoHashMap([]const u8, i32).init(allocator);
try h.put("over", 9000);
defer h.deinit();
}</code></pre>
<p>gives us the following error:</p>
<blockquote>error: <code>std.auto_hash.autoHash</code> does not allow slices here (<code>[]const u8</code>) because the intent is unclear. Consider using <code>std.StringHashMap</code> for hashing the contents of <code>[]const u8</code>. Alternatively, consider using <code>std.auto_hash.hash</code> or providing your own hash function instead.</blockquote>
<p>As I said earlier, it isn't that slices can't be hashed or compared, it's that some cases might consider slices equal only if they reference the same memory, while others might consider two slices equal if their elements are the same. But, in the cases of strings, most people expect "teg" to equal "teg" regardless of where each is stored.</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const name1: []const u8 = &.{'T', 'e', 'g'};
const name2 = try allocator.dupe(u8, name1);
const eql1 = std.meta.eql(name1, name2);
const eql2 = std.mem.eql(u8, name1, name2);
std.debug.print("{any}\n{any}", .{eql1, eql2});
}</code></pre>
<p>The above program prints "false" followed by "true". <code>std.meta.eql</code> compares pointers using: <code>a.ptr == b.ptr and a.len == b.len</code>. But, specifically for strings, most programmers probably expect the behavior of <code>std.mem.eql</code>, which compares the bytes within the strings.</p>
<p>Therefore, just like <code>AutoHashMap</code> wraps <code>HashMap</code> with a auto-generated context, <code>StringHashMap</code> also wraps <code>HashMap</code> with a string-specific context. We'll look more closely at contexts next, but here's the context that <code>StringHashMap</code> uses:</p><pre><code>
pub const StringContext = struct {
pub fn hash(self: @This(), s: []const u8) u64 {
_ = self;
return std.hash.Wyhash.hash(0, s);
}
pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
_ = self;
return std.mem.eql(u8, a, b);
}
};</code></pre>
<h3>Custom Context</h3>
<p>We'll finish part 1 by looking at using <code>HashMap</code> directly, which means providing our own context. We'll start with a simple example: creating a HashMap for case-insensitive ascii strings. We want the following to output: <em>Goku = 9000</em>. Notice though that while we insert using the key "GOKU" we fetch using "goku":</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var h = std.HashMap([]const u8, i32, CaseInsensitiveContext, std.hash_map.default_max_load_percentage).init(allocator);
defer h.deinit();
try h.put("Goku", 9000);
std.debug.print("Goku = {d}\n", .{h.get("goku").?});
}</code></pre>
<p>Unlike the <code>StringHashMap</code> generic which only takes the value type, or <code>AutoHashMap</code> that takes both the key and value type, <code>HashMap</code> takes the key type, value type, context type and a fill factor. I'm not going to cover the fill factor; above we're using Zig's default fill factor (80). The part that we are interested in is the <code>CaseInsensitiveContext</code>. Let's look at its implementation:</p><pre><code>
const CaseInsensitiveContext = struct {
pub fn hash(_: CaseInsensitiveContext, s: []const u8) u64 {
var key = s;
var buf: [64]u8 = undefined;
var h = std.hash.Wyhash.init(0);
while (key.len >= 64) {
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
h.update(lower);
key = key[64..];
}
if (key.len > 0) {
const lower = std.ascii.lowerString(buf[0..key.len], key);
h.update(lower);
}
return h.final();
}
pub fn eql(_: CaseInsensitiveContext, a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
};</code></pre>
<p>The first parameter to both functions is an instance of the context itself. This allows more advanced patterns where the context might have state. In many cases though, it isn't used.</p>
<p>Our <code>eql</code> function uses the existing <code>std.ascii.eqlIgnoreCase</code> function to compare two keys in a case-insensitive manner. Straightforward.</p>
<p>Our <code>hash</code> function can be broken down into two parts. The first is lower-casing the key. If we want "goku" and "GOKU" to be treated as equal, our <code>hash</code> function has to return the same hash code for both. We do this in batches of 64 bytes to avoid having to allocate a buffer to hold the lower-cased value. This is possible because our hashing function can be updated with new bytes (which is quite common).</p>
<p>This leads us to the second part, what's <code>std.hash.Wyhash</code>? When talking about a hashing algorithm for hash maps (which are distinct from cryptographic hashing algorithms), we're looking for a number of properties, such as performance (every operation on a hash map requires hashing the key), uniform distribution (if our hash function returns an u64, then a random set of inputs should be well distributed within that range) and collision resistance (different values can result in the same hash code, but the less it happens the better). There are a number of algorithms, some specialized for specific inputs (e.g. short strings), some designed for specific hardware. WyHash is a popular option that works well across a number of inputs and characteristics. You essentially feed it bytes and, once done, get a u64 out (or u32 depending on the version).</p><pre><code>
const std = @import("std");
pub fn main() !void {
{
const name = "Teg";
var h = std.hash.Wyhash.init(0);
h.update(name);
std.debug.print("{d}\n", .{h.final()});
}
{
const name = "Teg";
const err = @intFromError(error.OutOfMemory);
var h = std.hash.Wyhash.init(0);
h.update(name);
h.update(std.mem.asBytes(&err));
std.debug.print("{d}\n", .{h.final()});
}
}</code></pre>
<p>This code outputs: 17623169834704516898 followed by 7758855794693669122. The numbers shouldn't mean anything. The goal is only to show how data can be fed into our hashing function to generate a hash code.</p>
<p>Let's look at another example. Say we have a <code>User</code> that we'd like to use as a key in a hash map:</p><pre><code>
const User = struct {
id: u32,
name: []const u8,
};</code></pre>
<p>We can't use an <code>AutoHashMap</code> for this since it doesn't support slices (which our <code>name</code> is). Let's start with a skeleton: </p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var h = std.HashMap(User, i32, User.HashContext, std.hash_map.default_max_load_percentage).init(allocator);
defer h.deinit();
try h.put(.{.id = 1, .name = "Teg"}, 100);
try h.put(.{.id = 2, .name = "Duncan"}, 200);
std.debug.print("{d}\n", .{h.get(.{.id = 1, .name = "Teg"}).?});
std.debug.print("{d}\n", .{h.get(.{.id = 2, .name = "Duncan"}).?});
}
const User = struct {
id: u32,
name: []const u8,
pub const HashContext = struct {
pub fn hash(_: HashContext, u: User) u64 {
// TODO
}
pub fn eql(_: HashContext, a: User, b: User) bool {
// TODO
}
};
};</code></pre>
<p>We need to implement the <code>hash</code> and <code>eql</code> functions. <code>eql</code>, as is often the case, is straightforward:</p><pre><code>
pub fn eql(_: HashContext, a: User, b: User) bool {
if (a.id != b.id) return false;
return std.mem.eql(u8, a.name, b.name);
}</code></pre>
<p>And if you look at our other hash examples, you might be able to come up with its implementation:</p><pre><code>
pub fn hash(_: HashContext, u: User) u64 {
var h = std.hash.Wyhash.init(0);
h.update(u.name);
h.update(std.mem.asBytes(&u.id));
return h.final();
}</code></pre>
<p>Plug in the two functions and the above example should work.</p>
<h3>Conclusion</h3>
<p>Hopefully you now have a better understanding of how Zig's hash maps are implemented and how to leverage them within your code. In most cases, <code>std.StringHashMap</code> or <code>std.AutoHashMap</code> are all you'll need. But knowing about the existence and purpose of the <code>*Unmanaged</code> variants, as well as the more generic <code>std.HashMap</code>, might prove useful. If nothing else, the documentation and their implementation should now be easier to follow.</p>
<p>In the next part we'll dive deep into hash map keys and values, how they're stored and how they're managed.</p>
<div class="pager">
<span></span>
<a class="next" href="https://www.openmymind.net/Zigs-HashMap-Part-2/">part 2</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Your Website Search Hurts My Feelings2023-12-26T00:00:00Z/Your-Website-Search-Hurts-My-Feelings/
<style>
img{border: 1px solid black;margin:0 auto 40px;display:block}
</style>
<p>I wouldn't mind this <strong>huge</strong> ad so much if the pancake mix was made with lentil flour...on second thought, that doesn't sound good:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/walmart-1.png" alt="walmart search result showing ads instead of results">
<p>And if I complain about not being able to sort by price/quality (or weight), I assume some people will point out that choice adds complexity. But it's hard to understand what value this choice is adding:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/walmart-2.png" alt="facet with a single option">
<p>Since we're talking about facets, I'm curious what category those other 632 products fall under:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/walmart-3.png" alt="category facets showing 16 total products despite having 648 results">
<p>At costco, a search for "rice" yields this amazing facet:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/costco-1.png" alt="Each facet contains all the products">
<p>Surprise, the search returned 25 results (seriously).</p>
<p>I'm particularly intrigued by rice that isn't vegan, lactose free, or gluten free:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/costco-2.png" alt="Rice facet showing only 1 vegan option">
<p>The sorting made me check wiki to see how much revenue they have ($242 billion):</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/costco-3.png" alt="Case-sensitive sorting of brands">
<p>There must be a term in information theory that describes what's going on here:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/redmart-1.png" alt="Search for 'alcohol' returns non-alcohol results">
<p>You can't tell, but the 2nd result <em>is also</em> alcohol free (the first 10 results are). For any Singaporean thinking that Alibaba ruined Lazada who ruined Redmart: no.</p>
<p>I don't think anyone would be offended if I said e-commerce in the Philippines has a lot of catching up to do. But this first page search result is something else (yes, they sell a lot of other laundry detergent):</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/landers-1.png" alt="Search for 'laundry detergent' returns umbrellas">
<p>The inspiration for this post is that nintendo.com lets you filter for <code>2+</code> players, but that includes single system, local wireless and online. So there's no way to get a list of games that supports couch coop. Unless you want to check 1680 games that is:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/nintendo-1.png" alt="1680 2+ player games">
<p>Haha, J/K. The search won't return more than 1000 records.</p>
<p>And while we're talking about nintendo.com, if you goto Games > Nintendo Switch Games:</p>
<img src="https://www.openmymind.net/assets/forposts/website-catalogs/nintendo-2.png" alt="nintendo.com homepage top nav">
<p>You'll get 120 games and only 120 games. Of course the list of switch games isn't under Games > Nintendo Switch Games that would be silly. But, for days I've tried and failed to figure out what these 120 games have in common and why they get this special treatment. I can't sleep.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Zig's MemoryPool Allocator2023-12-22T00:00:00Z/Zig-MemoryPool-Allocator/
<p>You're probably familiar with Zig's <code>GeneralPurposeAllocator</code>, <code>ArenaAllocator</code> and <code>FixedBufferAllocator</code>, but Zig's standard library has another allocator you should be keeping at the ready: <code>std.heap.MemoryPool</code>.</p>
<p>What makes the <code>MemoryPool</code> allocator special is that it can only create one type. As compensation for this limitation, it's very fast when allocating and freeing that type. It's really easy to use:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// This pool can only create and destroy our User
var user_pool = std.heap.MemoryPool(User).init(allocator);
defer user_pool.deinit();
const user1 = try user_pool.create();
defer user_pool.destroy(user1);
// ...
}
const User = struct {
id: i32
};</code></pre>
<p>The real value of the <code>MemoryPool</code> is when you're frequently allocating and destroying (not just once, as above). It works by maintaining a free list of previously destroyed objects. When the list is empty, the item is created using the underlying allocator (a <code>GeneralPuposeAllocator</code> above). But when you destroy an item, it's added the list, where subsequent calls to <code>create</code> can re-use the memory.</p>
<p>The implementation helps explain why <code>MemoryPool</code> works with a single type: the use of a free list and re-use of memory requires all objects to be of the same size. It also explains the performance advantage over other allocators when there's high churn.</p>
<p>In the last blog post, we briefly looked at <a href="https://www.openmymind.net/Zig-Tiptoeing-Around-ptrCast/">Zig's @ptrCast</a> and how we can use it to force the compiler to treat memory as a specific type. The <code>MemoryPool</code> is a good example of <code>@ptrCast</code>'s potential.</p>
<p>If I was going to write something like <code>MemoryPool</code>, I'd probably start with this skeleton:</p><pre><code>
fn MemoryPool(comptime Item: type) type {
return struct {
free_list: ?*Node,
fallback: std.mem.Allocator,
const Node = struct {
data: *Item,
next: ?*Node,
};
};
}</code></pre>
<p>The problem is that we'll need to allocate a <code>Node</code> on every call to <code>destroy</code> to build up our list. Look at the clever trick <code>MemoryPool</code> uses to avoid this:</p><pre><code>
// slightly modified version of what MemoryPool does
pub fn destroy(pool: *Self, ptr: *Item) void {
ptr.* = undefined;
const node = @as(*Node, @ptrCast(ptr));
node.* = Node{
.next = pool.free_list,
};
pool.free_list = node;
}</code></pre>
<p>Do you see what's going on? The memory allocated for our <code>User</code> is being re-purposed to be the node in our free list. From the time that you you call <code>create</code> to the time that you call <code>destroy</code> the memory is yours and its of type <code>*T</code>. But once you call <code>destroy</code> the memory becomes a <code>*Node</code> and is used to create a chain of free memory. This works for two reasons. The first is that there's no time where the two identities overlap (unless you use the memory after calling <code>destroy</code>, but that's a problem with any allocator). The second is that <code>T</code> needs to be at least as large as <code>MemoryPool(T).Node</code>. The pool guaratees that via this line of code:</p><pre><code>
pub const item_size = @max(@sizeOf(Node), @sizeOf(Item));</code></pre>
<p>When we need to allocate using the underlying allocator, we'll allocate <code>item_size</code>. In most cases, <code>@sizeOf(Item)</code> will be larger than <code>@sizeOf(Node)</code>, so this approach not only results in no additional allocations, but also no overhead.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Zig: Tiptoeing Around @ptrCast2023-12-11T00:00:00Z/Zig-Tiptoeing-Around-ptrCast/
<p>A variable associates memory with a specific type. The compiler uses this information to generate the correct instructions (or to tell us that our code is invalid). For example, given this code:</p><pre><code>
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
};
pub fn main() !void {
const user1 = User{.id = 1, .name = "Leto"};
std.debug.print("{d}\n", .{user1.id});
}</code></pre>
<p>The compiler can use <code>user1's</code> type, <code>User</code>, to generate the instructions necessary for reading and writing the <code>id</code> and <code>name</code> fields. This is possible because, while different <code>Users</code> might have different values, they all have the same memory layout. Thus, given a <code>User</code>, the compiler knows the relative position (relative to the memory referenced by the variable) of each field and can generate the instructions.</p>
<p>While the above <code>user1</code> is unquestionably a <code>User</code>, we can use <code>@ptrCast</code> to create a new variable that points to the same location but as a different type:</p><pre><code>
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
};
const Node = struct {
next: ?*Node,
};
pub fn main() !void {
var user1 = User{.id = 1, .name = "Leto"};
const node1: *Node = @ptrCast(&user1);
node1.next = null;
std.debug.print("{}\n", .{node1});
}</code></pre>
<p>This code not only compiles, but it also runs. Compiling and running are two distinct aspects we must consider. The code compiles because we told the compiler it was ok to treat the memory as a <code>*Node</code>. <code>@ptrCast</code> isn't changing the memory at runtime, it's forcing the compiler to see the memory as a <code>*Node</code>. In this case, the code runs because there are some truths we can rely on that make it so the memory used to represent a <code>User</code> can safely be used to represent a <code>Node</code>.</p>
<p>Consider the opposite transformation:</p><pre><code>
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
};
const Node = struct {
next: ?*Node,
};
pub fn main() !void {
var node1 = Node{.next = null};
const user: *User = @ptrCast(&node1);
std.debug.print("Id: {d}\n", .{user.id});
std.debug.print("Name: {d}\n", .{user.name});
}</code></pre>
<p>Now we're creating a <code>Node</code> and telling the compiler to see the underlying memory as a <code>User</code>. Again, this code compiles. But what happens when we try to run it? You'll probably get the same thing I did: <code>Id: 0</code> followed by a segfault. Why does it work one way but not the other? Consider the size of a <code>Node</code> and the size of a <code>User</code>:</p><pre><code>
const std = @import("std");
pub fn main() !void {
std.debug.print("Node: {d} User: {d}\n", .{@sizeOf(Node), @sizeOf(User)});
}</code></pre>
<p>Assuming you're on a modern platform, you'll likely see: <code>Node: 8 User: 24</code>. This highlights the power and danger of <code>@ptrCast</code>: it's obvious that the memory underlying a <code>Node</code> isn't big enough to represent a whole <code>User</code>, but <code>@ptrCast</code> forces the compiler to proceed as though it can.</p>
<p>But size constraints aren't are only issue. Let's go back to our original example and add 2 more lines at the end:</p><pre><code>
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
};
const Node = struct {
next: ?*Node,
};
pub fn main() !void {
var user1 = User{.id = 1, .name = "Leto"};
const node1: *Node = @ptrCast(&user1);
node1.next = null;
std.debug.print("{}\n", .{node1});
std.debug.print("{d}\n", .{user1.id}); // added
std.debug.print("{s}\n", .{user1.name}); // added
}</code></pre>
<p>The underlying memory for <code>node1</code> is more than big enough, but the code still crashes. The issue is that <code>node1.next</code> has written data into the memory which isn't valid when interpreted the memory as <code>User</code>. When we're writing to <code>user.id</code> or <code>user1.name</code>, the compiler will enforce correctness: <code>id</code> must be an <code>u32</code> and <code>name</code> must be a <code>[]const u8</code>. When we write to <code>node1.next</code>, our value is valid based on the <code>Node</code> type. But mix those two: and you'll likely get something invalid.</p>
<p>One last thing worth pointing out is that, unless a structure is declared as <code>packed</code>, Zig makes no guarantee about its memory layout. In almost all cases, you should not write to memory as one type and read it as another (which is exactly what we've done throughout the post). Unless the struct is <code>packed</code> or the struct is very simple, you cannot predict how those read/writes will be interpreted by different types sharing the same memory.</p>
<p>In a previous post exploring <a href="https://www.openmymind.net/Zig-Interfaces/">Zig Interfaces</a> we used <code>@ptrCast</code> to restore erased type information. In this post, we're doing something a little different: alternating and using two concrete types. In the next post we'll examine a wonderful type in Zig's standard library which safely exploits what we've learned here.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Performance of reading a file line by line in Zig2023-11-27T00:00:00Z/Performance-of-reading-a-file-line-by-line-in-Zig/
<p>Maybe I'm wrong, but I believe the canonical way to read a file, line by line, in Zig is:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
// Things are _a lot_ slower if we don't use a BufferedReader
var buffered = std.io.bufferedReader(file.reader());
var reader = buffered.reader();
// lines will get read into this
var arr = std.ArrayList(u8).init(allocator);
defer arr.deinit();
var line_count: usize = 0;
var byte_count: usize = 0;
while (true) {
reader.streamUntilDelimiter(arr.writer(), '\n', null) catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
line_count += 1;
byte_count += arr.items.len;
arr.clearRetainingCapacity();
}
std.debug.print("{d} lines, {d} bytes", .{line_count, byte_count});
}</code></pre>
<p>We could use one of <code>reader</code>'s thin wrappers around <code>streamUntilDelimiter</code> to make the code a little neater, but since our focus is on performance, we'll stick with less abstraction.</p>
<p>The equivalent (I hope) Go code is:</p><pre><code>
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
lineCount := 0
byteCount := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineCount += 1
byteCount += len(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("%d lines, %d bytes", lineCount, byteCount)
}</code></pre>
<p>What's interesting to me is that, on my computer, the Go version runs more than 4x faster. Using ReleaseFast with <a href="https://github.com/json-iterator/test-data/blob/master/large-file.json">this 24MB json-line</a> I found, Zig takes roughly 95ms whereas Go only takes 20ms. What gives?</p>
<p>The issue comes down to how the <code>std.io.Reader</code> functions like <code>streamUntilDelimiter</code> are implemented and how that integrates with <code>BufferedReader</code>. Much like Go's <code>io.Reader</code>, Zig's <code>std.io.Reader</code> requires implementations to provide a single function: <code>fn read(buffer: []u8) !usize</code>. Any functionality provided by <code>std.io.Reader</code> has to rely on this single <code>read</code> function.</p>
<p>This is a fair representation of <code>std.io.Reader.streamUntilDelimiter</code> along with the <code>readByte</code> it depends on:</p><pre><code>
pub fn streamUntilDelimiter(self: Reader, writer: anytype, delimiter: u8) !void {
while (true) {
// will bubble error.EndOfStream, which is how we break out
// of the while loop
const byte: u8 = try self.readByte();
if (byte == delimiter) return;
try writer.writeByte(byte);
}
}
pub fn readByte(self: Reader) !u8 {
var result: [1]u8 = undefined;
// self.read calls our implementations read (e.g. BufferedReader.read)
const amt_read = try self.read(result[0..]);
if (amt_read < 1) return error.EndOfStream;
return result[0];
}</code></pre>
<p>This implementation will safely work for any type that implements a functional <code>read(buffer: []u8) !usize)</code>. But by targeting the lowest common denominator, we potentially lose a lot of performance. If you knew that the underlying implementation had a buffer you could come up with a much more efficient solutions. The biggest, but not only, performance gain would be to leverage the SIMD-optimized <code>std.mem.indexOfScalar</code> to scan for <code>delimiter</code> over the entire buffer.</p>
<p>Here's what that might look like:</p><pre><code>
fn streamUntilDelimiter(buffered: anytype, writer: anytype, delimiter: u8) !void {
while (true) {
const start = buffered.start;
if (std.mem.indexOfScalar(u8, buffered.buf[start..buffered.end], delimiter)) |pos| {
// we found the delimiter
try writer.writeAll(buffered.buf[start..start+pos]);
// skip the delimiter
buffered.start += pos + 1;
return;
} else {
// we didn't find the delimiter, add everything to the output writer...
try writer.writeAll(buffered.buf[start..buffered.end]);
// ... and refill the buffer
const n = try buffered.unbuffered_reader.read(buffered.buf[0..]);
if (n == 0) {
return error.EndOfStream;
}
buffered.start = 0;
buffered.end = n;
}
}
}</code></pre>
<p>If you're curious why <code>buffer</code> is an <code>anytype</code>, it's because <code>std.io.BufferedReader</code> is a generic, and we want our <code>streamUntilDelimiter</code> for any variant, regardless of the type of the underlying unbuffered reader.</p>
<p>If you take this function and use it in a similar way as our initial code, circumventing <code>std.io.Reader.streamUntilDelimiter</code>, you end up with similar performance as Go. And we'd still have room for some optimizations.</p>
<p>This is something I tried to fix in Zig's standard library. I thought I could use Zig's comptime capabilities to detect if the underlying implementation has its own <code>streamUntilDelimeter</code> and use it, falling back to <code>std.io.Reader</code>'s implementation otherwise. And while this is certainly possible, using <code>std.meta.trait.hasFn</code> for example, I ran into problems that I just couldn't work around. The issue is that the <code>buffered.reader()</code> doesn't return an <code>std.io.Reader</code> directly, but goes through an intermediary: <code>std.io.GenericReader</code>. This <code>GenericReader</code> then creates an <code>std.io.Reader</code> on each function call. This double layer of abstraction was more than I wanted to, and probably could, work through.</p>
<p>Instead I <a href="https://github.com/ziglang/zig/issues/17985">opened an issue</a> and wrote a more generic <a href="https://github.com/karlseguin/zul">zig utility library</a> for the little Zig snippets I've collected.</p>
<p>I'm not sure how big the issue actually is. If we assume the above code is right and using a <code>BufferedReader</code> via an <code>std.io.Reader</code> is inefficient, then it's at least a real issue for this common task (on my initial real-world input which is where I ran into this issue, the overhead was over 10x). But the "interface" pattern of building functionality atop the lowest common denominator is common, so I wonder where else performance is being lost. In this specific case though, I think there's an argument to be made that functionality like <code>streamUntilDelimeter</code> should only be made available on something like a <code>BufferedReader</code>.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Zig's std.json.Parsed(T)2023-11-17T00:00:00Z/Zigs-std-json-Parsed/
<p>When parsing JSON using one of Zig's <code>std.json.parseFrom*</code> functions, you'll have to deal with the return type: an <code>std.json.Parsed(T)</code>:</p><pre><code>
const file = try std.fs.openFileAbsolute(file_path, .{});
defer file.close();
var buffered = std.io.bufferedReader(file.reader());
var reader = std.json.reader(allocator, buffered.reader());
defer reader.deinit();
const parsed = try std.json.parseFromTokenSource(Config, allocator, &reader, .{
.allocate = .alloc_always,
});
defer parsed.deinit();
const config = parsed.value;</code></pre>
<p>In the above code <code>parseFromTokenSource</code> returns a <code>std.json.Parsed(Config)</code>. This is a simple structure which exposes a <code>deinit</code> function as well as the parsed <code>value</code>. The reason <code>parseFromTokenSource</code> cannot simply return <code>T</code> (<code>Config</code> in the above case), is that parsing JSON likely results in memory allocations. For example, if our imaginary <code>Config</code> struct had a <code>tags: [][]const u8</code> field, then <code>parseFromTokenSource</code> would need to allocate a slice.</p>
<p>Memory allocated while parsing can be difficult to manage, at least in a way that works in all cases. It would be an unreasonable burden to ask the caller to know what needs freeing, especially for complex/nested objects. To work around this, <code>parseFromTokenSource</code> creates an <code>std.heap.ArenaAllocator</code> from which all allocations are made. When <code>deinit()</code> is called on the returned <code>Parsed(T)</code>, the arena allocator is freed and destroyed.</p>
<p>You might have noticed the <code>.allocate = .alloc_always</code> passed as an option to <code>parseFromTokenSource</code>. This option only relates to whether or not strings are duplicated from the source. In the above example, because our source, <code>file</code>, outlives <code>parsed</code>, we could use the alternative option <code>.alloc_if_needed</code>. But that would not alter the fact that other allocations, such as slices for arrays, would still be created and managed by the arena.</p>
<p>While <code>Parsed(T)</code> is an effective way of dealing with allocations, it's still, in my opinion, cumbersome to use. Specifically, in our above example, <code>config</code> is tied to the lifecycle of <code>parsed</code>. If we wanted to write a <code>loadConfig</code> function, we wouldn't be able to return <code>Config</code>, we'd have to return <code>std.json.Parsed(Config)</code>. A program that deals with a lot of JSON might find itself passing <code>std.json.Parsed(T)</code> all over the place.</p>
<p>Unfortunately, there's no <em>great</em> solution to this problem. If our object is simple, we can clone all allocate fields thus allowing us to decouple the lifetime of our object from <code>std.json.Parsed(T)</code>, but that's not efficient or scalable. If you look at the implementation of <code>parseFromTokenSource</code>, you'll see that it creates the <code>ArenaAllocator</code> and calls a <code>Leaky</code> variant:</p><pre><code>
fn parseFromTokenSource(comptime T: type, allocator: Allocator, ....) !Parsed(T)
var parsed = Parsed(T){
.arena = try allocator.create(ArenaAllocator),
.value = undefined,
};
errdefer allocator.destroy(parsed.arena);
parsed.arena.* = ArenaAllocator.init(allocator);
errdefer parsed.arena.deinit();
parsed.value = try parseFromTokenSourceLeaky(T, parsed.arena.allocator(), scanner_or_reader, options);
return parsed;
}</code></pre>
<p>Since <code>parseFromTokenSourceLeaky</code> is public, we can call it directly and provide our own <code>std.heap.ArenaAllocator</code>. It isn't that different than using <code>parseFromTokenSource</code>, but some cases might already have a suitable ArenaAlloator, or the suitable lifetime hooks in place. In such cases, using the <code>Leaky</code> variant makes sense.</p>
<p>While it might seem superficial, I really dislike exposing <code>std.json.Parsed(T)</code>. The fact that <code>T</code> came from JSON is an irrelevant implementation detail. Also, there's nothing JSON or parsing-specific about <code>std.json.Parsed</code>. It's just an <code>ArenaAllocator</code> and your value. So, while it doesn't really change anything, given no other option, I like to create my own little <code>Parsed(T)</code>-like wrapper:</p><pre><code>
pub fn Managed(comptime T: type) type {
return struct {
value: T,
arena: *std.heap.ArenaAllocator,
const Self = @This();
pub fn fromJson(parsed: std.json.Parsed(T)) Self {
return .{
.arena = parsed.arena,
.value = parsed.value,
};
}
pub fn deinit(self: Self) void {
const arena = self.arena;
const allocator = arena.child_allocator;
arena.deinit();
allocator.destroy(arena);
}
};
}</code></pre>
<p>Which means that I can pass an <code>myApp.Managed(Config)</code> around rather than an <code>std.json.Parse(T)</code>.</p>
<p>Our original example, changed as a function that returns a <s><code>Config</code></s> <code>Managed(Config)</code> from a function, looks like:</p><pre><code>
fn parseConfig(file_path: []const u8, allocator: Allocator) !Managed(Config) {
const file = try std.fs.openFileAbsolute(file_path, .{});
defer file.close();
var buffered = std.io.bufferedReader(file.reader());
var reader = std.json.reader(allocator, buffered.reader());
defer reader.deinit();
const parsed = try std.json.parseFromTokenSource(Config, allocator, &reader, .{
.allocate = .alloc_always,
});
return Managed(Config).fromJson(parsed);
}</code></pre>
<p><a href="#new_comment">Leave a comment</a></p>
Fast Path to Burnout - Delaying Deploys2023-10-27T00:00:00Z/Fast-Path-To-Burnout-Not-Deploying/
<p>Deploying code to production is either the funnest part of my day or the most stressful. Where I land on that spectrum largely comes down to how much control I have over the process. Specifically, I like being able to deploy code when <strong>I</strong> want to deploy it. Process, and people, that don't get this, don't understand software development (and I'm not sure they understand how the human brain works).</p>
<p>I recognize that release management can be a significant process. A lot of what I'm going to say doesn't directly apply to complex software or safety-critical systems. But in such cases, I expect ownership (note, not merely stakeholders) to be shared by a multi-disciplinary team (QA, development, infrastructure, support, legal, etc).</p>
<p>It boils down to a simple reality: I'm intimately familiar with code that I'm currently working on, and less so with code that I worked on a couple weeks ago. Not only is there decay over time, there's also limited capacity for deep understanding. It isn't just that I want to deploy at the height of my understanding, it's that I <em>don't</em> want to deploy once things get fuzzy.</p>
<p>I enjoying watching my deploys. I want to keep an eye on the logs and monitor new metrics or existing metrics that should (or should not!) be impacted. I want to see the impact on system resources and monitor DB load. I want to play with it in production and maybe try to break it. I want to breathe a sigh of relief. The more recent my direct experience with the code, the better and more confidently I can do all of the above.</p>
<p>Delaying deploys burdens my FIFO-cognitive capacity. For a week, two weeks, a month, I try to hold on to the deep understanding, while working on new things. It's stressful and it's amplified by changes to other parts of the system. Taking a logical extreme, I doubt anyone would question the added risk of deploying code after a year (I already made an exception for safety-critical systems). Knowledge would be lost; the system would be different. Maybe I have a pea-sized brain, but once I start working on something new, anything else gets a massive clarity downgrade.</p>
<p>You might be saying <em>that's what staging is for</em>. I agree that can help, but I've never worked in an environment where staging was close-enough to production to give me the necessary confidence <strong>I</strong> need for my own self-accountability. Servers and infrastructure are different, load is different, usage patterns are different.</p>
<p>I'm not advocating for continuous delivery (CD). Though on the spectrum, the ability to do CD is something engineering teams should aspire to. CD directly and indirectly promotes other good engineering practices, such as automated testing and useful logging and metrics. Anyone who advocates fixed schedules to limit deployment risk doesn't understand how to build good software. You can treat this as a litmus test for any engineering leadership interview.</p>
<p>Other stakeholders can have legitimate reasons for delaying releases, and those should be taken into consideration. If I'm being honest, it tends to have little to do with the reason and a lot to do with who's asking. If you have a QA team things can get more complicated (it depends on the exact nature/relationship since there's some pretty drastic difference in how QA teams can operate). But if QA is a bottleneck to deploying, that's something that needs to be fixed.</p>
<p>Accountability without giving control isn't real. When that comes with cognitive overload, you get burnout. When it stems from arbitrary timelines and schedules made up by people who have the control but not the accountability, you get resentment exit interviews.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Zig Interfaces2023-10-08T00:00:00Z/Zig-Interfaces/
<p>If you're picking up Zig, it won't be long before you realize there's no syntactical sugar for creating interfaces. But you'll probably notice interface-like things, such as <code>std.mem.Allocator</code>. This is because Zig doesn't have a simple mechanism for creating interfaces (e.g. <code>interface</code> and <code>implements</code> keywords), but the language itself can be used to achieve the same goal.</p>
<p>There are existing articles that I've been able to copy and paste to get this working, but none really clicked with me. It wasn't until I broke the code down into two parts: making it work and then making it pretty, that I finally understood. So, first, let's make something that works.</p>
<h3>A Simple Interface Implementation</h3>
<p>We're going to create a <code>Writer</code> interface; it's something simple to understand that won't get in our way. We'll stick with a single function; once we know how to do this with one function, it's easy to repeat the pattern for more. First, the interface itself:</p><pre><code>
const Writer = struct {
ptr: *anyopaque,
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
fn writeAll(self: Writer, data: []const u8) !void {
return self.writeAllFn(self.ptr, data);
}
};</code></pre>
<p>This is our complete interface. Depending on your knowledge of Zig, there might be some things you aren't sure about. Our interface has two fields. The first, <code>ptr</code>, is a pointer to the actual implementation. We'll talk about <code>*anyopaque</code> in a bit. The second, <code>writeAllFn</code>, is a function pointer to the function of the actual implementation.</p>
<p>Notice that the interface's <code>writeAll</code> implementation just calls our function pointer and passes it the <code>ptr</code> field as well as any other arguments. Here's a skeleton implementation:</p><pre><code>
const File = struct {
fd: os.fd_t,
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
// What to do with ptr?!
}
fn writer(self: *File) Writer {
return .{
.ptr = self,
.writeAllFn = writeAll,
};
}
};</code></pre>
<p>First, the <code>writer</code> function is how we get a <code>Writer</code> from a <code>*File</code>. This is like calling <code>gpa.allocator()</code> on a GeneralPurposeAllocator to get an <code>std.mem.Allocator</code>. Aside from the fact that we're able to assign <code>self</code> to <code>ptr</code> (a <code>*File</code> to <code>*anyopaque</code>), there's nothing special here. We're just creating a <code>Writer</code> struct. And even this assignment isn't too special, Zig's automatic type coercion requires guaranteed safety and no ambiguity, two properties that are always true when assigning to an <code>*anyopaque</code>.</p>
<p>The part that glues everything together, the part that we've left out, is: what do we do with the <code>ptr: *anyopaque</code> passed back into <code>writeAll</code>? First,<code>*anyopaque</code> is a pointer to something of unknown type and unknown size. Hopefully it's clear why <code>Writer.ptr</code> has to be of this type. It can't be a <code>*File</code>, else it wouldn't be usable for other implementations. The nature of interfaces means that, at compile time, we don't know what the implementation will be and thus <code>*anyopaque</code> is the only possible choice.</p>
<p>It's important to know that when we create a <code>Writer</code> via <code>file.writer()</code>, <code>ptr</code> <em>is</em> the file because we assign it to <code>self</code>. But because <code>ptr</code> is of type <code>*anyopaque</code>, the assignment erases its true type. The memory pointed to by <code>ptr</code> <em>does</em> represent a <code>*File</code>, the compiler just doesn't know that. We need a way to inject this information into the compiler. We can do this with a combination of <code>ptrCast</code> and <code>alignCast</code>:</p><pre><code>
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
const self: *File = @ptrCast(@alignCast(ptr));
_ = try std.os.write(self.fd, data);
}</code></pre>
<p><code>@ptrCast</code> converts a pointer from one type to another. The type to convert to is inferred by the value the result is assigned to. In the above case, we're telling the compiler: <em>give me a variable pointing to the same thing as <code>ptr</code> but treat that like a <code>*File</code>, trust me, I know what I'm doing.</em> <code>@ptrCast</code> is powerful as it allows us to force the type associated with specific memory. If we're wrong and use <code>@ptrCast</code> to convert a pointer into a type incompatible with the underlying memory, we'll have serious runtime issues, with a crash being the best possible outcome.</p>
<p><code>@alignCast</code> is more complicated. There are CPU-specific rules for how data must be arranged in memory. This is called data alignment and it deals with how fields in a structure are aligned in memory. <code>anyopaque</code> always has an alignment of 1. But our <code>File</code> has a different alignment (4). If you want, you can see this by printing <code>@alignOf(File)</code> and <code>@alignOf(anyopaque)</code>. Just like we need <code>@ptrCast</code> to tell the compiler what the type is, we need <code>@alignCast</code> to tell the compiler what the alignment is. And, just like <code>@ptrCast</code>, <code>@alignCast</code> infers this based on what it's being assigned to.</p>
<p>Our complete solution is:</p><pre><code>
const Writer = struct {
ptr: *anyopaque,
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
fn writeAll(self: Writer, data: []const u8) !void {
return self.writeAllFn(self.ptr, data);
}
};
const File = struct {
fd: os.fd_t,
fn writeAll(ptr: *anyopaque, data: []const u8) !void {
const self: *File = @ptrCast(@alignCast(ptr));
// os.write might not write all of `data`, we should really look at the
// returned value, the number of bytes written, and handle cases where data
// wasn't all written.
_ = try std.os.write(self.fd, data);
}
fn writer(self: *File) Writer {
return .{
.ptr = self,
.writeAllFn = writeAll,
};
}
};</code></pre>
<p>Hopefully, this is pretty clear to you. It comes down to two things: using
<code>*anyopaque</code> to be able to store a pointer to any implementation, and
then using <code>@ptrCast(@alignCast(ptr))</code> to restore the correct type information.</p>
<p>As an aside, the interface's <code>ptr</code> type <strong>has</strong> to be a pointer to <code>anyopaque</code>, i.e. <code>*anyopaque</code>, it cannot be just <code>anyopaque</code>. Do you know why? As I said, <code>anyopaque</code> is of unknown size and in Zig, like most languages, all types have to have a known size. <code>Writer</code> has a size of 16 bytes: 2 pointers with each pointer being 8 bytes on a 64 bit platform. If we were to try and use <code>anyopaque</code>, then the size of <code>Writer</code> becomes unknown, which the compiler will not allow. (pointers always have a known type which depends on the underlying architecture, e.g. 4 bytes on a 32bit CPU)</p>
<h3>Making it Prettier</h3>
<p>I'm a fan of the above implementation. There's only a little magic to know and implement. Some of the interfaces in the standard library, like <code>std.mem.Allocator</code>, look just like it. (Because <code>Allocator</code> has a few more functions, a nested structure called <code>VTable</code> (virtual table) is used to hold the function pointers, but that's a small change.)</p><p>
</p><p>The major drawback is that it's only usable through the interface. We can't use <code>file.writeAll</code> directly since <code>writeAll</code> doesn't have a <code>*File</code> receiver. So it's fine if implementations are always accessed through the interface, like Zig's allocators, but it won't work if we need implementations to function on their own as well as through an interface.</p>
<p>In other words, we'd like <code>File.writeAll</code> to be a normal method, essentially not having to deal with <code>*anyopaque</code>:</p><pre><code>
fn writeAll(self: *File, data: []const u8) !void {
_ = try std.os.write(self.fd, data);
}</code></pre>
<p>This is something we can achieve, but it requires changing our <code>Writer</code> interface:</p><pre><code>
const Writer = struct {
// These two fields are the same as before
ptr: *anyopaque,
writeAllFn: *const fn (ptr: *anyopaque, data: []const u8) anyerror!void,
// This is new
fn init(ptr: anytype) Writer {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn writeAll(pointer: *anyopaque, data: []const u8) anyerror!void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.Pointer.child.writeAll(self, data);
}
};
return .{
.ptr = ptr,
.writeAllFn = gen.writeAll,
};
}
// This is the same as before
pub fn writeAll(self: Writer, data: []const u8) !void {
return self.writeAllFn(self.ptr, data);
}
};</code></pre>
<p>What's new here is the <code>init</code> function. To me, it's pretty complicated, but it helps to think of it from the point of view of our original implementation. The point of all the code in <code>init</code> is to turn an <code>*anyopaque</code> into a concrete type, such as <code>*File</code>. This was easy to do from within <code>*File</code>, because within <code>File.writeAll</code>, <code>ptr</code> had to be a <code>*File</code>. But here, to know the type, we need to capture more information.</p>
<p>To better understand <code>init</code>, it might help to see how it's used. Our <code>File.writer</code>, which previous created a <code>Writer</code> directly, is now changed to:</p><pre><code>
fn writer(self: *File) Writer {
return Writer.init(self);
}</code></pre>
<p>So we know that the <code>ptr</code> argument to <code>init</code> is our implementation. The <code>@TypeOf</code> and <code>@typeInfo</code> builtin functions are central to most compile-time work in Zig. The first returns the type of <code>ptr</code>, in this case <code>*File</code>, and the latter returns a tagged union which fully describes the type. You can see that we create a nested structure which also has a <code>writeAll</code> implementation. This is where the <code>*anyopaque</code> is converted to the correct type and the implementation's function is invoked. The structure is needed because Zig lacks anonymous functions. We need <code>Writer.writeAllFn</code> to be this little two-line wrapper, and using a nested structure is the only way to do that.</p>
<p>Obviously <code>file.writer()</code> is something that'll be executed at runtime. It can be tempting to think that everything inside <code>Writer.init</code>, which <code>file.writer()</code> calls, is created at runtime. You might wonder about the lifetime of our internal <code>gen</code> structure, particularly in the face of multiple implementations. But aside from the <code>return</code> statement, <code>init</code> is all compile-time code generation. Specifically, the Zig compiler will create a version of <code>init</code> for each type of <code>ptr</code> that the program uses. The <code>init</code> function is more like a template for the compiler (all because the <code>ptr</code> argument is <code>anytype</code>). When <code>file.writer()</code> is called at runtime, the <code>Writer.init</code> function that ends up being executed is distinct from the <code>Writer.init</code> function that would be executed for a different type.</p>
<p>In the original version, each implementation is responsible for converting <code>*anyopaque</code> to the correct type. Essentially by including that one line of code, <code>@ptrCast(@alignCast(ptr))</code>. In this fancy version, each implementation also has its own code to do this conversion, we've just managed to embed it in the interface and leveraged Zig's comptime capabilities to generate the code for us.</p>
<p>The last part of this code is the function invocation, via <code>ptr_info.Pointer.child.writeAll(self, data)</code>. <code>@typeInfo(T)</code> returns a <code>std.builtin.Type</code> which is a tagged union that describes a type. It can describe 24 different types, such as integers, optional, structs, pointers, etc. Each type has its own properties. For example, an integer has a <code>signedness</code> which other types don't. Here's what <code>@typeInfo(*File)</code> looks like:</p><pre><code>
builtin.Type{
.Pointer = builtin.Type.Pointer{
.address_space = builtin.AddressSpace.generic,
.alignment = 4,
.child = demo.File,
.is_allowzero = false,
.is_const = false,
.is_volatile = false,
.sentinel = null,
.size = builtin.Type.Pointer.Size.One
}
}</code></pre>
<p>The <code>child</code> field is the actual type behind the pointer. When <code>init</code> is called with a <code>*File</code>, <code>ptr_info.Pointer.child.writeAll(...)</code> translates to <code>File.writeAll(...)</code>, exactly what we want.</p>
<p>If you look at other implementations of this pattern, you might find their <code>init</code> function does a few more things. For example, you might find these two additional lines of code after <code>ptr_info</code> is created:</p><pre><code>
if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single item pointer");</code></pre>
<p>The purpose of these is to add additional compile-time checks on the type of value passed to <code>init</code>. Essentially making sure that we passed it a pointer to a single item.</p>
<p>Also, instead of calling the function via:</p><pre><code>
ptr_info.Pointer.child.writeAll(self, data);</code></pre>
<p>You might see:</p><pre><code>
@call(.always_inline, ptr_info.Pointer.child.writeAll, .{self, data});</code></pre>
<p>The <code>@call</code> builtin function, is the same as calling a function directly (as we did), but gives more flexibility by allowing us to supply a <code>CallModifier</code>. As you can see, using <code>@call</code> allows us to tell the compiler to inline the function.</p>
<p>Hopefully this has made the implementation of interfaces in Zig clearer and maybe exposed new capabilities of the language. However, for simple cases where all implementations are known, you might want to consider a different approach.</p>
<h3>Tagged Unions</h3>
<p>As an alternative to the above solutions, tagged unions can be used to emulate interfaces. Here's a complete working example:</p><pre><code>
const Writer = union(enum) {
file: File,
fn writeAll(self: Writer, data: []const u8) !void {
switch (self) {
.file => |file| return file.writeAll(data),
}
}
};
const File = struct {
fd: os.fd_t,
fn writeAll(self: File, data: []const u8) !void {
_ = try std.os.write(self.fd, data);
}
};
pub fn main() !void {
const file = File{.fd = std.io.getStdOut().handle};
const writer = Writer{.file = file};
try writer.writeAll("hi");
}</code></pre>
<p>Remember that when we switch a tagged union, the captured values, e.g. <code>|file|</code>, has the correct type. <code>File</code> in this case.
</p><p>The downside with this approach is that it requires all implementations to be known ahead of time. You can't use it, for example, to create an interface that third parties can create implementations for. Each possible implementation has to be baked into the union.</p>
<p>Within an app, there are plenty of cases where such a restriction is fine. You can build a <code>Cache</code> union with all supported caching implementations, e.g. InMemory, Redis and PostgreSQL. If your application adds a new implementation, you just update the union.</p>
<p>In many cases, the interface will call the underlying implementation directly. For those cases, you can use the special <code>inline else</code> syntax:</p><pre><code>
switch (self) {
.null => {}, // do nothing
inline else => |impl| return impl.writeAll(data),
}</code></pre>
<p>This essentially gets expanded automatically for us, meaning that <code>impl</code> will have the correct underlying type for each case. The other thing this highlights is that the interface can inject its own logic . Above, we see it short-circuit the call when the "null" implementation is active. Exactly how much logic you want to add to the interface is up to you, but we're all adults here and I'm not going to tell you that interfaces should only do dispatching.</p>
<p>As far as I'm concerned, tagged unions should be your first option.</p>
<p>As an aside, I want to mention that there's yet another option for creating interfaces which relies on the <code>@fieldParentPtr</code> builtin. This used to be the standard way to create interfaces, but is now infrequently used. Still, you might see references to it.</p>
<p>If you're interested in learning Zig, consider my <a href="https://www.openmymind.net/learning_zig/">Learning Zig</a> series.</p>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Conclusion2023-09-08T00:00:00Z/learning_zig/conclusion/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/coding_in_zig/">Coding In Zig</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<span></span>
</div>
<article>
<section>
<h1 tabindex="-1" id="coding"><a href="https://www.openmymind.net/#coding" aria-hidden="true">Conclusion</a></h1>
<p>Some readers might recognize me as the author of various "The Little $TECH Book" and wonder why this isn't called "The Little Zig Book". The truth is I'm not sure Zig fits in a "The Little" format. Part of the challenge is that Zig's complexity and learning curve will vary greatly depending on your own background and experience. If you're a seasoned C or C++ programmer, then a concise summary of the language is probably fine, but then, you'll probably rely on the <a href="https://ziglang.org/documentation/master/">Zig Language Reference</a>.</p>
<p>While we've covered a lot in this guide, there's still a large amount of content we haven't touched. I don't want that to discourage or overwhelm you. All languages are multi-layered and you now have a foundation, and a reference, to start and begin your mastery. Frankly, the parts that I didn't cover I simply don't understand well enough to explain. That hasn't stopped me from using and building meaningful things in Zig, like a popular http server library.</p>
<p>I do want to highlight one thing that was completely skipped over. It's probably something you already know, but Zig works particularly well with C code. Because the ecosystem is still young, and the standard library small, you might run into cases where using a C library is your best option. For example, there's no regular expression module in Zig's standard library, and one reasonable option would be to use a C library. I've written Zig libraries for SQLite and DuckDB, and it was straightforward. If you've mostly followed all that's in this guide, you shouldn't have any problems.</p>
<p>I hope this resource helps and I hope you have fun programming.</p>
</section>
<section>
<h2 tabindex="-1" id="thanks"><a href="https://www.openmymind.net/#thanks" aria-hidden="true">Thanks</a></h2>
<p>Thank you to all the people who have contributed fixes and suggestion to this series. In particular, thanks to <a href="https://github.com/gonzus">Gonzalo Diethelm</a> for providing a thorough edit.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/coding_in_zig/">Coding In Zig</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<span></span>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Coding in Zig2023-09-08T00:00:00Z/learning_zig/coding_in_zig/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/generics/">Generics</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/conclusion/">Conclusion</a>
</div>
<article>
<section>
<h1 tabindex="-1" id="coding"><a href="https://www.openmymind.net/#coding" aria-hidden="true">Coding In Zig</a></h1>
<p>With much of the language now covered, we're going to wrap things up by revisiting a few topics and looking at a few more practical aspects of using Zig. In doing so, we're going to introduce more of the standard library and present less trivial code snippets. </p>
</section>
<section>
<h2 tabindex="-1" id="danling_pointers"><a href="https://www.openmymind.net/#danling_pointers" aria-hidden="true">Dangling Pointers</a></h2>
<p>We begin by looking at more examples of dangling pointers. This might seem like an odd thing to focus on, but if you're coming from a garbage collected language, this is likely the biggest challenge you'll face.</p>
<p>Can you figure out what the following outputs?</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(User).init(allocator);
defer lookup.deinit();
const goku = User{.power = 9001};
try lookup.put("Goku", goku);
// returns an optional, .? would panic if "Goku"
// wasn't in our hashmap
const entry = lookup.getPtr("Goku").?;
std.debug.print("Goku's power is: {d}\n", .{entry.power});
// returns true/false depending on if the item was removed
_ = lookup.remove("Goku");
std.debug.print("Goku's power is: {d}\n", .{entry.power});
}
const User = struct {
power: i32,
};</code></pre>
<p>When I ran this, I got:</p><pre><code>
Goku's power is: 9001
Goku's power is: -1431655766</code></pre>
<p>This code introduces Zig's generic <code>std.StringHashMap</code> which is a specialized version of <code>std.AutoHashMap</code> with the key type set to <code>[]const u8</code>. Even if you aren't 100% sure what's going on, it's a good guess that my output relates to the fact that our second <code>print</code> happens after we <code>remove</code> the entry from <code>lookup</code>. Comment out the call to <code>remove</code>, and the output is normal.</p>
<p>The key to understanding the above code is to be aware of where data/memory exists, or, put differently, who <em>owns</em> it. Remember that Zig arguments are passed-by-value, that is, we pass a [shallow] copy of the value. The <code>User</code> in our <code>lookup</code> is not the same memory referenced by <code>goku</code>. Our above code has <strong>two</strong> users, each with their own owner. <code>goku</code> is owned by <code>main</code>, and its copy is owned by <code>lookup</code>.</p>
<p>The <code>getPtr</code> method returns a pointer to the value in the map, in our case, it returns a <code>*User</code>. Herein lies the problem, <code>remove</code> makes our <code>entry</code> pointer invalid. In this example, the proximity of <code>getPtr</code> and <code>remove</code> makes the issue somewhat obvious. But it isn't hard to imagine code calling <code>remove</code> without knowing that a reference to the entry is being held somewhere else.</p>
<aside>When I wrote this example, I wasn't sure what would happen. It was possible for <code>remove</code> to be implemented by setting an internal flag, delaying actual removal until a later event. If that had been the case, the above might have "worked" in our simple cases, but would have failed with more complicated usage. That sounds terrifyingly difficult to debug.</aside>
<p>Besides not calling <code>remove</code>, we can fix this a few different ways. The first is that we could use <code>get</code> instead of <code>getPtr</code>. This would return a <code>User</code> instead of a <code>*User</code> and thus would return copy of the value in <code>lookup</code>. We'd then have three <code>Users</code>.</p>
<ol class="inline">
<li>Our original <code>goku</code>, tied to the function.
</li><li>The copy in <code>lookup</code>, owned by the lookup.
</li><li>And a copy of our copy, <code>entry</code>, also tied to the function.
</li></ol>
<p>Because <code>entry</code> would now be its own independent copy of the user, removing it from <code>lookup</code> would not invalidate it.</p>
<p>Another option is to change <code>lookup</code>'s type from <code>StringHashMap(User)</code> to <code>StringHashMap(*const User)</code>. This code works:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// User -> *const User
var lookup = std.StringHashMap(*const User).init(allocator);
defer lookup.deinit();
const goku = User{.power = 9001};
// goku -> &goku
try lookup.put("Goku", &goku);
// getPtr -> get
const entry = lookup.get("Goku").?;
std.debug.print("Goku's power is: {d}\n", .{entry.power});
_ = lookup.remove("Goku");
std.debug.print("Goku's power is: {d}\n", .{entry.power});
}
const User = struct {
power: i32,
};</code></pre>
<p>There are a number of subtleties in the above code. First of all, we now have a single <code>User</code>, <code>goku</code>. The value in <code>lookup</code> and <code>entry</code> are both references to <code>goku</code>. Our call to <code>remove</code> still removes the value from our <code>lookup</code>, but that value is just the address of <code>user</code>, it isn't <code>user</code> itself. If we had stuck with <code>getPtr</code>, we'd get an invalid <code>**User</code>, invalid because of <code>remove</code>. In both solutions, we had to use <code>get</code> instead of <code>getPtr</code>, but in this case, we're just copying the address, not the full <code>User</code>. For large objects, that can be a significant difference.</p>
<p>With everything in a single function and a small value like <code>User</code>, this still feels like an artificially created problem. We need an example that legitimately makes data ownership an immediate concern.</p>
</section>
<section>
<h2 tabindex="-1" id="ownership"><a href="https://www.openmymind.net/#ownership" aria-hidden="true">Ownership</a></h2>
<p>I love hash maps because they're something everyone knows and everyone uses. They also have a lot of different use cases, most of which you've probably experienced first hand. While they can be used as short lived lookups, they're often long-lived and thus require equally long-lived values.</p>
<p>This code populates our <code>lookup</code> with names you enter in the terminal. An empty name stops the prompt loop. Finally, it detects whether "Leto" was one of the supplied names.</p><pre><code>
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var lookup = std.StringHashMap(User).init(allocator);
defer lookup.deinit();
// stdin is an std.io.Reader
// the opposite of an std.io.Writer, which we already saw
const stdin = std.io.getStdIn().reader();
// stdout is an std.io.Writer
const stdout = std.io.getStdOut().writer();
var i: i32 = 0;
while (true) : (i += 1) {
var buf: [30]u8 = undefined;
try stdout.print("Please enter a name: ", .{});
if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| {
var name = line;
if (builtin.os.tag == .windows) {
// In Windows lines are terminated by \r\n.
// We need to strip out the \r
name = std.mem.trimRight(u8, name, "\r");
}
if (name.len == 0) {
break;
}
try lookup.put(name, .{.power = i});
}
}
const has_leto = lookup.contains("Leto");
std.debug.print("{any}\n", .{has_leto});
}
const User = struct {
power: i32,
};</code></pre>
<p>The code is case sensitive, but no mater how perfectly we type "Leto", <code>contains</code> always returns <code>false</code>. Let's debug this by iterating through <code>lookup</code> and dumping the keys and values:</p><pre><code>
// Place this code after the while loop
var it = lookup.iterator();
while (it.next()) |kv| {
std.debug.print("{s} == {any}\n", .{kv.key_ptr.*, kv.value_ptr.*});
}</code></pre>
<p>This iterator pattern is common in Zig, and relies on the synergy between <code>while</code> and optional types. Our iterator item returns pointers to our key and value, hence we dereference them with <code>.*</code> to access the actual value rather than the address. The output will depend on what you enter, but I got:</p><pre><code>
Please enter a name: Paul
Please enter a name: Teg
Please enter a name: Leto
Please enter a name:
�� == learning.User{ .power = 1 }
��� == learning.User{ .power = 0 }
��� == learning.User{ .power = 2 }
false</code></pre>
<p>The values look ok, but not the keys. If you're not sure what's happening, it's probably my fault. Earlier, I intentionally misdirected your attention. I said that hash maps are often long-lived and thus require long-lived <em>values</em>. The truth is that they require long-lived values <strong>as well as</strong> long-lived keys! Notice that <code>buf</code> is defined inside our <code>while</code> loop. When we call <code>put</code>, we're giving our hash map a key that has a far shorter lifetime than the hash map itself. Moving <code>buf</code> <em>outside</em> the <code>while</code> loop solves our lifetime issue, but that buffer is reused in each iteration. It still won't work because we're mutating the underlying key data.</p>
<p>For our above code, there's really only one solution: our <code>lookup</code> must take ownership of the keys. We need to add one line and change another:</p><pre><code>
// replace the existing lookup.put with these two lines
const owned_name = try allocator.dupe(u8, name);
// name -> owned_name
try lookup.put(owned_name, .{.power = i});</code></pre>
<p><code>dupe</code> is a method of <code>std.mem.Allocator</code> that we haven't seen before. It allocates a duplicate of the given value. The code now works because our keys, now on the heap, outlive <code>lookup</code>. In fact, we've done too good a job of extending the lifetime of those strings: we've introduced memory leaks.</p>
<p>You might have thought that when we called <code>lookup.deinit</code>, our keys and values would be freed for us. But there's no one-size-fits-all solution that <code>StringHashMap</code> could use. First, the keys could be string literals, which cannot be freed. Second, they could have been created with a different allocator. Finally, while more advanced, there are legitimate cases where keys might not be owned by the hash map.</p>
<p>The only solution is to free the keys ourselves. At this point, it would probably make sense to create our own <code>UserLookup</code> type and encapsulate this cleanup logic in our <code>deinit</code> function. We'll keep things messy:</p><pre><code>
// replace the existing:
// defer lookup.deinit();
// with:
defer {
var it = lookup.keyIterator();
while (it.next()) |key| {
allocator.free(key.*);
}
lookup.deinit();
}</code></pre>
<p>Our <code>defer</code> logic, the first we've seen with a block, frees each key and then deinitializes <code>lookup</code>. We're using <code>keyIterator</code> to only iterate the keys. The iterator value is a pointer to the key entry in the hash map, a <code>*[]const u8</code>. We want to free the actual value, since that's what we allocated via <code>dupe</code>, so we dereference the value using <code>.*</code>.</p>
<p>I promise, we're done talking about dangling pointers and memory management. What we've discussed might still be unclear or too abstract. It's fine to revisit this when you have a more hands on problem to solve. That said, if you plan on writing anything non-trivial, this is something you'll almost certainly need to master. When you're feeling up to it, I urge you to take the prompt loop example and play with it on your own. Introduce a <code>UserLookup</code> type that encapsulates all of the memory management we had to do. Try having <code>*User</code> values instead of <code>User</code>, creating the users on the heap and freeing them like we did the keys. Write tests that covers your new structure, using the <code>std.testing.allocator</code> to make sure you aren't leaking any memory.</p>
</section>
<section>
<h2 tabindex="-1" id="arraylist"><a href="https://www.openmymind.net/#arraylist" aria-hidden="true">ArrayList</a></h2>
<p>You'll be glad to know that you can forget about our <code>IntList</code> and the generic alternative we created. Zig has a proper dynamic array implementation: <code>std.ArrayList(T)</code>.</p>
<p>It's pretty standard stuff, but it's such a commonly needed and used data structure that it's worth seeing it in action:</p><pre><code>
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arr = std.ArrayList(User).init(allocator);
defer {
for (arr.items) |user| {
user.deinit(allocator);
}
arr.deinit();
}
// stdin is an std.io.Reader
// the opposite of an std.io.Writer, which we already saw
const stdin = std.io.getStdIn().reader();
// stdout is an std.io.Writer
const stdout = std.io.getStdOut().writer();
var i: i32 = 0;
while (true) : (i += 1) {
var buf: [30]u8 = undefined;
try stdout.print("Please enter a name: ", .{});
if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| {
var name = line;
if (builtin.os.tag == .windows) {
// In Windows lines are terminated by \r\n.
// We need to strip out the \r
name = std.mem.trimRight(u8, name, "\r");
}
if (name.len == 0) {
break;
}
const owned_name = try allocator.dupe(u8, name);
try arr.append(.{.name = owned_name, .power = i});
}
}
var has_leto = false;
for (arr.items) |user| {
if (std.mem.eql(u8, "Leto", user.name)) {
has_leto = true;
break;
}
}
std.debug.print("{any}\n", .{has_leto});
}
const User = struct {
name: []const u8,
power: i32,
fn deinit(self: User, allocator: Allocator) void {
allocator.free(self.name);
}
};</code></pre>
<p>Above is a reproduction of our hash map code, but using an <code>ArrayList(User)</code>. All of the same lifetime and memory management rules apply. Notice that we're still creating a <code>dupe</code> of the name, and we're still freeing each name before we <code>deinit</code> the <code>ArrayList</code>.</p>
<p>This is a good time to point out that Zig doesn't have properties or private fields. You can see this when we access <code>arr.items</code> to iterate through the values. The reason for not having properties is to eliminate a source of surprises. In Zig, if it looks like a field access, it <strong>is</strong> a field access. Personally, I think the lack of private fields is a mistake, but it's certainly something we can work around. I've taken to prefixing fields with underscore to signal "internal use only".</p>
<p>Because the string "type" is a <code>[]u8</code> or <code>[]const u8</code>, an <code>ArrayList(u8)</code> is the appropriate type for a string builder, like .NET's <code>StringBuilder</code> or Go's <code>strings.Builder</code>. In fact, you'll often use this when a function takes a <code>Writer</code> and you want a string. We previously saw an example which used <code>std.json.stringify</code> to output JSON to stdout. Here's how you'd use an <code>ArrayList(u8)</code> to output it into a variable:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try std.json.stringify(.{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2}, out.writer());
std.debug.print("{s}\n", .{out.items});
}</code></pre>
</section>
<section>
<h2 tabindex="-1" id="anytype"><a href="https://www.openmymind.net/#anytype" aria-hidden="true">Anytype</a></h2>
<p>In part 1, we briefly talked about <code>anytype</code>. It's a pretty useful form of compile-time duck-typing. Here's a simple logger:</p><pre><code>
pub const Logger = struct {
level: Level,
// "error" is reserved, names inside an @"..." are always
// treated as identifiers
const Level = enum {
debug,
info,
@"error",
fatal,
};
fn info(logger: Logger, msg: []const u8, out: anytype) !void {
if (@intFromEnum(logger.level) <= @intFromEnum(Level.info)) {
try out.writeAll(msg);
}
}
};</code></pre>
<p>The <code>out</code> parameter of our <code>info</code> function has the type <code>anytype</code>. This means that our <code>Logger</code> can log messages to any structure that has a <code>writeAll</code> method accepting a <code>[]const u8</code> and returning a <code>!void</code>. This isn't a runtime feature. Type checking happens at compile-time and for each type used, a correctly typed function is created. If we try to call <code>info</code> with a type that doesn't have all the of the necessary functions (in this case just <code>writeAll</code>), we'll get a compile time error:</p><pre><code>
var l = Logger{.level = .info};
try l.info("sever started", true);</code></pre>
<p>Giving us: <em>no field or member function named 'writeAll' in 'bool'</em>. Using the <code>writer</code> of an <code>ArrayList(u8)</code> works:</p><pre><code>
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var l = Logger{.level = .info};
var arr = std.ArrayList(u8).init(allocator);
defer arr.deinit();
try l.info("sever started", arr.writer());
std.debug.print("{s}\n", .{arr.items});
}</code></pre>
<p>One massive drawback of <code>anytype</code> is documentation. Here's the signature for the <code>std.json.stringify</code> function we've used a few times:</p><pre><code>
// I **hate** multi-line function definitions
// But I'll make an exception for a guide which
// you might be reading on a small screen.
fn stringify(
value: anytype,
options: StringifyOptions,
out_stream: anytype
) @TypeOf(out_stream).Error!void</code></pre>
<p>The first parameter, <code>value: anytype</code> is kind of obvious. It's the value to serialize and it can be anything (actually, there are some things Zig's JSON serializer can't serialize). We can guess that the <code>out_stream</code> is <em>where</em> to write the JSON, but your guess is as good as mine about what methods it needs to implement. The only way to figure it out is to read the source code or, alternatively, pass a dummy value and use the compiler errors as our documentation. This is something that might get improved with better auto document generators. But, not for the first time, I wish Zig had interfaces.</p>
</section>
<section>
<h2 tabindex="-1" id="typeof"><a href="https://www.openmymind.net/#typeof" aria-hidden="true">@TypeOf</a></h2>
<p>In previous parts, we used <code>@TypeOf</code> to help us examine the type of various variables. From our usage, you'd be forgiven for thinking that it returns the name of the type as a string. However, given that it's a PascalCase function, you should know better: it returns a <code>type</code>.</p>
<p>One of my favorite usages of <code>anytype</code> is to pair it with the <code>@TypeOf</code> and <code>@hasField</code> builtin functions for writing test helpers. Although every <code>User</code> type that we've seen has been very simple, I'll ask you to imagine a more complex structure with many fields. In many of our tests, we need a <code>User</code>, but we only want to specify the fields relevant to the test. Let's create a <code>userFactory</code>:</p><pre><code>
fn userFactory(data: anytype) User {
const T = @TypeOf(data);
return .{
.id = if (@hasField(T, "id")) data.id else 0,
.power = if (@hasField(T, "power")) data.power else 0,
.active = if (@hasField(T, "active")) data.active else true,
.name = if (@hasField(T, "name")) data.name else "",
};
}
pub const User = struct {
id: u64,
power: u64,
active: bool,
name: [] const u8,
};</code></pre>
<p>A default user can be created by calling <code>userFactory(.{})</code>, or we can override specific fields with <code>userFactory(.{.id = 100, .active = false})</code>. It's a small pattern, but I really like it. It's also a nice baby step into the world of metaprogramming.</p>
<p>More commonly <code>@TypeOf</code> is paired with <code>@typeInfo</code>, which returns an <code>std.builtin.Type</code>. This is a powerful tagged union that fully describes a type. The <code>std.json.stringify</code> function recursively uses this on the provided <code>value</code> to figure out how to serialize it.</p>
</section>
<section>
<h2 tabindex="-1" id="build"><a href="https://www.openmymind.net/#build" aria-hidden="true">Zig Build</a></h2>
<p>If you've read through this entire guide waiting for insight into setting up more complex projects, with multiple dependencies and various targets, you're about to be disappointed. Zig has a powerful build system, so much so that an increasing number of non-Zig projects are making use of it, such as libsodium. Unfortunately, all of that power means that, for simpler needs, it isn't the easiest to use, or understand.</p>
<aside>The truth is, I don't understand Zig's build system well enough to explain it.</aside>
<p>Still, we can at least get a brief overview. To run our Zig code, we've used <code>zig run learning.zig</code>. Once, we also used <code>zig test learning.zig</code> to run a test. The <code>run</code> and <code>test</code> commands are fine for playing around, but it's the <code>build</code> command you'll need for anything more complex. The <code>build</code> command relies on a <code>build.zig</code> file with the special <code>build</code> entrypoint. Here's a skeleton:</p><pre><code>
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) !void {
_ = b;
}</code></pre>
<p>Every build has a default "install" step, which you can now run with <code>zig build install</code>, but since our file is mostly empty, you won't get any meaningful artifacts. We need to tell our build about our program's entry point, which is in <code>learning.zig</code>:</p><pre><code>
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// setup executable
const exe = b.addExecutable(.{
.name = "learning",
.target = target,
.optimize = optimize,
.root_source_file = .{ .path = "learning.zig" },
});
b.installArtifact(exe);
}</code></pre>
<p>Now if you run <code>zig build install</code>, you'll get a binary at <code>./zig-out/bin/learning</code>. Using the standard targets and optimizations allows us to override the default as command line arguments. For example to build a size-optimized version of our program for Windows, we'd do:</p><pre><code>
zig build install -Doptimize=ReleaseSmall -Dtarget=x86_64-windows-gnu</code></pre>
<p>An executable will often have two additional steps, beyond the default "install": "run" and "test". A library might have a single "test" step. For a basic argument-less <code>run</code>, we need to add four lines to the end of our build:</p><pre><code>
// add after: b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Start learning!");
run_step.dependOn(&run_cmd.step);</code></pre>
<p>This creates two dependencies via the two calls to <code>dependOn</code>. The first ties our new run command to the built-in install step. The second ties the "run" step to our newly created "run" command. You might be wondering why you need a run command as well as a run step. I believe this separation exists to support more complicated setups: steps that depend on multiple commands, or commands that are used across multiple steps. If you run <code>zig build --help</code> and scroll to the top, you'll see our new "run" step. You can now run the program by executing <code>zig build run</code>.</p>
<p>To add a "test" step, you'll duplicate most of the run code we just added, but rather than <code>b.addExecutable</code>, you'll kick things off with <code>b.addTest</code>:</p><pre><code>
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = .{ .path = "learning.zig" },
});
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);</code></pre>
<p>We gave this step the name of "test". Running <code>zig build --help</code> should now show another available step, "test". Since we don't have any tests, it's hard to tell whether this is working or not. Within <code>learning.zig</code>, add:</p><pre><code>
test "dummy build test" {
try std.testing.expectEqual(false, true);
}</code></pre>
<p>Now when you run <code>zig build test</code>, you should get a test failure. If you fix the test and run <code>zig build test</code> again, you won't get any output. By default, Zig's test runner only outputs on failure. Use <code>zig build test --summary all</code> if, like me, pass or fail, you always want a summary.</p>
<p>This is the minimal configuration you'll need to get up and running. But rest easy knowing that if you need to build it, Zig can probably handle it. Finally, you can, and probably should, use <code>zig init-exe</code> or <code>zig init-lib</code> within your project root to have Zig create a well-documented build.zig file for you.</p>
</section>
<section>
<h2 tabindex="-1" id="dependencies"><a href="https://www.openmymind.net/#dependencies" aria-hidden="true">Third Party Dependencies</a></h2>
<p>Zig's built-in package manager is relatively new and, as a consequence, has a number of rough edges. While there is room for improvements, it's usable as is. There are two parts we need to look at: creating a package and using packages. We'll go through this in full.</p>
<p>First, create a new folder named <code>calc</code> and create three files. The first is <code>add.zig</code>, with the following content:</p><pre><code>
// Oh, a hidden lesson, look at the type of b
// and the return type!!
pub fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
return a + b;
}
const testing = @import("std").testing;
test "add" {
try testing.expectEqual(@as(i32, 32), add(30, 2));
}</code></pre>
<p>It's a bit silly, a whole package just to add two values, but it will let us focus on the packaging aspect. Next we'll add an equally silly: <code>calc.zig</code>:</p><pre><code>
pub const add = @import("add.zig").add;
test {
// By default, only tests in the specified file
// are included. This magic line of code will
// cause a reference to all nested containers
// to be tested.
@import("std").testing.refAllDecls(@This());
}</code></pre>
<p>We're splitting this up between <code>calc.zig</code> and <code>add.zig</code> to prove that <code>zig build</code> will automatically build and package all of our project files. Finally, we can add a <code>build.zig</code>:</p><pre><code>
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = .{ .path = "calc.zig" },
});
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);
}</code></pre>
<p>This is all a repetition of what we saw in the previous section. With this, you can run <code>zig build test --summary all</code>.</p>
<p>Back to our <code>learning</code> project and our previously created <code>build.zig</code>. We'll begin by adding our local <code>calc</code> as a dependency. We need to make three additions. First, we'll create a module pointing to our <code>calc.zig</code>:</p><pre><code>
// You can put this near the top of the build
// function, before the call to addExecutable.
const calc_module = b.addModule("calc", .{
.root_source_file = .{ .path = "PATH_TO_CALC_PROJECT/calc.zig" },
});</code></pre>
<p>You'll need to adjust the path to <code>calc.zig</code>. We now need to add this module to both our existing <code>exe</code> and <code>tests</code> variables. Since our <code>build.zig</code> is getting busier, we'll try to organize things a little:</p><pre><code>
const std = @import("std");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const calc_module = b.addModule("calc", .{
.root_source_file = .{ .path = "PATH_TO_CALC_PROJECT/calc.zig" },
});
{
// setup our "run" cmmand
const exe = b.addExecutable(.{
.name = "learning",
.target = target,
.optimize = optimize,
.root_source_file = .{ .path = "learning.zig" },
});
// add this
exe.root_module.addImport("calc", calc_module);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Start learning!");
run_step.dependOn(&run_cmd.step);
}
{
// setup our "test" command
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = .{ .path = "learning.zig" },
});
// add this
tests.root_module.addImport("calc", calc_module);
const test_cmd = b.addRunArtifact(tests);
test_cmd.step.dependOn(b.getInstallStep());
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&test_cmd.step);
}
}</code></pre>
<p>From within your project, you're now able to <code>@import("calc")</code>:</p><pre><code>
const calc = @import("calc");
...
calc.add(1, 2);</code></pre>
<p>Adding a remote dependency takes a bit more effort. First, we need to go back to the <code>calc</code> project and define a module. You might think that the project itself is a module, but a project can expose multiple modules, so we need to explicitly create it. We use the same <code>addModule</code>, but discard the return value. Simply calling <code>addModule</code> is enough to define the module which other projects will then be able to import.</p><pre><code>
_ = b.addModule("calc", .{
.root_source_file = .{ .path = "calc.zig" },
});</code></pre>
<p>This is the only change we need to make to our library. Because this is an exercise in having a remote dependency, I've pushed this <code>calc</code> project to Github so that we can import it into our learning project. It's available at <a href="https://github.com/karlseguin/calc.zig">https://github.com/karlseguin/calc.zig</a>.</p>
<p>Back in our learning project, we need a new file, <code>build.zig.zon</code>. "ZON" stands for Zig Object Notation and it allows Zig data to be expressed in a human readable format, and for that human readable format to be turned into Zig code. The contents of the <code>build.zig.zon</code> will be:</p><pre><code>
.{
.name = "learning",
.paths = .{""},
.version = "0.0.0",
.dependencies = .{
.calc = .{
.url = "https://github.com/karlseguin/calc.zig/archive/d1881b689817264a5644b4d6928c73df8cf2b193.tar.gz",
.hash = "12ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
},
},
}</code></pre>
<p>There are two questionable values in this file, the first is <code>d1881b689817264a5644b4d6928c73df8cf2b193</code> within the <code>url</code>. This is simply the git commit hash. The second is the value of <code>hash</code>. As far as I know, there's currently no great way to tell what this value should be, so we use a dummy value for the time being.</p>
<p>To use this dependency, we need to make one change to our <code>build.zig</code>:</p><pre><code>
// replace this:
const calc_module = b.addModule("calc", .{
.root_source_file = .{ .path = "calc/calc.zig" },
});
// with this:
const calc_dep = b.dependency("calc", .{.target = target,.optimize = optimize});
const calc_module = calc_dep.module("calc");</code></pre>
<p>In <code>build.zig.zon</code> we named the dependency <code>calc</code>, and that's the dependency that we're loading here. From within this dependency, we're grabbing the <code>calc</code> module, which is what we named the module in <code>calc</code>'s <code>build.zig</code>.</p>
<p>If you try to run <code>zig build test</code>, you should see an error:</p><pre><code>
hash mismatch: manifest declares
122053da05e0c9348d91218ef015c8307749ef39f8e90c208a186e5f444e818672da
but the fetched package has
122036b1948caa15c2c9054286b3057877f7b152a5102c9262511bf89554dc836ee5</code></pre>
<p>Copy and paste the correct hash back into the <code>build.zig.zon</code> and try running <code>zig build test</code> again. Everything should now be working.</p>
<p>It sounds like a lot, and I hope things get streamlined. But it's mostly something you can copy and paste from other projects and, once setup, you can move on.</p>
<p>A word of warning, I've found Zig's caching of dependencies to be on the aggressive side. If you try to update a dependency but Zig doesn't seem to detect the change...well, I nuke the project's <code>zig-cache</code> folder as well as <code>~/.cache/zig</code>.</p>
</section>
<section>
<p>We've covered a lot of ground, exploring a few core data structures and bringing large chunks of previous parts together. Our code has become a little more complex, focusing less on specific syntax and looking more like real code. I'm excited by the possibility that, despite this complexity, the code mostly made sense. It didn't, don't give up. Pick an example and break it, add print statements, write some tests for it. Get hands on with the code, making your own, and then come back and read over those parts that didn't make sense.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/generics/">Generics</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/conclusion/">Conclusion</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Generics2023-09-08T00:00:00Z/learning_zig/generics/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/heap_memory/">Heap Memory & Allocators</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/coding_in_zig/">Coding In Zig</a>
</div>
<article>
<section>
<h1 tabindex="-1" id="generics"><a href="https://www.openmymind.net/#generics" aria-hidden="true">Generics</a></h1>
<p>In the previous part we built a bare-boned dynamic array called <code>IntList</code>. The goal of the data structure was to store a dynamic number of values. Although the algorithm we used would work for any type of data, our implementation was tied to <code>i64</code> values. Enter generics, the goal of which is to abstract algorithms and data structures from specific types.</p>
<p>Many languages implement generics with special syntax and generic-specific rules. With Zig, generics are less of a specific feature and more of an expression of what the language is capable of. Specifically, generics leverage Zig's powerful compile-time metaprogramming.</p>
<p>We'll begin by looking at a silly example, just to get our bearings:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var arr: IntArray(3) = undefined;
arr[0] = 1;
arr[1] = 10;
arr[2] = 100;
std.debug.print("{any}\n", .{arr});
}
fn IntArray(comptime length: usize) type {
return [length]i64;
}</code></pre>
<p>The above prints <code>{ 1, 10, 100 }</code>. The interesting part is that we have a function that returns a <code>type</code> (hence the function is PascalCase). Not just any type either, but a type based on a function parameter. This code only worked because we declared <code>length</code> as <code>comptime</code>. That is, we require anyone who calls <code>IntArray</code> to pass a compile-time known <code>length</code> parameter. This is necessary because our function returns a <code>type</code> and <code>types</code> must always be compile-time known.</p>
<p>A function can return <em>any</em> type, not just primitives and arrays. For example, with a small change, we can make it return a structure:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var arr: IntArray(3) = undefined;
arr.items[0] = 1;
arr.items[1] = 10;
arr.items[2] = 100;
std.debug.print("{any}\n", .{arr.items});
}
fn IntArray(comptime length: usize) type {
return struct {
items: [length]i64,
};
}</code></pre>
<p>It might seem odd, but <code>arr</code>'s type really is an <code>IntArray(3)</code>. It's a type like any other type and <code>arr</code> is a value like any other value. If we called <code>IntArray(7)</code> that would be a different type. Maybe we can make things neater:</p><pre><code>
const std = @import("std");
pub fn main() !void {
var arr = IntArray(3).init();
arr.items[0] = 1;
arr.items[1] = 10;
arr.items[2] = 100;
std.debug.print("{any}\n", .{arr.items});
}
fn IntArray(comptime length: usize) type {
return struct {
items: [length]i64,
fn init() IntArray(length) {
return .{
.items = undefined,
};
}
};
}</code></pre>
<p>At first glance that might not look neater. But besides being nameless and nested in a function, our structure's looking like every other structure we've seen so far. It has fields, it has functions. You know what they say, <em>if it looks like a duck...</em>. Well, this looks, swims and quacks like a normal structure, because it is.</p>
<p>We've taken this route to get comfortable with a function that returns a type and the accompanying syntax. To get a more typical generic, we need to make one last change: our function has to take a <code>type</code>. In reality, this is a small change, but <code>type</code> can feel more abstract than <code>usize</code>, so we took it slowly. Let's make a leap and modify our previous <code>IntList</code> to work with any type. We'll start with a skeleton:</p><pre><code>
fn List(comptime T: type) type {
return struct {
pos: usize,
items: []T,
allocator: Allocator,
fn init(allocator: Allocator) !List(T) {
return .{
.pos = 0,
.allocator = allocator,
.items = try allocator.alloc(T, 4),
};
}
};
}</code></pre>
<p>The above <code>struct</code> is almost identical to our <code>IntList</code> except <code>i64</code> has been replaced with <code>T</code>. That <code>T</code> might seem special, but it's just a variable name. We could have called it <code>item_type</code>. However, following Zig's naming convention, variables of type <code>type</code> are PascalCase.</p>
<aside>For good or bad, using a single letter to represent a type parameter is much older than Zig. <code>T</code> is a common default in most languages, but you will see context-specific variations, such as hash maps using <code>K</code> and <code>V</code> for their key and value parameter types.</aside>
<p>If you aren't sure about our skeleton, consider the two places we use <code>T</code>: <code>items: []T</code> and <code>allocator.alloc(T, 4)</code>. When we want to use this generic type, we'll create an instance using:</p><pre><code>
var list = try List(u32).init(allocator);</code></pre>
<p>When the code gets compiled, the compiler creates a new type by finding every <code>T</code> and replacing it with <code>u32</code>. If we use <code>List(u32)</code> again, the compiler will re-use the type it previous created. If we specify a new value for <code>T</code>, say <code>List(bool)</code> or <code>List(User)</code>, new types will be created.</p>
<p>To complete our generic <code>List</code> we can literally copy and paste the rest of the <code>IntList</code> code and replace <code>i64</code> with <code>T</code>. Here's a full working example:</p><pre><code>
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var list = try List(u32).init(allocator);
defer list.deinit();
for (0..10) |i| {
try list.add(@intCast(i));
}
std.debug.print("{any}\n", .{list.items[0..list.pos]});
}
fn List(comptime T: type) type {
return struct {
pos: usize,
items: []T,
allocator: Allocator,
fn init(allocator: Allocator) !List(T) {
return .{
.pos = 0,
.allocator = allocator,
.items = try allocator.alloc(T, 4),
};
}
fn deinit(self: List(T)) void {
self.allocator.free(self.items);
}
fn add(self: *List(T), value: T) !void {
const pos = self.pos;
const len = self.items.len;
if (pos == len) {
// we've run out of space
// create a new slice that's twice as large
var larger = try self.allocator.alloc(T, len * 2);
// copy the items we previously added to our new space
@memcpy(larger[0..len], self.items);
self.allocator.free(self.items);
self.items = larger;
}
self.items[pos] = value;
self.pos = pos + 1;
}
};
}</code></pre>
<p>Our <code>init</code> function returns a <code>List(T)</code>, and our <code>deinit</code> and <code>add</code> functions take a <code>List(T)</code> and <code>*List(T)</code>. In our simple class, that's fine, but for large data structures, writing the full generic name can become a little tedious, especially if we have multiple type parameters (e.g. a hash map that takes a separate <code>type</code> for its key and value). The <code>@This()</code> builtin function returns the innermost <code>type</code> from where it's called. Most likely, our <code>List(T)</code> would be written as:</p><pre><code>
fn List(comptime T: type) type {
return struct {
pos: usize,
items: []T,
allocator: Allocator,
// Added
const Self = @This();
fn init(allocator: Allocator) !Self {
// ... same code
}
fn deinit(self: Self) void {
// .. same code
}
fn add(self: *Self, value: T) !void {
// .. same code
}
};
}</code></pre>
<p><code>Self</code> isn't a special name, it's just a variable, and it's PascalCase because its value is a <code>type</code>. We can use <code>Self</code> where we had previously used <code>List(T)</code>.</p>
</section>
<section>
<p>We could create more complex examples, with multiple type parameters and more advanced algorithms. But, in the end, the core generic code would be no different than the simple examples above. In the next part we'll touch on generics again when we look at the standard library's <code>ArrayList(T)</code> and <code>StringHashMap(V)</code>.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/heap_memory/">Heap Memory & Allocators</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/coding_in_zig/">Coding In Zig</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Heap Memory & Allocators2023-09-08T00:00:00Z/learning_zig/heap_memory/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/stack_memory/">Stack Memory</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/generics/">Generics</a>
</div>
<article>
<section>
<h1 tabindex="-1" id="heap_memory"><a href="https://www.openmymind.net/#heap_memory" aria-hidden="true">Heap Memory and Allocators</a></h1>
<p>Everything we've seen so far has been constrained by requiring an upfront size. Arrays always have a compile-time known length (in fact, the length is part of the type). All of our strings have been string literals, which have a compile-time known length.</p>
<p>Furthermore, the two types of memory management strategies we've seen, global data and the call stack, while simple and efficient, are limiting. Neither can deal with dynamically sized data and both are rigid with respect to data lifetimes.</p>
<p>This part is divided into two themes. The first is a general overview of our third memory area, the heap. The other is Zig's straightforward but unique approach to managing heap memory. Even if you're familiar with heap memory, say from using C's <code>malloc</code>, you'll want to read the first part as it is quite specific to Zig.</p>
</section>
<section>
<h2 tabindex="-1" id="heap"><a href="https://www.openmymind.net/#heap" aria-hidden="true">The Heap</a></h2>
<p>The heap is the third and final memory area at our disposal. Compared to both global data and the call stack, the heap is a bit of a wild west: anything goes. Specifically, within the heap, we can create memory at runtime with a runtime known size and have complete control over its lifetime.</p>
<p>The call stack is amazing because of the simple and predictable way it manages data (by pushing and popping stack frames). This benefit is also a drawback: data has a lifetime tied to its place on the call stack. The heap is the exact opposite. It has no built-in life cycle, so our data can live for as long or as short as necessary. And that benefit is its drawback: it has no built-in life cycle, so if we don't free data no one will.</p>
<p>Let's look at an example:</p><pre><code>
const std = @import("std");
pub fn main() !void {
// we'll be talking about allocators shortly
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// ** The next two lines are the important ones **
var arr = try allocator.alloc(usize, try getRandomCount());
defer allocator.free(arr);
for (0..arr.len) |i| {
arr[i] = i;
}
std.debug.print("{any}\n", .{arr});
}
fn getRandomCount() !u8 {
var seed: u64 = undefined;
try std.posix.getrandom(std.mem.asBytes(&seed));
var random = std.rand.DefaultPrng.init(seed);
return random.random().uintAtMost(u8, 5) + 5;
}</code></pre>
<p>We'll cover Zig allocators shortly, for now know that the <code>allocator</code> is a <code>std.mem.Allocator</code>. We're using two of its methods: <code>alloc</code> and <code>free</code>. Because we're calling <code>allocator.alloc</code> with a <code>try</code>, we know that it can fail. Currently, the only possible error is <code>OutOfMemory</code>. Its parameters mostly tell us how it works: it wants a type (<code>T</code>) as well as a count and, on success, returns a slice of <code>[]T</code>. This allocation happens at runtime - it has to, our count is only known at runtime.</p>
<p>As a general rule, every <code>alloc</code> will have a corresponding <code>free</code>. Where <code>alloc</code> allocates memory, <code>free</code> releases it. Don't let this simple code limit your imagination. This <code>try alloc</code> + <code>defer free</code> pattern <em>is</em> common, and for good reason: freeing close to where we allocate is relatively foolproof. But equally common is allocating in one place while freeing in another. As we said before, the heap has no builtin life cycle management. You can allocate memory in an HTTP handler and free it in a background thread, two completely separate parts of the code.</p>
</section>
<section>
<h2 tabindex="-1" id="defer"><a href="https://www.openmymind.net/#defer" aria-hidden="true">defer & errdefer</a></h2>
<p>As a small detour, the above code introduced a new language feature: <code>defer</code> which executes the given code, or block, on scope exit. "Scope exit" includes reaching the end of the scope or returning from the scope. <code>defer</code> isn't strictly related to allocators or memory management; you can use it to execute any code. But the above usage is common.</p>
<p>Zig's defer is similar to Go's, with one major difference. In Zig, the defer will be run at the end of its containing scope. In Go, defer is run at the end of the containing function. Zig's approach is probably less surprising, unless you're a Go developer.</p>
<p>A relative of <code>defer</code> is <code>errdefer</code> which similarly executes the given code or block on scope exit, but only when an error is returned. This is useful when doing more complex setup and having to undo a previous allocation because of an error.</p>
<p>The following example is a jump in complexity. It showcases both <code>errdefer</code> and a common pattern that sees <code>init</code> allocating and <code>deinit</code> freeing:</p><pre><code>
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Game = struct {
players: []Player,
history: []Move,
allocator: Allocator,
fn init(allocator: Allocator, player_count: usize) !Game {
var players = try allocator.alloc(Player, player_count);
errdefer allocator.free(players);
// store 10 most recent moves per player
var history = try allocator.alloc(Move, player_count * 10);
return .{
.players = players,
.history = history,
.allocator = allocator,
};
}
fn deinit(game: Game) void {
const allocator = game.allocator;
allocator.free(game.players);
allocator.free(game.history);
}
};</code></pre>
<p>Hopefully this highlights two things. First, the usefulness of <code>errdefer</code>. Under normal conditions, <code>players</code> is allocated in <code>init</code> and released in <code>deinit</code>. But there's an edge case when the initialization of <code>history</code> fails. In this case and only this case we need to undo the allocation of <code>players</code>.</p>
<p>The second noteworthy aspect of this code is that the life cycle of our two dynamically allocated slices, <code>players</code> and <code>history</code>, is based on our application logic. There's no rule that dictates when <code>deinit</code> has to be called or who has to call it. This is good, because it gives us arbitrary lifetimes, but bad because we can mess it up by never calling <code>deinit</code> or calling it more than once.</p>
<aside>The names <code>init</code> and <code>deinit</code> aren't special. They're just what the Zig standard library uses and what the community has adopted. In some cases, including in the standard library, <code>open</code> and <code>close</code>, or other more appropriate names, are used.</aside>
</section>
<section>
<h2 tabindex="-1" id="memory_leaks"><a href="https://www.openmymind.net/#memory_leaks" aria-hidden="true">Double Free & Memory Leaks</a></h2>
<p>Just above, I mentioned that there were no rules that govern when something has to be freed. But that's not entirely true, there are a few important rules, they're just not enforced except by your own meticulousness.</p>
<p>The first rule is that you can't free the same memory twice.</p><pre><code>
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arr = try allocator.alloc(usize, 4);
allocator.free(arr);
allocator.free(arr);
std.debug.print("This won't get printed\n", .{});
}</code></pre>
<p>The last line of this code is prophetic, it <em>won't</em> be printed. This is because we <code>free</code> the same memory twice. This is known as a double-free and is not valid. This might seem simple enough to avoid, but in large projects with complex lifetimes, it can be hard to track down.</p>
<p>The second rule is that you can't free memory you don't have a reference to. That might sound obvious, but it isn't always clear who is responsible for freeing it. The following creates a new lowercase string:</p><pre><code>
const std = @import("std");
const Allocator = std.mem.Allocator;
fn allocLower(allocator: Allocator, str: []const u8) ![]const u8 {
var dest = try allocator.alloc(u8, str.len);
for (str, 0..) |c, i| {
dest[i] = switch (c) {
'A'...'Z' => c + 32,
else => c,
};
}
return dest;
}</code></pre>
<p>The above code is fine. But the following usage isn't:</p><pre><code>
// For this specific code, we should have used std.ascii.eqlIgnoreCase
fn isSpecial(allocator: Allocator, name: [] const u8) !bool {
const lower = try allocLower(allocator, name);
return std.mem.eql(u8, lower, "admin");
}</code></pre>
<p>This is a memory leak. The memory created in <code>allocLower</code> is never freed. Not only that, but once <code>isSpecial</code> returns, it can never be freed. In languages with garbage collectors, when data becomes unreachable, it'll eventually be freed by the garbage collector. But in the above code, once <code>isSpecial</code> returns, we lose our only reference to the allocated memory, the <code>lower</code> variable. The memory is gone until our process exits. Our function might only leak a few bytes, but if it's a long running process and this function is called repeatedly, it <em>will</em> add up and we'll eventually run out of memory.</p>
<p>At least in the case of double free, we'll get a hard crash. Memory leaks can be insidious. It isn't just that the root cause can be difficult to identify. Really small leaks or leaks in infrequently executed code, can be even harder to detect. This is such a common problem that Zig does provide help, which we'll see when talking about allocators.</p>
</section>
<section>
<h2 tabindex="-1" id="create"><a href="https://www.openmymind.net/#create" aria-hidden="true">create & destroy</a></h2>
<p>The <code>alloc</code> method of <code>std.mem.Allocator</code> returns a slice with the length that was passed as the 2nd parameter. If you want a single value, you'll use <code>create</code> and <code>destroy</code> instead of <code>alloc</code> and <code>free</code>. A few parts back, when learning about pointers, we created a <code>User</code> and tried to increment its power. Here's the working heap-based version of that code using <code>create:</code></p><pre><code>
const std = @import("std");
pub fn main() !void {
// again, we'll talk about allocators soon!
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// create a User on the heap
var user = try allocator.create(User);
// free the memory allocated for the user at the end of this scope
defer allocator.destroy(user);
user.id = 1;
user.power = 100;
// this line has been added
levelUp(user);
std.debug.print("User {d} has power of {d}\n", .{user.id, user.power});
}
fn levelUp(user: *User) void {
user.power += 1;
}
pub const User = struct {
id: u64,
power: i32,
};</code></pre>
<p>The <code>create</code> method takes a single parameter, the type (<code>T</code>). It returns a pointer to that type or an error, i.e. <code>!*T</code>. Maybe you're wondering what would happen if we created our <code>user</code> but didn't set the <code>id</code> and/or <code>power</code>. This is like setting those fields to <code>undefined</code> and the behavior is, well, undefined.</p>
<p>When we explored dangling pointers, we had a function incorrectly return the address of the local user:</p><pre><code>
pub const User = struct {
fn init(id: u64, power: i32) *User{
var user = User{
.id = id,
.power = power,
};
// this is a dangling pointer
return &user;
}
};</code></pre>
<p>In this case, it might have made more sense to return a <code>User</code>. But sometimes you <em>will</em> want a function to return a pointer to something it creates. You'll do this when you want a lifetime to be free from the call stack's rigidity. To solve our dangling pointer above, we could have used <code>create</code>:</p><pre><code>
// our return type changed, since init can now fail
// *User -> !*User
fn init(allocator: std.mem.Allocator, id: u64, power: i32) !*User{
const user = try allocator.create(User);
user.* = .{
.id = id,
.power = power,
};
return user;
}</code></pre>
<p>I've introduced new syntax, <code>user.* = .{...}</code>. It's a bit weird, and I don't love it, but you will see it. The right side is something you've already seen: it's a structure initializer with an inferred type. We could have been explicit and used: <code>user.* = User{...}</code>. The left side, <code>user.*</code>, is how we dereference a pointer. <code>&</code> takes a <code>T</code> and gives us <code>*T</code>. <code>.*</code> is the opposite, applied to a value of type <code>*T</code> it gives us <code>T</code>. Remember that <code>create</code> returns a <code>!*User</code>, so our <code>user</code> is of type <code>*User</code>.</p>
</section>
<section>
<h2 tabindex="-1" id="allocators"><a href="https://www.openmymind.net/#allocators" aria-hidden="true">Allocators</a></h2>
<p>One of Zig's core principle is <em>no hidden memory allocations</em>. Depending on your background, that might not sound too special. But it's a sharp contrast to what you'll find in C where memory is allocated with the standard library's <code>malloc</code> function. In C, if you want to know whether or not a function allocates memory, you need to read the source and look for calls to <code>malloc</code>.</p>
<p>Zig doesn't have a default allocator. In all of the above examples, functions that allocated memory took an <code>std.mem.Allocator</code> parameter. By convention, this is usually the first parameter. All of Zig's standard library, and most third party libraries, require the caller to provide an allocator if they intend to allocate memory.</p>
<p>This explicitness can take one of two forms. In simple cases, the allocator is provided on each function call. There are many examples of this, but <code>std.fmt.allocPrint</code> is one you'll likely need sooner or later. It's similar to the <code>std.debug.print</code> we've been using, but allocates and returns a string instead of writing it to stderr:</p><pre><code>
const say = std.fmt.allocPrint(allocator, "It's over {d}!!!", .{user.power});
defer allocator.free(say);</code></pre>
<p>The other form is when an allocator is passed to <code>init</code>, and then used internally by the object. We saw this above with our <code>Game</code> structure. This is less explicit, since you've given the object an allocator to use, but you don't know which method calls will actually allocate. This approach is more practical for long-lived objects.</p>
<p>The advantage of injecting the allocator isn't just explicitness, it's also flexibility. <code>std.mem.Allocator</code> is an interface which provides the <code>alloc</code>, <code>free</code>, <code>create</code> and <code>destroy</code> functions, along with a few others. So far we've only seen the <code>std.heap.GeneralPurposeAllocator</code>, but other implementations are available in the standard library or as third party libraries.</p>
<aside>Zig doesn't have nice syntactical sugar for creating interfaces. One pattern for interface-life behaviour are tagged unions, though that's relatively constrained compared to true interfaces. Other patterns have emerged and are used throughout the standard library, such as with <code>std.mem.Allocator</code>. If you're interested, I've written <a href="https://www.openmymind.net/Zig-Interfaces/">a separate blog post explaining interfaces</a>.</aside>
<p>If you're building a library, then it's best to accept an <code>std.mem.Allocator</code> and let users of your library decide which allocator implementation to use. Otherwise, you'll need to chose the right allocator, and, as we'll see, these are not mutually exclusive. There can be good reasons to create different allocators within your program.</p>
</section>
<section>
<h2 tabindex="-1" id="gpa"><a href="https://www.openmymind.net/#gpa" aria-hidden="true">General Purpose Allocator</a></h2>
<p>As the name implies, the <code>std.heap.GeneralPurposeAllocator</code> is an all around "general purpose", thread-safe allocator that can serve as your application's main allocator. For many programs, this will be the only allocator needed. On program start, an allocator is created and passed to functions that need it. The sample code from my HTTP server library is a good example:</p><pre><code>
const std = @import("std");
const httpz = @import("httpz");
pub fn main() !void {
// create our general purpose allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// get an std.mem.Allocator from it
const allocator = gpa.allocator();
// pass our allocator to functions and libraries that require it
var server = try httpz.Server().init(allocator, .{.port = 5882});
var router = server.router();
router.get("/api/user/:id", getUser);
// blocks the current thread
try server.listen();
}</code></pre>
<p>We create the <code>GeneralPurposeAllocator</code>, get an <code>std.mem.Allocator</code> from it and pass it to the <code>init</code> function of the HTTP server. In a more complex project, <code>allocator</code> would get passed to multiple parts of the code, each of those possibly passing it to their own functions, objects and dependencies.</p>
<p>You might notice that the syntax around the creation of <code>gpa</code> is a little strange. What is this: <code>GeneralPurposeAllocator(.{}){}</code>? It's all things we've seen before, just smashed together. <code>std.heap.GeneralPurposeAllocator</code> is a function, and since it's using PascalCase, we know that it returns a type. (We'll talk more about generics in the next part). Knowing that it returns a type, maybe this more explicit version will be easier to decipher:</p><pre><code>
const T = std.heap.GeneralPurposeAllocator(.{});
var gpa = T{};
// is the same as:
var gpa = std.heap.GeneralPurposeAllocator(.{}){};</code></pre>
<p>Maybe you're still unsure about the meaning of <code>.{}</code>. This is also something we've seen before: it's a struct initializer with an implicit type. What's the type and where are the fields? The type is an <code>std.heap.general_purpose_allocator.Config</code> though it isn't directly exposed like this, which is one reason we aren't explicit. No fields are set because the <code>Config</code> struct defines defaults, which we'll be using. This is a common pattern with configuration / options. In fact, we see it again a few lines down when we pass <code>.{.port = 5882}</code> to <code>init</code>. In this case, we're using the default value for all but one field, the <code>port</code>.</p>
</section>
<section>
<h2 tabindex="-1" id="testing"><a href="https://www.openmymind.net/#testing" aria-hidden="true">std.testing.allocator</a></h2>
<p>Hopefully you were sufficiently troubled when we talked about memory leaks and then eager to learn more when I mentioned that Zig could help. This help comes from the <code>std.testing.allocator</code>, which is an <code>std.mem.Allocator</code>. Currently it's implemented using the <code>GeneralPurposeAllocator</code> with added integration in Zig's test runner, but that's an implementation detail. The important thing is that if we use <code>std.testing.allocator</code> in our tests, we can catch most memory leaks.</p>
<p>You're likely already familiar with dynamic arrays, often called ArrayLists. In many dynamic programming languages all arrays are dynamic arrays. Dynamic arrays support a variable number of elements. Zig has a proper generic ArrayList, but we'll create one specifically to hold integers and to demonstrate leak detection:</p><pre><code>
pub const IntList = struct {
pos: usize,
items: []i64,
allocator: Allocator,
fn init(allocator: Allocator) !IntList {
return .{
.pos = 0,
.allocator = allocator,
.items = try allocator.alloc(i64, 4),
};
}
fn deinit(self: IntList) void {
self.allocator.free(self.items);
}
fn add(self: *IntList, value: i64) !void {
const pos = self.pos;
const len = self.items.len;
if (pos == len) {
// we've run out of space
// create a new slice that's twice as large
var larger = try self.allocator.alloc(i64, len * 2);
// copy the items we previously added to our new space
@memcpy(larger[0..len], self.items);
self.items = larger;
}
self.items[pos] = value;
self.pos = pos + 1;
}
};</code></pre>
<p>The interesting part happens in <code>add</code> when <code>pos == len</code> indicating that we've filled our current array and need to create a larger one. We can use <code>IntList</code> like so:</p><pre><code>
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var list = try IntList.init(allocator);
defer list.deinit();
for (0..10) |i| {
try list.add(@intCast(i));
}
std.debug.print("{any}\n", .{list.items[0..list.pos]});
}</code></pre>
<p>The code runs and prints the correct result. However, even though we <em>did</em> call <code>deinit</code> on <code>list</code>, there's a memory leak. It's ok if you didn't catch it because we're going to write a test and use <code>std.testing.allocator</code>:</p><pre><code>
const testing = std.testing;
test "IntList: add" {
// We're using testing.allocator here!
var list = try IntList.init(testing.allocator);
defer list.deinit();
for (0..5) |i| {
try list.add(@intCast(i+10));
}
try testing.expectEqual(@as(usize, 5), list.pos);
try testing.expectEqual(@as(i64, 10), list.items[0]);
try testing.expectEqual(@as(i64, 11), list.items[1]);
try testing.expectEqual(@as(i64, 12), list.items[2]);
try testing.expectEqual(@as(i64, 13), list.items[3]);
try testing.expectEqual(@as(i64, 14), list.items[4]);
}</code></pre>
<aside><code>@as</code> is a built-in that performs type coercion. If you're wondering why our test had to use so many of them, you aren't the only one. Technically it's because the second parameter, the "actual", is coerced to the first, the "expected". In the above, our "expected" are all <code>comptime_int</code>, which causes issues. Many, myself included, find this a <a href="https://github.com/ziglang/zig/issues/4437">strange and unfortunate behavior</a>.</aside>
<p>If you're following along, place the test in the same file as <code>IntList</code> and <code>main</code>. Zig tests are normally written in the same file, often near the code, they're testing. When we use <code>zig test learning.zig</code> to run our test, we get an amazing failure:</p><pre><code>
Test [1/1] test.IntList: add... [gpa] (err): memory address 0x101154000 leaked:
/code/zig/learning.zig:26:32: 0x100f707b7 in init (test)
.items = try allocator.alloc(i64, 2),
^
/code/zig/learning.zig:55:29: 0x100f711df in test.IntList: add (test)
var list = try IntList.init(testing.allocator);
... MORE STACK INFO ...
[gpa] (err): memory address 0x101184000 leaked:
/code/test/learning.zig:40:41: 0x100f70c73 in add (test)
var larger = try self.allocator.alloc(i64, len * 2);
^
/code/test/learning.zig:59:15: 0x100f7130f in test.IntList: add (test)
try list.add(@intCast(i+10));</code></pre>
<p>We have multiple memory leaks. Thankfully the testing allocator tells us exactly where the leaking memory was allocated. Are you able to spot the leak now? If not, remember that, in general, every <code>alloc</code> should have a corresponding <code>free</code>. Our code calls <code>free</code> once, in <code>deinit</code>. However, <code>alloc</code> is called once in <code>init</code> and then every time <code>add</code> is called and we need more space. Every time we <code>alloc</code> more space, we need to <code>free</code> the previous <code>self.items</code>:</p><pre><code>
// existing code
var larger = try self.allocator.alloc(i64, len * 2);
@memcpy(larger[0..len], self.items);
// Added code
// free the previous allocation
self.allocator.free(self.items);</code></pre>
<p>Adding this last line, after copying the items to our <code>larger</code> slice, solves the problem. If you run <code>zig test learning.zig</code>, there should be no error.</p>
</section>
<section>
<h2 tabindex="-1" id="arena"><a href="https://www.openmymind.net/#arena" aria-hidden="true">ArenaAllocator</a></h2>
<p>The GeneralPurposeAllocator is a reasonable default because it works well in all possible cases. But within a program, you might run into allocation patterns which can benefit from more specialized allocators. One example is the need for short-lived state which can be discarded when processing is completed. Parsers often have such a requirement. A skeleton <code>parse</code> function might look like:</p><pre><code>
fn parse(allocator: Allocator, input: []const u8) !Something {
const state = State{
.buf = try allocator.alloc(u8, 512),
.nesting = try allocator.alloc(NestType, 10),
};
defer allocator.free(state.buf);
defer allocator.free(state.nesting);
return parseInternal(allocator, state, input);
}</code></pre>
<p>While this isn't too hard to manage, <code>parseInternal</code> might need other short lived allocations which will need to be freed. As an alternative, we could create an ArenaAllocator which allows us to free all allocations in one shot:</p><pre><code>
fn parse(allocator: Allocator, input: []const u8) !Something {
// create an ArenaAllocator from the supplied allocator
var arena = std.heap.ArenaAllocator.init(allocator);
// this will free anything created from this arena
defer arena.deinit();
// create an std.mem.Allocator from the arena, this will be
// the allocator we'll use internally
const aa = arena.allocator();
const state = State{
// we're using aa here!
.buf = try aa.alloc(u8, 512),
// we're using aa here!
.nesting = try aa.alloc(NestType, 10),
};
// we're passing aa here, so we're guaranteed that
// any other allocation will be in our arena
return parseInternal(aa, state, input);
}</code></pre>
<p>The <code>ArenaAllocator</code> takes a child allocator, in this case the allocator that was passed into <code>init</code>, and creates a new <code>std.mem.Allocator</code>. When this new allocator is used to allocate or create memory, we don't need to call <code>free</code> or <code>destroy</code>. Everything will be released when we call <code>deinit</code> on the <code>arena</code>. In fact, the <code>free</code> and <code>destroy</code> of an ArenaAllocator do nothing.</p>
<p>The <code>ArenaAllocator</code> has to be used carefully. Since there's no way to free individual allocations, you need to be sure that the arena's <code>deinit</code> will be called within a reasonable memory growth. Interestingly, that knowledge can either be internal or external. For example, in our above skeleton, leveraging an ArenaAllocator makes sense from within the Parser since the details of the state's lifetime is an internal matter.</p>
<aside>Allocators like the ArenaAllocator that have a mechanism for freeing all previous allocations can break the rule that every <code>alloc</code> should have a corresponding <code>free</code>. However, if you receive an <code>std.mem.Allocator</code> you should not make any assumptions about the underlying implementation.</aside>
<p>The same can't be said for our <code>IntList</code>. It can be used to store 10 or 10 million values. It can have a lifetime measured in milliseconds or weeks. It's in no position to decide the type of allocator to use. It's the code making use of <code>IntList</code> that has this knowledge. Originally, we managed our <code>IntList</code> like so:</p><pre><code>
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var list = try IntList.init(allocator);
defer list.deinit();</code></pre>
<p>We could have opted to supply an ArenaAllocator instead:</p><pre><code>
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const aa = arena.allocator();
var list = try IntList.init(aa);
// I'm honestly torn on whether or not we should call list.deinit.
// Technically, we don't have to since we call defer arena.deinit() above.
defer list.deinit();
...</code></pre>
<p>We don't need to change <code>IntList</code> since it only deals with an <code>std.mem.Allocator</code>. And if <code>IntList</code> <em>did</em> internally create its own arena, that would work too. There's no reason you can't create an arena within an arena.</p>
<p>As a last quick example, the HTTP server I mentioned above exposes an arena allocator on the <code>Response</code>. Once the response is sent, the arena is cleared. The predictable lifetime of the arena (from request start to request end), makes it an efficient option. Efficient in terms of performance and ease of use.</p>
</section>
<section>
<h2 tabindex="-1" id="fixedbuffer"><a href="https://www.openmymind.net/#fixedbuffer" aria-hidden="true">FixedBufferAllocator</a></h2>
<p>The last allocator that we'll look at is the <code>std.heap.FixedBufferAllocator</code> which allocates memory from a buffer (i.e. <code>[]u8</code>) that we provide. This allocator has two major benefits. First, since all the memory it could possibly use is created upfront, it's fast. Second, it naturally limits how much memory can be allocated. This hard limit can also be seen as a drawback. Another drawbacks is that <code>free</code> and <code>destroy</code> will only work on the last allocated/created item (think of a stack). Freeing the non-last allocation is safe to call, but won't do anything.</p><pre><code>
const std = @import("std");
pub fn main() !void {
var buf: [150]u8 = undefined;
var fa = std.heap.FixedBufferAllocator.init(&buf);
// this will free all memory allocate with this allocator
defer fa.reset();
const allocator = fa.allocator();
const json = try std.json.stringifyAlloc(allocator, .{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2});
// We can free this allocation, but since we know that our allocator is
// a FixedBufferAllocator, we can rely on the above `defer fa.reset()`
defer allocator.free(json);
std.debug.print("{s}\n", .{json});
}</code></pre>
<p>The above prints:</p><pre><code>
{
"this_is": "an anonymous struct",
"above": true,
"last_param": "are options"
}</code></pre>
<p>But change our <code>buf</code> to be a <code>[120]u8</code> and you'll get an <code>OutOfMemory</code> error.</p>
<p>A common pattern with FixedBufferAllocators, and to a lesser degree ArenaAllocators, is to <code>reset</code> them and reuse them. This frees all previous allocations and allows the allocator to be reused.</p>
</section>
<section>
<p>By not having a default allocator, Zig is both transparent and flexible with respect to allocations. The <code>std.mem.Allocator</code> interface is powerful, allowing specialized allocators to wrap more general ones, as we saw with the <code>ArenaAllocator</code>.</p>
<p>More generally, the power and associated responsibilities of heap allocations are hopefully apparent. The ability to allocate arbitrary sized memory with an arbitrary lifetime is essential to most programs.</p>
<p>However, because of the complexity that comes with dynamic memory, you should keep an eye open for alternatives. For example, above we used <code>std.fmt.allocPrint</code> but the standard library also has an <code>std.fmt.bufPrint</code>. The latter takes a buffer instead of an allocator:</p><pre><code>
const std = @import("std");
pub fn main() !void {
const name = "Leto";
var buf: [100]u8 = undefined;
const greeting = try std.fmt.bufPrint(&buf, "Hello {s}", .{name});
std.debug.print("{s}\n", .{greeting});
}</code></pre>
<p>This API moves the memory management burden to the caller. If we had a longer <code>name</code>, or a smaller <code>buf</code>, our <code>bufPrint</code> could return an <code>NoSpaceLeft</code> error. But there are plenty of scenarios where an application has known limits, such as a maximum name length. In those cases <code>bufPrint</code> is safer and faster.</p>
<p>Another possible alternative to dynamic allocations is streaming data to an <code>std.io.Writer</code>. Like our <code>Allocator</code>, <code>Writer</code> is an interface implemented by many types, such as files. Above, we used <code>stringifyAlloc</code> to serialize JSON into a dynamically allocated string. We could have used <code>stringify</code> and provided a <code>Writer</code>:</p><pre><code>
const std = @import("std");
pub fn main() !void {
const out = std.io.getStdOut();
try std.json.stringify(.{
.this_is = "an anonymous struct",
.above = true,
.last_param = "are options",
}, .{.whitespace = .indent_2}, out.writer());
}</code></pre>
<aside>While allocators are often given as the first parameter of a function, writers are normally the last. <span style="font-family: monospace;">ಠ_ಠ</span></aside>
<p>In many cases, wrapping our writer in an <code>std.io.BufferedWriter</code> would give a nice performance boost.</p>
<p>The goal isn't to eliminate all dynamic allocations. It wouldn't work, as these alternatives only make sense in specific cases. But now you have many options at your disposal. From stack frames to a general purpose allocator, and all the things in between, like static buffers, streaming writers and specialized allocators.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/stack_memory/">Stack Memory</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/generics/">Generics</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Stack Memory2023-09-08T00:00:00Z/learning_zig/stack_memory/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/pointers/">Pointers</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/heap_memory/">Heap Memory & Allocators</a>
</div>
<article>
<section>
<h1 tabindex="-1" id="stack_memory"><a href="https://www.openmymind.net/#stack_memory" aria-hidden="true">Stack Memory</a></h1>
<p>Diving into pointers provided insight into the relationship between variables, data and memory. So we're getting a sense of what the memory looks like, but we've yet to talk about how data and, by extension, memory is managed. For short lived and simple scripts, this likely doesn't matter. In an age of 32GB laptop, you can start your program, use a few hundred megabytes of RAM reading a file and parsing an HTTP response, do something amazing, and exit. On program exit, the OS knows that whatever memory it gave your program can now be used for something else.</p>
<p>But for programs that run for days, months or even years, memory becomes a limited and precious resource, likely sought after by other processes running on the same machine. There's simply no way to wait until the program exits to free memory. This is a garbage collector's primary job: knowing what data is no longer in-use and freeing its memory. In Zig, you're the garbage collector.</p>
<p>Most of the programs you write will make use of three "areas" of memory. The first is global space, which is where program constants, including string literals, are stored. All global data is baked into the binary, fully known at compile time (and thus runtime) and immutable. This data exists throughout the lifetime of the program, never needing more or less memory. Aside from the impact it has on the size of our binary, this isn't something we need to worry about at all.</p>
<p>The second area of memory is the call stack, the topic for this part. The third area is the heap, the topic for our next part.</p>
<aside>There isn't a real physical difference between the areas of memory, it's a concept created by the OS and the executable.</aside>
</section>
<section>
<h2 tabindex="-1" id="stack_frames"><a href="https://www.openmymind.net/#stack_frames" aria-hidden="true">Stack Frames</a></h2>
<p>All of the data we've seen so far have been constants stored in the global data section of our binary or local variables. "Local" indicates that the variable is only valid within the scope where it's declared. In Zig, scopes begin and end with curly braces, <code>{ ... }</code>. Most variables are scoped to a function, including function parameters, or a control-flow block, like an <code>if</code>. But, as we've seen, you can create arbitrary blocks and thus, arbitrary scopes.</p>
<p>In the previous part, we visualized the memory of our <code>main</code> and <code>levelUp</code> functions, each with a <code>User</code>:</p><pre><code>
main: user -> ------------- (id: 1043368d0)
| 1 |
------------- (power: 1043368d8)
| 100 |
------------- (name.len: 1043368dc)
| 4 |
------------- (name.ptr: 1043368e4)
| 1182145c0 |-------------------------
levelUp: user -> ------------- (id: 1043368ec) |
| 1 | |
------------- (power: 1043368f4) |
| 100 | |
------------- (name.len: 1043368f8) |
| 4 | |
------------- (name.ptr: 104336900) |
| 1182145c0 |-------------------------
------------- |
|
............. empty space |
............. or other data |
|
------------- (1182145c0) <---
| 'G' |
-------------
| 'o' |
-------------
| 'k' |
-------------
| 'u' |
-------------</code></pre>
<p>There's a reason <code>levelUp</code> is immediately after <code>main</code>: this is our [simplified] call stack. When our program starts, <code>main</code>, along with its local variables are pushed onto the call stack. When <code>levelUp</code> is called, its parameters and any local variables are pushed onto the call stack. Importantly, when <code>levelUp</code> returns, it's popped off the stack. After <code>levelUp</code> returns and control is back in <code>main</code>, our call stack looks like:</p><pre><code>
main: user -> ------------- (id: 1043368d0)
| 1 |
------------- (power: 1043368d8)
| 100 |
------------- (name.len: 1043368dc)
| 4 |
------------- (name.ptr: 1043368e4)
| 1182145c0 |-------------------------
-------------
|
............. empty space |
............. or other data |
|
------------- (1182145c0) <---
| 'G' |
-------------
| 'o' |
-------------
| 'k' |
-------------
| 'u' |
-------------</code></pre>
<p>When a function is called, its entire stack frame is pushed onto the call stack. This is one of the reasons we need to know the size of every type. While we might not know the length of our user's name until that specific line of code is executed (assuming it wasn't a constant string literal), we do know that our function has a <code>User</code> and, in addition to the other fields, we'll need 8 bytes for <code>name.len</code> and 8 bytes <code>name.ptr</code>.</p>
<p>When the function returns, its stack frame, which was the last pushed onto the call stack, is popped off. Something amazing just happened: the memory used by <code>levelUp</code> has been automatically freed! While technically that memory could be returned to the OS, as far as I know, no implementation actually shrinks the call stack (they will dynamically grow it when necessary though). Still, the memory used to store <code>levelUp</code>'s stack frame is now free to be used within our process for another stack frame.</p>
<aside>In a normal program, the call stack can grow quite large. Between all the framework code and libraries that a typical program uses, you end up with deeply nested functions. Normally, that isn't a problem, but now and again, you might have run into some type of stack overflow error. This happens when our call stack has run out of space. More often than not, this happens with recursive functions - a function that calls itself.</aside>
<p>Like our global data, the call stack is managed by the OS and the executable. On program start, and for each thread we start thereafter, a call stack is created (the size of which can normally be configured in the OS). The call stack exists for the life of the program or, in the case of a thread, the life of the thread. On program or thread exit, the call stack is freed. But where our global data has all of the programs global data, the call stack only has stack frames for the currently executing hierarchy of functions. This is efficient both in terms of memory usage as well as the simplicity of pushing and popping stack frames on and off the stack.</p>
</section>
<section>
<h2 tabindex="-1" id="danling_pointers"><a href="https://www.openmymind.net/#danling_pointers" aria-hidden="true">Dangling Pointers</a></h2>
<p>The call stack is amazing for both its simplicity and efficiency. But it's also frightening: when a function returns, any of its local data becomes inaccessible. That might sound reasonable, it is <em>local</em> data after all, but it can introduce serious issues. Consider this code:</p><pre><code>
const std = @import("std");
pub fn main() void {
const user1 = User.init(1, 10);
const user2 = User.init(2, 20);
std.debug.print("User {d} has power of {d}\n", .{user1.id, user1.power});
std.debug.print("User {d} has power of {d}\n", .{user2.id, user2.power});
}
pub const User = struct {
id: u64,
power: i32,
fn init(id: u64, power: i32) *User{
var user = User{
.id = id,
.power = power,
};
return &user;
}
};</code></pre>
<p>At quick glance, it would be reasonable to expect the following output:</p><pre><code>
User 1 has power of 10
User 2 has power of 20</code></pre>
<p>I got:</p><pre><code>
User 2 has power of 20
User 9114745905793990681 has power of 0</code></pre>
<p>You might get different results, but based on my output, <code>user1</code> has inherited the values of <code>user2</code>, and <code>user2</code> values are nonsensical. The key problem with this code is that <code>User.init</code> returns the address of the local user, <code>&user</code>. This is called a dangling pointer, a pointer that references invalid memory. It's the source of many segfaults.</p>
<p>When a stack frame is popped off the call stack, any references we have to that memory are invalid. The result of trying to access that memory is undefined. You'll likely get nonsense data or a segfault. We could try to make some sense out of my output, but it isn't a behavior we would want to, or even could, rely on.</p>
<p>One challenge with this type of bug is that, in languages with garbage collectors, the above code is perfectly fine. Go for example would detect that the local <code>user</code> outlives its scope, the <code>init</code> function, and would ensure its validity for as long as it's needed (how Go does this is an implementation detail, but it has a few options, including moving the data to the heap, which is what the next part is about).</p>
<p>The other issue, I'm sorry to say, is that it can be a hard to spot bug. In our above example, we're clearly returning the address of a local. But such behavior can hide inside of nested function and complex data types. Do you see any possible issues with the following incomplete code:</p><pre><code>
fn read() !void {
const input = try readUserInput();
return Parser.parse(input);
}</code></pre>
<p>Whatever <code>Parser.parse</code> returns outlives <code>input</code>. If <code>Parser</code> holds a reference to <code>input</code>, that'll be a dangling pointer just waiting to crash our app. Ideally, if <code>Parser</code> needs <code>input</code> to live as long as it does, it will make a copy of it and that copy will be tied to its own lifetime (more on this in the next part). But there's nothing here to enforce this contract. <code>Parser</code>'s documentation might shed some light on what it expects of <code>input</code> or what it does with it. Lacking that, we might need to dig into the code to figure it out.</p>
</section>
<section>
<p>The simple way to solve our initial bug is to change <code>init</code> so that it returns a <code>User</code> rather than a <code>*User</code> (pointer to a <code>User</code>). We'd then be able to <code>return user;</code> rather than <code>return &user;</code>. But that won't always be possible. Data often has to live beyond the rigid boundaries of function scopes. For that we have the third memory area, the heap, the topic of the next part.</p>
<p>Before diving into the heap, know that we'll see one final example of dangling pointers before the end of this guide. At that point, we'll have covered enough of the language to give a sightly less convoluted example. I want to revisit this topic because, for developers coming from garbage collected languages, this is likely to cause bugs and frustration. It is something you <strong>will</strong> get a handle on. It comes down to being aware of where and when data exists.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/pointers/">Pointers</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/heap_memory/">Heap Memory & Allocators</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Pointers2023-09-08T00:00:00Z/learning_zig/pointers/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/style_guide/">Style Guide</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/stack_memory/">Stack Memory</a>
</div>
<article>
<h1 tabindex="-1" id="pointers"><a href="https://www.openmymind.net/#pointers" aria-hidden="true">Pointers</a></h1>
<p>Zig doesn't include a garbage collector. The burden of managing memory is on you, the developer. It's a big responsibility as it has a direct impact on the performance, stability and security of your application.</p>
<p>We'll begin by talking about pointers, which is an important topic to discuss in and of itself, but also to start training ourselves to see our program's data from a memory-oriented point of view. If you're already comfortable with pointers, heap allocations and dangling pointers, feel free to skip ahead a couple of parts to <a href="https://www.openmymind.net/learning_zig/heap_memory/">heap memory & allocators</a>, which is more Zig-specific.</p>
<section>
<p>The following code creates a user with a <code>power</code> of 100, and then calls the <code>levelUp</code> function which increments the user's power by 1. Can you guess the output?</p><pre><code>
const std = @import("std");
pub fn main() void {
var user = User{
.id = 1,
.power = 100,
};
// this line has been added
levelUp(user);
std.debug.print("User {d} has power of {d}\n", .{user.id, user.power});
}
fn levelUp(user: User) void {
user.power += 1;
}
pub const User = struct {
id: u64,
power: i32,
};</code></pre>
<p>That was a unkind trick; the code won't compile: <em>local variable is never mutated</em>. This is in reference to the <code>user</code> variable in <code>main</code>. A variable that is never mutated must be declare <code>const</code>. You might be thinking: but in <code>levelUp</code> we <em>are</em> mutating <code>user</code>, what gives? Let's assume the Zig compiler is mistaken and trick it. We'll force the compiler to see that <code>user</code> is mutated:</p><pre><code>
const std = @import("std");
pub fn main() void {
var user = User{
.id = 1,
.power = 100,
};
user.power += 0;
// rest of the code is the same</code></pre>
<p>Now we get an error in <code>levelUp</code>: <em>cannot assign to constant</em>. We saw in part 1 that function parameters are constants, thus <code>user.power += 1;</code> is not valid. To fix the compile-time error, we could change the <code>levelUp</code> function to:</p><pre><code>
fn levelUp(user: User) void {
var u = user;
u.power += 1;
}</code></pre>
<p>Which will compile, but our output is <em>User 1 has power of 100</em>, even though the intent of our code is clearly for <code>levelUp</code> to increase the user's power to <code>101</code>. What's happening?</p>
<p>To understand, it helps to think about data with respect to memory, and variables as labels that associate a type with a specific memory location. For example, in <code>main</code>, we create a <code>User</code>. A simple visualization of this data in memory would be:</p><pre><code>
user -> ------------ (id)
| 1 |
------------ (power)
| 100 |
------------</code></pre>
<p>There are two important things to note. The first is that our <code>user</code> variable points to the beginning of our structure. The second is that the fields are laid out sequentially. Remember that our <code>user</code> also has a type. That type tells us that <code>id</code> is a 64 bit integer and <code>power</code> is a 32 bit integer. Armed with a reference to the start of our data and the type, the compiler can translate <code>user.power</code> to: <em>access a 32 bit integer located 64 bits from the beginning.</em> That's the power of variables, they reference memory and include the type information necessary to understand and manipulate the memory in a meaningful way.</p>
<aside>By default, Zig doesn't make guarantees about the memory layout of structures. It could store fields in alphabetical order, by ascending size, or with gaps. It can do what it wants, so long as it's able to translate our code correctly. This freedom can enable certain optimizations. Only if we declare a <code>packed struct</code> will we get strong guarantees about the memory layout. We can also create an <code>extern struct</code> which guarantees a that the memory layout will match the C Application Binary Interface (ABI). Still, our visualization of <code>user</code> is reasonable and useful.</aside>
<p>Here's a slightly different visualization which includes memory addresses. The memory address of the start of this data is a random address I came up with. This is the memory address referenced by the <code>user</code> variable, which is also the value of our first field, <code>id</code>. However, given this initial address, all subsequent addresses have a known relative address. Since <code>id</code> is a 64 bit integer, it takes 8 bytes of memory. Therefore, <code>power</code> has to be at $start_address + 8:</p><pre><code>
user -> ------------ (id: 1043368d0)
| 1 |
------------ (power: 1043368d8)
| 100 |
------------</code></pre>
<p>To verify this for yourself, I'd like to introduce the addressof operator: <code>&</code>. As the name implies, the addressof operator returns the address of an variable (it can also return the address of a function, isn't that something?!). Keeping the existing <code>User</code> definition, try this <code>main</code>:</p><pre><code>
pub fn main() void {
const user = User{
.id = 1,
.power = 100,
};
std.debug.print("{*}\n{*}\n{*}\n", .{&user, &user.id, &user.power});
}</code></pre>
<p>This code prints the address of <code>user</code>, <code>user.id</code> and <code>user.power</code>. You might get different results based on your platform and other factors, but hopefully you'll see that the address of <code>user</code> and <code>user.id</code> are the same, while <code>user.power</code> is at an 8 byte offset. I got: </p><pre><code>
learning.User@1043368d0
u64@1043368d0
i32@1043368d8</code></pre>
<p>The addressof operator returns a pointer to a value. A pointer to a value is a distinct type. The address of a value of type <code>T</code> is a <code>*T</code>. We pronounce that <em>a pointer to T</em>. Therefore, if we take the address of <code>user</code>, we'll get a <code>*User</code>, or a pointer to <code>User</code>:</p><pre><code>
pub fn main() void {
var user = User{
.id = 1,
.power = 100,
};
user.power += 0;
const user_p = &user;
std.debug.print("{any}\n", .{@TypeOf(user_p)});
}</code></pre>
<p>Our original goal was to increase our user's <code>power</code> by 1, via the <code>levelUp</code> function. We got the code to compile, but when we printed <code>power</code> it was still the original value. It's a bit of a leap, but let's change the code to print the address of <code>user</code> in <code>main</code> and in <code>levelUp</code>:</p><pre><code>
pub fn main() void {
var user = User{
.id = 1,
.power = 100,
};
user.power += 0;
// added this
std.debug.print("main: {*}\n", .{&user});
levelUp(user);
std.debug.print("User {d} has power of {d}\n", .{user.id, user.power});
}
fn levelUp(user: User) void {
// add this
std.debug.print("levelUp: {*}\n", .{&user});
var u = user;
u.power += 1;
}</code></pre>
<p>If you run this, you'll get two different addresses. This means that the <code>user</code> being modified in <code>levelUp</code> is different from the <code>user</code> in <code>main</code>. This happens because Zig passes a copy of the value. That might seem like a strange default, but one of the benefits is that the caller of a function can be sure that the function won't modify the parameter (because it can't). In a lot of cases, that's a good thing to have guaranteed. Of course, sometimes, like with <code>levelUp</code>, we <em>want</em> the function to modify a parameter. To achieve this, we need <code>levelUp</code> to act on the actual <code>user</code> in <code>main</code>, not a copy. We can do this by passing the address of our user into the function:</p><pre><code>
const std = @import("std");
pub fn main() void {
var user = User{
.id = 1,
.power = 100,
};
// no longer needed
// user.power += 1;
// user -> &user
levelUp(&user);
std.debug.print("User {d} has power of {d}\n", .{user.id, user.power});
}
// User -> *User
fn levelUp(user: *User) void {
user.power += 1;
}
pub const User = struct {
id: u64,
power: i32,
};</code></pre>
<p>We had to make two changes. The first is calling <code>levelUp</code> with the address of user, i.e. <code>&user</code>, instead of <code>user</code>. This means that our function no longer receives a <code>User</code>. Instead, it receives a <code>*User</code>, which was our second change.</p>
<p>We no longer need that ugly hack of forcing user to be mutated via <code>user.power += 0;</code>. Initially, we failed to get the code to compile because <code>user</code> was a <code>var</code> but, the compiler told us, was never mutated. We thought maybe the compiler was wrong and "tricked" it by forcing a mutation. But, as we now know, the <code>user</code> being mutated in <code>levelUp</code> was a different; the compiler was right.</p>
<p>The code now works as intended. There are still many subtleties with function parameters and our memory model in general, but we're making progress. Now might be a good time to mention that, aside from the specific syntax, none of this is unique to Zig. The model that we're exploring here is the most common, some languages might just hide many of the details, and thus flexibility, from developers.</p>
<section>
<h2 tabindex="-1" id="methods"><a href="https://www.openmymind.net/#methods" aria-hidden="true">Methods</a></h2>
<p>More than likely, you'd have written <code>levelUp</code> as a method of the <code>User</code> structure:</p><pre><code>
pub const User = struct {
id: u64,
power: i32,
fn levelUp(user: *User) void {
user.power += 1;
}
};</code></pre>
<p>This begs the question: how do we call a method with a pointer receiver? Maybe we have to do something like: <code>&user.levelUp()</code>? Actually, you just call it normally, i.e. <code>user.levelUp()</code>. Zig knows that the method expects a pointer and passes the value correctly (by reference).</p>
<p>I initially chose a function because it's explicit and thus easier to learn from.</p>
<section>
<h2 tabindex="-1" id="const_paremeters"><a href="https://www.openmymind.net/#const_paremeters" aria-hidden="true">Constant Function Parameters</a></h2>
<p>I more than implied that, by default, Zig will pass a copy of a value (called "pass by value"). Shortly we'll see that the reality is a bit more subtle (hint: what about complex values with nested objects?)</p>
<p>Even sticking with simple types, the truth is that Zig can pass parameters however it wants, so long as it can guarantee that the intent of the code is preserved. In our original <code>levelUp</code>, where the parameter was a <code>User</code>, Zig could have passed a copy of the user or a reference to <code>main.user</code>, as long as it could guarantee that the function would not mutate it. (I know we ultimately <em>did</em> want it mutated, but by making the type <code>User</code>, we were telling the compiler that we didn't).</p>
<p>This freedom allows Zig to use the most optimal strategy based on the parameter type. Small types, like <code>User</code>, can be cheaply passed by value (i.e. copied). Larger types might be cheaper to pass by reference. Zig can use any approach, so long as the intent of the code is preserved. To some degree, this is made possible by having constant function parameters.</p>
<p>Now you know one of the reasons function parameters are constants.</p>
<aside>Maybe you're wondering how passing by reference could ever be slower, even compared to copying a really small structure. We'll see this more clearly next, but the gist is that doing <code>user.power</code> when <code>user</code> is a pointer adds a tiny bit of overhead. The compiler has to weigh the cost of copying versus the cost of accessing fields indirectly through a pointer.</aside>
</section>
<section>
<h2 tabindex="-1" id="pointer_to_pointer"><a href="https://www.openmymind.net/#pointer_to_pointer" aria-hidden="true">Pointer to Pointer</a></h2>
<p>We previous looked at what the memory of <code>user</code> within our <code>main</code> function looked like. Now that we've changed <code>levelUp</code> what would its memory look like?:</p><pre><code>
main:
user -> ------------ (id: 1043368d0) <---
| 1 | |
------------ (power: 1043368d8) |
| 100 | |
------------ |
|
............. empty space |
............. or other data |
|
levelUp: |
user -> ------------- (*User) |
| 1043368d0 |----------------------
-------------</code></pre>
<p>Within <code>levelUp</code>, <code>user</code> is a pointer to a <code>User</code>. Its value is an address. Not just any address, of course, but the address of <code>main.user</code>. It's worth being explicit that the <code>user</code> variable in <code>levelUp</code> represents a concrete value. This value happens to be an address. And, it's not <em>just</em> an address, it's also a type, a <code>*User</code>. It's all very consistent, it doesn't matter if we're talking about pointers or not: variables associate type information with an address. The only special thing about pointers is that, when we use the dot syntax, e.g. <code>user.power</code>, Zig, knowing that <code>user</code> is a pointer, will automatically follow the address.</p>
<aside>Some languages require a different symbol when accessing a field through a pointer.</aside>
<p>What's important to understand is that the <code>user</code> variable in <code>levelUp</code> itself exists in memory at some address. Just like we did before, we can see this for ourselves:</p><pre><code>
fn levelUp(user: *User) void {
std.debug.print("{*}\n{*}\n", .{&user, user});
user.power += 1;
}</code></pre>
<p>The above prints the address the <code>user</code> variable references as well as its value, which is the address of the <code>user</code> in <code>main</code>.</p>
<p>If <code>user</code> is a <code>*User</code>, then what is <code>&user</code>? It's a <code>**User</code>, or a <em>pointer to a pointer to a <code>User</code></em>. I can do this until one of us runs out of memory!</p>
<p>There <em>are</em> use-cases for multiple levels of indirection, but is isn't anything we need right now. The purpose of this section is to show that pointers aren't special, they're just a value, which is an address, and a type.</p>
</section>
<section>
<h2 tabindex="-1" id="nested_pointers"><a href="https://www.openmymind.net/#nested_pointers" aria-hidden="true">Nested Pointers</a></h2>
<p>Up until now, our <code>User</code> has been simple, containing two integers. It's easy to visualize its memory and, when we talk about "copying", there isn't any ambiguity. But what happens when <code>User</code> becomes more complex and contains a pointer?</p><pre><code>
pub const User = struct {
id: u64,
power: i32,
name: []const u8,
};</code></pre>
<p>We've added <code>name</code> which is a slice. Recall that a slice is a length and a pointer. If we initialized our <code>user</code> with the name of <code>"Goku"</code>, what would it look like in memory?</p><pre><code>
user -> ------------- (id: 1043368d0)
| 1 |
------------- (power: 1043368d8)
| 100 |
------------- (name.len: 1043368dc)
| 4 |
------------- (name.ptr: 1043368e4)
------| 1182145c0 |
| -------------
|
| ............. empty space
| ............. or other data
|
---> ------------- (1182145c0)
| 'G' |
-------------
| 'o' |
-------------
| 'k' |
-------------
| 'u' |
-------------</code></pre>
<p>The new <code>name</code> field is a slice which is made up of a <code>len</code> and <code>ptr</code> field. These are laid out in sequence along with all the other fields. On a 64 bit platform both <code>len</code> and <code>ptr</code> will be 64 bits, or 8 bytes. The interesting part is the value of <code>name.ptr</code>: it's an address to some other place in memory.</p>
<aside>Since we used a string literal, <code>user.name.ptr</code> will point to a specific location within the area where all the constants are stored inside our binary.</aside>
<p>Types can get much more complex than this with deep nesting. But simple or complex, they all behave the same. Specifically, if we go back to our original code where <code>levelUp</code> took a plain <code>User</code> and Zig provided a copy, how would that look now that we have a nested pointer?</p>
<p>The answer is that only a shallow copy of the value is made. Or, as some put it, only the memory immediately addressable by the variable is copied. It might seem like <code>levelUp</code> would get a half-baked copy of <code>user</code>, possibly with an invalid <code>name</code>. But remember that a pointer, like our <code>user.name.ptr</code> is a value, and that value is an address. A copy of an address is still the same address:</p><pre><code>
main: user -> ------------- (id: 1043368d0)
| 1 |
------------- (power: 1043368d8)
| 100 |
------------- (name.len: 1043368dc)
| 4 |
------------- (name.ptr: 1043368e4)
| 1182145c0 |-------------------------
levelUp: user -> ------------- (id: 1043368ec) |
| 1 | |
------------- (power: 1043368f4) |
| 100 | |
------------- (name.len: 1043368f8) |
| 4 | |
------------- (name.ptr: 104336900) |
| 1182145c0 |-------------------------
------------- |
|
............. empty space |
............. or other data |
|
------------- (1182145c0) <---
| 'G' |
-------------
| 'o' |
-------------
| 'k' |
-------------
| 'u' |
-------------</code></pre>
<p>From the above, we can see that shallow copying will work. Since a pointer's value is an address, copying the value means we get the same address. This has important implications with respect to mutability. Our function can't mutate the fields directly accessible by <code>main.user</code> since it got a copy, but it does have access to the same <code>name</code>, so can it mutate that? In this specific case, no, <code>name</code> is a <code>const</code>. Plus, our value "Goku" is a string literal which are always immutable. But, with a bit of work, we can see the implication of shallow copying:</p><pre><code>
const std = @import("std");
pub fn main() void {
var name = [4]u8{'G', 'o', 'k', 'u'};
const user = User{
.id = 1,
.power = 100,
// slice it, [4]u8 -> []u8
.name = name[0..],
};
levelUp(user);
std.debug.print("{s}\n", .{user.name});
}
fn levelUp(user: User) void {
user.name[2] = '!';
}
pub const User = struct {
id: u64,
power: i32,
// []const u8 -> []u8
name: []u8
};</code></pre>
<p>The above code prints "Go!u". We had to change <code>name</code>'s type from <code>[]const u8</code> to <code>[]u8</code> and instead of a string literal, which are always immutable, create an array and slice it. Some might see inconsistency here. Passing by value prevents a function from mutating immediate fields, but not fields with a value behind a pointer. If we <em>did</em> want <code>name</code> to be immutable, we should have declared it as a <code>[]const u8</code> instead of a <code>[]u8</code>.</p>
<p>Some languages have a different implementation, but many languages work exactly like this (or very close). While all of this might seem esoteric, it's fundamental to day to day programming. The good news is that you can master this using simple examples and snippets; it doesn't get more complicated as other parts of the system grow in complexity.</p>
</section>
<section>
<h2 tabindex="-1" id="recursive_structures"><a href="https://www.openmymind.net/#recursive_structures" aria-hidden="true">Recursive Structures</a></h2>
<p>Sometimes you need a structure to be recursive. Keeping our existing code, let's add an optional <code>manager</code> of type <code>?User</code> to our <code>User</code>. While we're at it, we'll create two <code>Users</code> and assign one as the manager to another:</p><pre><code>
const std = @import("std");
pub fn main() void {
const leto = User{
.id = 1,
.power = 9001,
.manager = null,
};
const duncan = User{
.id = 1,
.power = 9001,
.manager = leto,
};
std.debug.print("{any}\n{any}", .{leto, duncan});
}
pub const User = struct {
id: u64,
power: i32,
manager: ?User,
};</code></pre>
<p>This code won't compile: <em>struct 'learning.User' depends on itself</em>. This fails because every type has to have a known compile-time size.</p>
<p>We didn't run into this problem when we added <code>name</code> even though names can be different lengths. The issue isn't with the size of values, it's with the size of the types themselves. Zig needs this knowledge to do everything we talked about above, like accessing a field based on its offset position. <code>name</code> was a slice, a <code>[]const u8</code>, and that has a known size: 16 bytes - 8 bytes for <code>len</code> and 8 bytes for <code>ptr</code>.</p>
<p>You might think this is going to be a problem with any optional or union. But for both optionals and unions, the largest possible size is known and Zig can use that. A recursive structure has no such upper-bound, the structure could recurse once, twice or millions of times. That number would vary from <code>User</code> to <code>User</code> and would not be known at compile time.</p>
<p>We saw the answer with <code>name</code>: use a pointer. Pointers always take <code>usize</code> bytes. On a 64-bit platform, that's 8 bytes. Just like the actual name "Goku" wasn't stored with/along our <code>user</code>, using a pointer means our manager is no longer tied to the <code>user</code>'s memory layout.</p><pre><code>
const std = @import("std");
pub fn main() void {
const leto = User{
.id = 1,
.power = 9001,
.manager = null,
};
const duncan = User{
.id = 1,
.power = 9001,
// changed from leto -> &leto
.manager = &leto,
};
std.debug.print("{any}\n{any}", .{leto, duncan});
}
pub const User = struct {
id: u64,
power: i32,
// changed from ?const User -> ?*const User
manager: ?*const User,
};</code></pre>
<p>You might never need a recursive structure, but this isn't about data modeling. It's about understanding pointers and memory models and better understanding what the compiler is up to.</p>
</section>
<section>
<p>A lot of developers struggle with pointers, there can be something elusive about them. They don't feel concrete like an integer, or string or <code>User</code>. None of this has to be crystal clear for you to move forward. But it is worth mastering, and not just for Zig. These details might be hidden in languages like Ruby, Python and JavaScript, and to a lesser extent C#, Java and Go, but they're still there, impacting how you write code and how that code runs. So take your time, play with examples, add debug print statements to look at variables and their address. The more you explore, the clearer it will get.</p>
</section>
</section></section></article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/style_guide/">Style Guide</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/stack_memory/">Stack Memory</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>
Learning Zig - Style Guide2023-09-08T00:00:00Z/learning_zig/style_guide/
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/language_overview_2/">Language Overview - Part 2</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/pointers/">Pointers</a>
</div>
<article>
<h1 tabindex="-1" id="styleguide"><a href="https://www.openmymind.net/#styleguide" aria-hidden="true">Style Guide</a></h1>
<p>In this short part, we'll cover two coding rules enforced by the compiler as well as the standard library's naming convention.</p>
<section>
<h2 tabindex="-1" id="unused_variables"><a href="https://www.openmymind.net/#unused_variables" aria-hidden="true">Unused Variables</a></h2>
<p>Zig does not allow variables to go unused. The following gives two compile-time errors:</p><pre><code>
const std = @import("std");
pub fn main() void {
const sum = add(8999, 2);
}
fn add(a: i64, b: i64) i64 {
// notice this is a + a, not a + b
return a + a;
}</code></pre>
<p>The first error is because <code>sum</code> is an <em>unused local constant</em>. The second error is because <code>b</code> is an <em>unused function parameter</em>. For this code, these are obvious bugs. But you might have legitimate reasons for having unused variables and function parameters. In such cases, you can assign the variables to underscore (<code>_</code>):</p><pre><code>
const std = @import("std");
pub fn main() void {
_ = add(8999, 2);
// or
const sum = add(8999, 2);
_ = sum;
}
fn add(a: i64, b: i64) i64 {
_ = b;
return a + a;
}</code></pre>
<p>As an alternative to doing <code>_ = b;</code>, we could have named the function parameter <code>_</code>, though, in my opinion, this leaves a reader guessing what the unused parameter is:</p><pre><code>
fn add(a: i64, _: i64) i64 {</code></pre>
<p>Notice that <code>std</code> is also unused but does not generate an error. At some point in the future, expect Zig to also treat this as a compile-time error.</p>
</section>
<section>
<h2 tabindex="-1" id="shadowing"><a href="https://www.openmymind.net/#shadowing" aria-hidden="true">Shadowing</a></h2>
<p>Zig doesn't allow one identifier to "hide" another by using the same name. This code, to read from a socket, isn't valid:</p><pre><code>
fn read(stream: std.net.Stream) ![]const u8 {
var buf: [512]u8 = undefined;
const read = try stream.read(&buf);
if (read == 0) {
return error.Closed;
}
return buf[0..read];
}</code></pre>
<p>Our <code>read</code> variable shadows our function name. I am not a fan of this rule as it generally leads developers to use short meaningless names. For example, to get this code to compile, I'd change <code>read</code> to <code>n</code>. This is a case where, in my opinion, developers are in a much better position to pick the most readable option.</p>
</section>
<section>
<h2 tabindex="-1" id="naming"><a href="https://www.openmymind.net/#naming" aria-hidden="true">Naming Convention</a></h2>
<p>Besides rules enforced by the compiler, you are, of course, free to follow whatever naming convention you prefer. But it does help to understand Zig's own naming convention since much of the code you'll interact with, from the standard library to third party libraries, makes us of it.</p>
<p>Zig source code is indented with 4 spaces. I personally use a tab which is objectively better for accessibility.</p>
<p>Function names are camelCase and variables are lowercase_with_underscores (aka snake case). Types are PascalCase. There is an interesting intersection between these three rules. Variables which reference a type, or functions which return a type, follow the type rule and are PascalCase. We already saw this, though you might have missed it.</p><pre><code>
std.debug.print("{any}\n", .{@TypeOf(.{.year = 2023, .month = 8})});</code></pre>
<p>We've seen other builtin functions: <code>@import</code>, <code>@rem</code> and <code>@intCast</code>. Since these are functions they are camelCase. <code>@TypeOf</code> is also a builtin function, but it's PascalCase, why? Because it returns a type and thus the type naming convention is used. Were we to assign the result of <code>@TypeOf</code> to a variable, using Zig's naming convention, that variable should be PascalCase also:</p><pre><code>
const T = @TypeOf(3)
std.debug.print("{any}\n", .{T});</code></pre>
</section>
<section>
<p>The <code>zig</code> executable does have a <code>fmt</code> command which, given a file or a directory, will format the file based on Zig's own style guide. It doesn't cover everything though, for example it will adjust indentation and brace positions, but won't change identifier casing.</p>
</section>
</article>
<div class="pager">
<a class="prev" href="https://www.openmymind.net/learning_zig/language_overview_2/">Language Overview - Part 2</a>
<a class="home" href="https://www.openmymind.net/learning_zig/">Intro</a>
<a class="next" href="https://www.openmymind.net/learning_zig/pointers/">Pointers</a>
</div>
<p><a href="#new_comment">Leave a comment</a></p>