paulrt

July 30, 2026, 3:54pm 1

I think I get why the buffer needs to be intrusive
but why does the vtable have to be as well?
Why isn’t it a copyable handle more similar to the Allocator interface?
something like this:

// std/Io/Writer.zig
const Writer = @This();

vtable: *const VTable,
state: *State,

pub const State = struct {
    buffer: []u8,
    end: usize = 0,
};
pub const VTable = struct {
    drain: *const fn (w: Writer, data: []const []const u8, splat: usize) Error!usize,
    // ...
};


pub fn write(w: Writer, bytes: []const u8) Error!usize {
    if (w.state.end + bytes.len <= w.state.buffer.len) {
        @branchHint(.likely);
        @memcpy(w.state.buffer[w.state.end..][0..bytes.len], bytes);
        w.state.end += bytes.len;
        return bytes.len;
    }
    return w.vtable.drain(w, &.{bytes}, 1);
}

// MyWriter.zig

const MyWriterImpl = @This();
interface_state: Io.Writer.State,

// and impl state...

pub fn drain(io_w: Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
    const w: *MyWriterImpl = @alignCast(@fieldParentPtr("interface_state", io_w.state));
    // funky implementation details
}    

I think this is the same amount if indirection
Is it so we can store it in half the size (without adding another indirection layer)? If so, why isn’t the Allocator vtable intrusive as well so we pass it by pointer?

xgallom July 30, 2026, 4:19pm 2

I presume it comes down to performance and memory requirements?

Since allocator represents truly type-erased runtime-polymorphic state (e.g. polymorphic layout - pointer to arena vs pointer to gpa), it makes sense to keep the interface as a fat pointer (anyopaque + vtable). It describes your complete state with minimum amount of data (ptr to state + ptr to behavior).

A reader/writer itself has a lot more common state, it isn’t a truly type erased interface in purest sense. It just makes sense to put the vtable inside, e.g. coupling vtable with all the state inside makes sense both at a system level (you can describe the complete state via a single pointer), but also it’s because you have static memory requirements (a reader/writer always has a known layout and always has a vtable and always has a buffer).

It also aligns with utilizing @fieldParentPtr offers the same workflow as it does with SinglyLinkedList, and the reasoning is pretty clear: firstly as you posted, performance. Having the state north of the vtable allows most interaction with the interface occur without a dispatch. Second is composition over inheritance. In this sense I would argue that the VTable is not actually “intrusive”. It’s inside the Writer object, not inside the FileWriter or any other specialization of the interface. An Allocator interface models polymporphism, strictly something that in C++ would be modelled via a single-level inheritance hierarchy and virtual methods (even though that would yield a sub-optimal layout strictly closer to the Reader/Writer implementation. I recommend a really cool video on the topic here.). A Reader Writer implements polymorphism via DoD - separating common functionality into a static type and then offering the implementation site freedom to integrate it however it sees fit via composition).

xgallom July 30, 2026, 4:32pm 3

Another important distinction is where this actually starts to matter - having the vtable as part of the handle (as Allocator does), affects memory fetching patterns if you have for example a 100k different allocators you call alloc on in a sequence, and they could have wildly unpredictable implementations.

Since neither Allocator nor Reader/Writer tend to operate in this mode (you are much more likely to either have up to 100 or 50 Writers of a similiar or different type, and you pretty much only ever use one or max like 5 allocators), you get into hardware prefetching, L1 D-cache and I-cache, and none of this makes any tangible difference. Because strictly speaking, the second indirection (Writer → VTable) does require the first indirection (ptr → Writer) to resolve before it can begin, Having it as you specify would be better for cases that these structures are unlikely to come across. This indirection is irrelevant because for the vtable to actually needed, you are already inside Writer methods, and those are already using end, buffer, etc, and the fetch has to be finished before their code that could dispatch to vtable can execute.