Type punning in Zig
Unlike C, Zig doesn't provide guarantees on the order of fields in a struct. From the language reference:
// Zig gives no guarantees about the order of fields and the size of
// the struct but the fields are guaranteed to be ABI-aligned.
This means we can't do anything like C type punning, even without taking strict aliasing into consideration (see last post). However the language does provide some facilities for navigating the structure which enable doing what Crafting Interpreters calls "struct inheritance". We have @fieldParentPtr():
const ObjString = struct {
obj: Obj,
payload: []const u8,
};
const Obj = struct {
obj_type: ObjType,
const ObjType = enum { string };
pub fn format(self: *const Obj, writer: *std.Io.Writer) !void {
switch (self.obj_type) {
.string => {
const s: *const ObjString = @fieldParentPtr("obj", self);
try writer.print("{s}", .{s.payload});
},
}
}
};
The idea here is that Obj will always be part of some larger struct (in this case I only have ObjString, but there could be others). From a pointer to an Obj we can use @fieldParentPtr() to access the address of that larger struct. We need a special construct for this because we don't know how the struct is laid out in memory so we don't know which offset the obj field in ObjString is at. (Although I suppose we could use another special construct, @offsetOf(), for this.)
We also need @alignCast() because ObjString has alignment 8 (on my machine anyway) and Obj has alignment 1, and we need to tell the compiler to increase the pointer's alignment. Compiling without it produces an error:
punning.zig:13:37: error: @fieldParentPtr increases pointer alignment
const s: *const ObjString = @fieldParentPtr("obj", self);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~
punning.zig:13:37: note: '*align(1) const punning.ObjString' has alignment '1'
punning.zig:13:37: note: '*const punning.ObjString' has alignment '8'
punning.zig:13:37: note: use @alignCast to assert pointer alignment
Anyway, this is how we can pass a pointer to an Obj to a function and have it use the larger struct around it. Note that we lose a bit of type safety with this since the compiler can't guarantee that the Objs we're passing around come from structs of the right type, which is why we check obj_type first.
Here's an example:
pub fn main() !void {
const obj_string: ObjString = .{
.obj = .{ .obj_type = .string },
.payload = "hello",
};
std.debug.print("{f}\n", .{&obj_string.obj});
const obj: Obj = .{ .obj_type = .string };
std.debug.print("{f}\n", .{&obj});
}
The first print() runs fine; the second one fails with "panic: incorrect alignment".