Here’s a very interesting bit of Zig that I came across again this week: @fieldParentPtr
. It’s a prism through which you can see a lot of Zig’s character. That’s not what the official docs say, of course. They say that @fieldParentPtr
:
Given a pointer to a field, returns the base pointer of a struct.
That doesn’t paint a, uhm, full picture, I’d say. So here’s some code:
const Parent = struct {
name: []const u8,
left: Child,
right: Child,
};
const Child = struct {
name: []const u8,
position: enum {
left,
right,
},
fn parentName(self: *Child) []const u8 {
const parent = switch (self.position) {
.left => @fieldParentPtr(Parent, "left", self),
.right => @fieldParentPtr(Parent, "right", self),
};
return parent.name;
}
};
Parent
has two Child
ren, all three have a name and the parentName
method on Child
returns its parent’s name.
The interesting bit is the invocation of @fieldParentPtr
:
@fieldParentPtr(Parent, "left", self)
What that says is: give me a pointer to Parent
, based on the pointer of the "left"
field on Parent
that I’m giving you.
It’s doing pointer math for you!
What it does is take self
, which is a pointer to a Child
, look up the position of left
field in Parent
, then calculate how many bytes to subtract from left
to get to the start of Parent
, do that calculation and return a pointer to Parent
.
Here’s roughly-equivalent C code to show what’s going on under the hood:
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdint.h>
enum position {
LEFT,
RIGHT
};
struct child {
const char *name;
enum position position;
};
struct parent {
const char *name;
struct child left;
struct child right;
};
const char *parentName(const struct child *child) {
uintptr_t offset = 0;
switch (child->position) {
// 1. Turn a `0` into a `struct parent *` pointer
// 2. Access `->left` or `->right`, take pointer of that.
// 3. That increased the `0` address to the field address
// -> our offset
case LEFT:
offset = (uintptr_t) &(((struct parent *)0)->left);
break;
case RIGHT:
offset = (uintptr_t) &(((struct parent *)0)->right);
break;
}
// Now we can take that offset and get to the name
struct parent *parent = (struct parent *)((char *)child - offset);
return parent->name;
}
int main(void) {
struct parent parent = {
.name = "bob",
.left = { .name = "child1", .position = LEFT },
.right = { .name = "child2", .position = RIGHT },
};
assert(strcmp(parentName(&parent.left), "bob") == 0);
assert(strcmp(parentName(&parent.right), "bob") == 0);
printf("And bob's... not your uncle?\n");
return 0;
}
What @fieldParentPtr
does is what parentName
does here, except it’s compile-time safe!
If I change the Zig code from
@fieldParentPtr(Parent, "left", self)
to
@fieldParentPtr(Parent, "in emergency: break dance", self)
it breaks!
field_parent_ptr_simple.zig:18:46: error: no field named 'in emergency: break dance' in struct 'f
ield_parent_ptr_simple.Parent'
.left => @fieldParentPtr(Parent, "in emergency: break dance", self),
^~~~~~~~~~~~~~~~~~~~~~~~~~~
One could re-implement @fieldParentPtr
in Zig without it being a compiler builtin. The fact it is a compiler builtin and that people use it to implement interfaces in Zig shows you what Zig is about.
It’s fun to learn.
It's a neat feature, tho not without dragons that made me looking for alternative ways to do interfaces in Zig: https://github.com/ziglang/zig/issues/591
I think zig does not guarantee the order of the fields in a struct, that’s why something like @filedParentPtr has to exist.