How to use @fieldParentPointer with a non-null version of a nullable field

I have a pointer to a field of a struct: data: *T, however it doesn’t actually point to the field, because the field is optional: field: ?T. I can’t use @fieldParentPtrbecause data needs to be of type *?T which it isn’t. I can’t just cast data, as the pointer might not point to the correct location, or at least that’s what I think.

Is there around this, or do I have to go redesign some stuff?

The field doesn’t need to be a pointer. The object you call @fieldParentPtr("name", object) does. Both data: *T and data: ?T would be valid targets. If I understand what you mean. This should work.

const T = struct {};

const Parent = struct {
    data0: *T,
    data1: ?T,
}

pub fn parent(child: *const T) *const Parent {
    const valid: *const Parent = @fieldParentPtr("data1", child);
  
   return valid;
}

test "example" {
    const p: Parent{} = undefined;
    try std.testing.expect(&p == parent(&(p.data1.?)));
}

Now that I’ve written it, I’m less convinced, it’s bug free (I do depend a lot on the compiler :sweat_smile:). You might need parent to be parent(*const ?T) You should test instead of trusting me. But the docs don’t mean that the type of the field needs to be a pointer. I.e. *T and T are both valid. But that you need a pointer to the type, that is the field. So **T and *T respectively. @fieldParentPtr works for any type, but it’s just the offset of the address.

Another example

const T2 = struct{ 
    base: usize,
};

test "example" {
    const t2: T2 = .{ .base = 2 };
    // notice t2.base isn't a ptr, but &t2.base is a pointer to the field.
    try std.testing.expectEqual(&T2, @as(*T2, @fieldParentPtr(&t2.base)); 
    // They're equal here, because the offset of the first field is 0
    // ptr + 0 == ptr;
}

It does not compile because of two syntax errors. After fixing them I got this:

example.zig:11:59: error: expected type '*const ?example.T', found '*const example.T'
    const valid: *const Parent = @fieldParentPtr("data1", child);
                                                          ^~~~~
example.zig:11:59: note: pointer type child 'example.T' cannot cast into pointer type child '?example.T'
example.zig:3:11: note: struct declared here
const T = struct {};
          ^~~~~~~~~
referenced by:
    test.example: example.zig:18:40

This works:

const T = struct {
    foo: u8,
};

const Parent = struct {
    data0: *T,
    data1: ?T,
};

pub fn parent(child: *const ?T) *const Parent {
    const valid: *const Parent = @alignCast(@fieldParentPtr("data1", child));
    return valid;
}

test "example" {
    var foo: T = .{ .foo = 37 };
    const p: Parent = .{ .data0 = &foo, .data1 = .{.foo = 42 }};
    try std.testing.expect(&p == parent(&(p.data1)));
}

$ zig version
0.15.2

1 Like
test "example" {
    const p: Parent = undefined;
    try std.testing.expect(&p == parent(&(p.data1.?)));
}

Here you pass a pointer to the data field of the optional value data1, not the pointer to the entire field. And since data1 is of type ?T the type @fieldParentPtr expects is *const ?T, and the compiler gives an error.

Here is a snippet representing my situation:

const Data = struct {
    some_data: Foo,
    child_data: ?ChildData,
};

const ChildData = struct {
    child: Child,
    bar: Baz,
};

const Child = struct {};

fn some_function() void {
    ...

    data.child_data = .{
        .child = get_child();
        .bar = undefined;
    };
    data.child_data.?.child.add_callback(callback, &data.child_data.?)

    ...
}

fn callback(child: *Child, callback_info: Info, data: *ChildData) void {
    if (callback_info.stuff) {
        const parent_data: *Data = @fieldParentPtr("child_data", data);
        //                                                       ~~~~ Err:
        // found type `*const ChildData`, should have been: `*const ?ChildData` 
        parent_data.some_data = Foo.yes();
    }
}

Now this was a very easy fix, I just had to pass in the bigger Data struct, instead of the ChildData, but even though I have solved the problem, I am still wondering if it’s possible to use @fieldParentPtr on a field that has been unwrapped.

No. By definition the field is optional and unwrapped is not the field.

1 Like

And you can’t calculate the address of the original optional value in some way?

If the type is ?*T yes, @fieldParentPtr() will (should?) work, not with ?T

You can try, but that behavior isn’t offered by the compiler… That said, @offsetOf() exists, so if you’re feeling really brave. There’s nothing stopping you from doing the ptr math yourself. But, in the case where you can’t pass ?T because you know it’s unwrapped. For all the cases I can imagine, you also can’t prove the pointer is what you expect. IMO, your time and attention would be better spent writing code that makes @fieldParentPtr() easy to use, by passing *?T instead of the unwrapped ?T… if you did want to use it. e.g. for t: *?T use if (t.* == null) return error.Null; and then t.? throughout.

Issue which tracks problem https://github.com/ziglang/zig/issues/25241

3 Likes

This also works on my machine:

test "pointer to optional equals pointer to unwrapped" {
    inline for (.{bool, u8, usize, f64, *u8, **u8}) |T| {
        const foo: T = undefined;
        const optional: ?T = foo;
        try std.testing.expectEqual(@as(*const T, @ptrCast(&optional)), &(optional.?));
    }
}

I am not sure if this is compiler implementation detail or somehow part of language specification.

According to mlugg:

as you also cannot use @offsetOf to extract the offset of an optional’s payload.

which seems to contradict my suggestion. I knew the pointer math would be sketchy, (and I still assume there’s a dangerous, hacky way that’s nearly guaranteed to break with every zig version) But it’s clearly not going to be as easy as adding 8 to the offset.

1 Like

optionals have no guaranteed layout, and there are proposals to do more optimization on the representation, so any tricy dependency you take on the current implementation of an optional in memory is very likely to break.

1 Like