`@fieldParentPtr()` is innocent; what is dangerous is modifying a local copy of a structure

The new std.Io.Writer interface has faced a lot of misuse. Some attribute this misuse to @fieldParentPtr(). Some are eager to have Pinned struct accepted - by preventing this misuse through restricting copies of structures of ‘specific types’.
In this regard, I would like to sing a different tune: @fieldParentPtr() is innocent! The real root cause of the mistake regarding the copying of structs is not the type, but the mutability of the copy target.
Let us consider a very simple type:

const S = struct {
    v: usize,
    counter: usize,
};

There is no @fieldParentPtr(), not even any sub-structures.
Now, we will copy one of its fields and then make modifications.

var s: S = .{ .v = 1437, .counter = 0 };
s.counter += 1;
try std.testing.expectEqual(s.counter, 1); // Success!
var counter = s.counter;
counter += 1;
try std.testing.expectEqual(s.counter, 2); // FAIL!

Hey, with such a simple structure, we made a mistake just by copying one of its fields! We wanted to get the counter of s and then increment the counter. However, since we only obtained a copy of counter, we did not modify s’s counter correctly at all.
Of course, the errors caused by direct operations on value types are relatively easy to avoid, but when they are wrapped in a structure and manipulated using a method, it can appear more confusing.

const Counter = struct {
    value: usize,
    pub const init: Counter = .{ .value = 0 };
    pub fn increment(self: *Counter) void {
        self.value += 1;
    }
};
const Session = struct {
    id: usize,
    c: Counter,
};
test Session {
    var s: Session = .{ .id = 1437, .c = .init };
    s.c.increment();
    try std.testing.expectEqual(1, s.c.value); // Success!
    var c = s.c;
    c.increment();
    try std.testing.expectEqual(2, s.c.value); // FAIL!
}

Those who misuse std.Io.Writer have made nothing more than a simple mistake, that’s all. Even without @fieldParentPtr(), such a copy is still an error.
In this regard, one opinion is: we need a pinned struct! If S here is designed as a pinned struct, such a simple mistake wouldn’t happen.
But is that really the case? Is it really unacceptable for any part of the structure of S to be copied?
In fact, it is perfectly acceptable to treat a part of a structure as a constant that does not need to be modified:

const Counter = struct {
    value: usize,
    pub const init: Counter = .{ .value = 0 };
    pub fn increment(self: *Counter) void {
        self.value += 1;
    }
    pub fn log(self: *const Counter) void {
        std.debug.print("{d}", .{self.value});
    }
};
const Session = struct {
    id: usize,
    c: Counter,
};

test Session {
    var s: Session = .{ .id = 1437, .c = .init };
    s.c.increment();
    try std.testing.expectEqual(1, s.c.value);
    const c = s.c;
    c.log();
}

If there is a copy of any part of the structure, and there is only a read-only requirement without any need for modification, then there will be no problem.
Even if there really is a need for modification, as long as the object we copy from the struct is a pointer, any indirect modifications made based on that pointer copy are also not a problem.

const Counter = struct {
    value: usize,
    pub const init: Counter = .{ .value = 0 };
    pub fn increment(self: *Counter) void {
        self.value += 1;
    }
    pub fn log(self: *const Counter) void {
        std.debug.print("{d}", .{self.value});
    }
};
const Session = struct {
    id: usize,
    c: Counter,
    c_interface: *Counter,
    pub fn init(self: *Session, id: usize) void {
        self.id = id;
        self.c = .init;
        self.c_interface = &self.c;
    }
};
test Session {
    var s: Session = undefined;
    s.init(1437);
    s.c.increment();
    try std.testing.expectEqual(1, s.c.value);
    const c = s.c_interface;
    c.increment();
    try std.testing.expectEqual(2, s.c.value);
}

So, do you still think that the key factor affecting whether certain parts of a structure can be copied is the type of the structure itself?
I no longer think that. In fact, what really matters is the mutability of the copy target location! If the copy target location is read-only, the copy is always safe. However, if the copy target location is mutable, then the copy is likely to be misused.
This is also why the secret of std.mem.Allocator interface being safely copyable exists: as a virtual table fat pointer, when we copy them, they are always read-only, and we always pass them with const instead of var.

Now, I want to say: @fieldParentPtr() is completely innocent; if you adhere to the principle of ‘never copying any contents of a struct as var’, you can never misuse it.
You cannot write a harmful API design using @fieldParentPtr(). For example, you cannot write the following declaration:

