Casting a pointer to a pointer of its parent struct

Hi,

I’m a C programmer who recently started to learn Zig. I defined a base struct NodeBase and its child struct NodeInternal as follow:

const std = @import("std");
const Allocator = std.mem.Allocator;

const NodeKind = enum {
    Internal,
    Leaf,
};

const NodeBase = struct {
    kind: NodeKind,

    pub fn init(kind: NodeKind) NodeBase {
        return .{
            .kind = kind,
        };
    }
};

const NodeLeaf = struct {
    base: NodeBase,
    child: *NodeBase,

    pub fn create(allocator: Allocator) !*NodeLeaf {
        var n = try allocator.create(NodeLeaf);
        n.base = NodeBase.init(NodeKind.Leaf);
        n.child = undefined;
        return n;
    }
};

And wanted to access a field (kind) of the base struct in the NodeInternal by casting a pointer type as follow:

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    const leaf_n = try NodeLeaf.create(allocator);
    const base_n: *NodeBase = @ptrCast(leaf_n);

    std.debug.print("align-of\t: leaf_n {d}, base_n {d}\n", .{ @alignOf(NodeLeaf), @alignOf(NodeBase) });
    std.debug.print("pointer\t\t: leaf_n {*}, base_n {*}\n", .{ leaf_n, base_n });
    std.debug.print("kind\t\t: leaf_n {any} base_n {any}\n", .{ leaf_n.base.kind, base_n.kind });
    std.debug.print("ptr of kind\t: leaf_n {*} base_n {*}\n", .{ &(leaf_n.base.kind), &(base_n.kind) });
}

However, I got the different addresses with/without a pointer casting; the last output shows different addresses for the same field:

align-of        : leaf_n 8, base_n 1
pointer         : leaf_n main.NodeLeaf@104818000, base_n main.NodeBase@104818000
kind            : leaf_n main.NodeKind.Leaf base_n main.NodeKind.Internal
ptr of kind     : leaf_n main.NodeKind@104818008 base_n main.NodeKind@104818000

How can I get the correct address of kind even after a pointer casting?

1 Like

struct in Zig does not have a defined layout, so it can rearrange fields however it wants. In your case, it’s putting the child field first:

std.debug.print("offset-of\t: base {d}, child {d}\n", .{ @offsetOf(NodeLeaf, "base"), @offsetOf(NodeLeaf, "child") });

outputs

offset-of       : base 8, child 0

One thing you can do instead: to get a *NodeBase pointer, get a pointer to the base field; you can then use @fieldParentPtr to go from that pointer-to-a-field to a pointer to its “parent” struct (further explanation from when I was first learning how this works).

Here’s your main function modified to show you what I mean:

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    const leaf_n = try NodeLeaf.create(allocator);
    const base_n: *NodeBase = &leaf_n.base;

    std.debug.print("align-of\t: leaf_n {d}, base_n {d}\n", .{ @alignOf(NodeLeaf), @alignOf(NodeBase) });
    std.debug.print("offset-of\t: base {d}, child {d}\n", .{ @offsetOf(NodeLeaf, "base"), @offsetOf(NodeLeaf, "child") });
    std.debug.print("pointer\t\t: leaf_n {*}, base_n {*}\n", .{ leaf_n, base_n });
    std.debug.print("kind\t\t: leaf_n {any} base_n {any}\n", .{ leaf_n.base.kind, base_n.kind });
    std.debug.print("ptr of kind\t: leaf_n {*} base_n {*}\n", .{ &(leaf_n.base.kind), &(base_n.kind) });

    const leaf_again: *NodeLeaf = @alignCast(@fieldParentPtr("base", base_n));
    std.debug.print("pointer\t\t: leaf_again {*}\n", .{leaf_again});
}

That outputs for me:

align-of        : leaf_n 8, base_n 1
offset-of       : base 8, child 0
pointer         : leaf_n node-cast.NodeLeaf@18f780d0000, base_n node-cast.NodeBase@18f780d0008
kind            : leaf_n node-cast.NodeKind.Leaf base_n node-cast.NodeKind.Leaf
ptr of kind     : leaf_n node-cast.NodeKind@18f780d0008 base_n node-cast.NodeKind@18f780d0008
pointer         : leaf_again node-cast.NodeLeaf@18f780d0000

Real world example:


If, instead, you want a guaranteed struct layout, you can use extern struct or packed struct.

2 Likes

Your example isn’t trying to get a parent pointer but a field pointer, which you can do with just &leaf_n.base

2 Likes

Also, just as an aside: double check that undefined is what you want to use here. undefined in Zig is a sharp tool that tells the compiler this value will be changed before I do anything with it. Comparing something against undefined is (currently unchecked, but will be checked in the future) illegal behavior.

If there is any possibility of you using child before setting it to a real value, then you probably want an optional instead:

    // note that an optional pointer is the same size as non-optional
    child: ?*NodeBase,

// ...

        n.child = null;
3 Likes