const MyStruct = struct {
    v: usize,
    i: Interface,
};
const Interface = struct {
    pub fn logMyStructValue(self: *const Interface) void { // Incorrect statement!
        const s: *MyStruct = @alignCast(@fieldParentPtr("i", self));
        std.debug.print("{d}", .{s.v});
    }
};

Declaring self as a const pointer will result in a compilation error:

test.zig:68:41: error: @fieldParentPtr discards const qualifier
        const s: *MyStruct = @alignCast(@fieldParentPtr("i", self));
                                        ^~~~~~~~~~~~~~~~~~~~~~~~~~
test.zig:68:41: note: use @constCast to discard const qualifier
referenced by:
    decltest.MyStruct: test.zig:74:25

Therefore, you can only design the @fieldParentPtr API to require a mutable pointer, regardless of whether you actually need to modify the related content:

const MyStruct = struct {
    v: usize,
    i: Interface,
};
const Interface = struct {
    pub fn logMyStructValue(self: *Interface) void {
        const s: *MyStruct = @alignCast(@fieldParentPtr("i", self));
        std.debug.print("{d}", .{s.v});
    }
};
test MyStruct {
    var s: MyStruct = .{ .v = 0, .i = .{} };
    s.i.logMyStructValue();
}

Once you try to copy a substructure that uses @fieldParentPtr to a const location:

test MyStruct {
    var s: MyStruct = .{ .v = 0, .i = .{} };
    s.i.logMyStructValue();
    const i = s.i;
    i.logMyStructValue();
}

You will encounter a compilation error:

test.zig:76:6: error: expected type '*test.Interface', found '*const test.Interface'
    i.logMyStructValue();
    ~^~~~~~~~~~~~~~~~~

If you copy it as a var, it will indeed cause a runtime error - but as mentioned earlier, this is logically incorrect in itself and has nothing to do with @fieldParentPtr.

The last question is: Is it necessary for the language itself to restrict assigning the content of a struct to a var?
In principle, I actually think it is unnecessary, because there is indeed a need to ‘copy a part of the structure and use it as a variable that is unrelated to the original structure.’
In reality, I believe that programmers should have the instinct to immediately recognize that ‘assigning part of a struct to var is a warning sign,’ but this awareness needs to be promoted.
However, it is undeniable that the current reality is that this awareness has not yet been cultivated. One reason is that the assignment to var itself is quite easy; indeed, something that seems very simple does not conform to the principle of ‘making safe things easy and dangerous things difficult.’
Therefore, perhaps restricting the assignment of any part of a struct to var, while additionally introducing a built-in function similar to @varAssign to explicitly specify this behavior, might be a solution worth considering.

6 Likes

The logMyStructValue function is the same code as in the code snippet above, so I think you made a copy/paste error and actually meant to write different code there? Did you intend to use self: *Interface?

Technically in this case you also could use const s: *const MyStruct = ... but that doesn’t seem to match the text.

1 Like

Thanks for pointing out the problem, yes, I copied the wrong code

1 Like

Great explanation, thanks. But I’m not sure it applies to all cases. Like… modules are structures, and we do make copies of their nested structures right? Even as var if these structures are meant to be modified. Doesn’t it depend (mostly?) on how these structures will interact with the rest of the code? If they behave like interfaces, that need ties with the surrounding code, or if they are stand-alone that can do their own business.

I’m also not convinced by the pinned option, sounds too restrictive to me. But maybe some sort of half-constness? Like non-const, but only const copies allowed?

I agree that there are use cases where a part of the structure is intentionally copied and then modified independently. But I think we need to consciously understand what this is doing when doing so. So if we can make this behavior a little more difficult, such as explicitly implementing it with a built-in function, some unintentional mistakes can be avoid.

I must apologize for my assertion, as I’ve found that const isn’t always reliable. A value that might be modified by an API could very well be const.
A typical example I’ve found so far is ArenaAllocator . Not all of its members are pointers—its state member contains an end_index , which is actually modified by the API ArenaAllocator.allocator().alloc. However, because this modification is hidden by anyopaque , this value, while actually mutable, can be defined as const by the user without receiving an error.

Due to the widespread use of anyopaque vtables, my assertion that using const to determine whether a value is reliably copyable is unfounded.