Why not just `.vtable = @ptrCast(vtable)`?

I’m getting a grasp of interfaces, and am deeply unwilling to have to write self = @ptrCast(@alignCast(ptr)) all over the place. There MUST be a better way, no?

( See also: https://www.youtube.com/watch?v=2Q8gB2OXB2E )
( See also (idea of casting the function): Vtable interfaces and the role of @ptrCast and @alignCast - #11 by efjimm )

So I watched the video above that neatly describes Zig interfaces, specifically std.mem.Allocator, and thought to myself:

“If I can cast *anyopaque to *T, then surely I can cast the function that receives *anyopaque to one that receives *T. And if I can cast the function, then surely I can cast the whole VTable containing the functions.”

Thus: (working example)

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

pub fn main(init: std.process.Init) !void {
    var workers: [2]WorkerInterface = undefined;

    workers[0] = try .init(init.gpa, Repeater, .{ .repeat_count = 3 });
    workers[1] = try .init(init.gpa, Tagger, .{ .tag = "bold" });
    defer for (workers) |worker| worker.deinit();

    for (workers) |worker| {
        worker.doStringStuff("Hello");
        print("=> {d}\n", .{worker.doNumberStuff(123)});
    }
}

pub const WorkerInterface = struct {
    ptr: *anyopaque,
    vtable: *const VTable(anyopaque),

    fn VTable(T: type) type {
        return struct {
            // VTable methods defined by init():
            getGPA: *const fn (*T) Allocator,
            selfDestroy: *const fn (*T) void,

            // VTable methods defined by interface implementations:
            deinit: *const fn (*T) void,
            doStringStuff: *const fn (*T, []const u8) void,
            doNumberStuff: *const fn (*T, usize) usize,
        };
    }

    pub fn init(gpa: Allocator, T: type, params: T.Params) !@This() {
        const ptr = try gpa.create(T);
        ptr.params = params;
        ptr.gpa = gpa;
        try ptr.init();

        const vtable = try gpa.create(VTable(T));

        // VTable methods defined by init():
        const DefinedByInterface = struct {
            fn getGPA(t: *T) Allocator {
                return t.gpa;
            }
            fn selfDestroy(t: *T) void {
                t.gpa.destroy(t);
            }
        };
        vtable.getGPA = DefinedByInterface.getGPA;
        vtable.selfDestroy = DefinedByInterface.selfDestroy;

        // VTable methods defined by interface implementations:
        vtable.deinit = T.deinit;
        vtable.doStringStuff = T.doStringStuff;
        vtable.doNumberStuff = T.doNumberStuff;

        return .{
            .ptr = ptr,
            .vtable = @ptrCast(vtable),
        };
    }

    pub fn deinit(self: @This()) void {
        const gpa = self.vtable.getGPA(self.ptr);
        self.vtable.deinit(self.ptr);
        self.vtable.selfDestroy(self.ptr);
        gpa.destroy(self.vtable);
    }

    pub fn doStringStuff(self: @This(), string: []const u8) void {
        self.vtable.doStringStuff(self.ptr, string);
    }

    pub fn doNumberStuff(self: @This(), number: usize) usize {
        return self.vtable.doNumberStuff(self.ptr, number);
    }
};

pub const Repeater = struct {
    pub const Params = struct { repeat_count: usize };
    params: Params,
    gpa: Allocator,

    pub fn init(_: *@This()) !void {}

    pub fn deinit(_: *@This()) void {}

    pub fn doStringStuff(self: *@This(), string: []const u8) void {
        for (0..self.params.repeat_count) |_| print("{s}\n", .{string});
    }

    pub fn doNumberStuff(self: *@This(), number: usize) usize {
        for (0..self.params.repeat_count) |_| print("{d} ", .{number});
        print("\n", .{});
        return number * self.params.repeat_count;
    }
};

pub const Tagger = struct {
    pub const Params = struct { tag: []const u8 };
    params: Params,
    gpa: Allocator,

    pub fn init(_: *@This()) !void {}

    pub fn deinit(_: *@This()) void {}

    pub fn doStringStuff(self: *@This(), string: []const u8) void {
        print(
            "<{s} type=\"string\">{s}</{s}>\n",
            .{ self.params.tag, string, self.params.tag },
        );
    }

    pub fn doNumberStuff(self: *@This(), number: usize) usize {
        print(
            "<{s} type=\"number\">{d}</{s}>\n",
            .{ self.params.tag, number, self.params.tag },
        );
        return number;
    }
};

Is this sound?

1 Like

Off topic - you can get rid of these (to me) obnoxious nested scrollbars that Discourse puts around code blocks by adding the following filter to uBlock Origin (or other places that accept the same syntax):

ziggit.dev##:is(pre, pre > code):style(max-height: unset !important;)

4 Likes

I think casting a *const fn (*anyopaque) void to *const fn (*T) void and then calling it is illegal behaviour. Zig callconv(.auto) is entirely unspecified and there is no guarantee that *anyopaque and *T are passed in the same way.

1 Like

Something like this should work though

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

const VTable = struct {
    getGPA: *const fn (*anyopaque) Allocator,
    selfDestroy: *const fn (*anyopaque) void,

    // VTable methods defined by interface implementations:
    deinit: *const fn (*anyopaque) void,

    // ...
};

fn getVTable(T: type) *const VTable {
    const wrap = struct {
        fn getGpa(ptr: *anyopaque) Allocator {
            return T.getGPA(@ptrCast(@alignCast(ptr)));
        }
        fn selfDestroy(ptr: *anyopaque) void {
            return T.selfDestroy(@ptrCast(@alignCast(ptr)));
        }
        fn deinit(ptr: *anyopaque) void {
            return T.deinit(@ptrCast(@alignCast(ptr)));
        }
    };
    return &.{
        .deinit = wrap.deinit,
        .getGPA = wrap.getGpa,
        .selfDestroy = wrap.selfDestroy,
    };
}

You could also call the inner function with @call(.always_inline, func, .{@ptrCast(@alignCast(ptr))} if you don’t trust llvm to inline that

1 Like

The reason is relatively simple for why both are necessary.

@ptrCast changes the type of what the pointer is pointing to, but not the requirements the pointed to thing needs to fulfill, like for example alignment.
@alignCast changes the alignment requirement of what the pointer is pointing, but that’s it.

anyopaque has (by default) an alignment requirement of 1 byte and is it’s own type.
Whatever your vtable has as alignment requirement depends on the vtable itself, and since a vtable consists normally just of pointers, this depends on the target platform itself. For example on (most) 64bit platforms it’s going to be 8 bytes.

A pointer to anything with 8 byte alignment is surely also aligned to 1 byte, but not the other way around. Think of an aligned pointer always needing fulfill the condition pointer mod alignment = 0.

So, why does const anyopaque_ptr: *anyopaque = @ptrCast(&vtable) work?
Well, as far as the typesystem is concerned, &vtable fulfills anyopaque’s alignment requirement of 1 byte. So that’s fine.
Other than that, it just changes the type.
So everything is fine.

Why does const vtable_ptr: *const VTable = @ptrCast(anyopaque_ptr) not work?
As far as the typesystem is concerned, anyopaque_ptr does not fulfill vtable_ptr’s alignment requirement since 1 byte alignment is weaker than 8 byte.
So you need @alignCast to tell the compiler to trust you about the correct alignment (which gets additionally checked when safety checks are enabled).

But here’s a thing one could do: One can additionally put an alignment requirement into the pointer type and overwrite the default alignment.
const anyopaque_ptr: *align(8) anyopaque = @ptrCast(&vtable).
This means that anyopaque_ptr has now an 8 byte alignment requirement.
If VTable has a weaker one than that (e.g. 4 bytes on 32bit platforms), you now need to cast here, of course, but if the cast from anyopaque_ptr to a *VTable can now leave the @alignCast out, if *VTablehas the same or a weaker alignment requirement than *align(8) anyopaque.

Now as for why your code example doesn’t work:
The WorkerInterface has as fields an *anyopaque and a *const VTable(anyopaque), so two known types.
In the WorkerInterface.init function, you try to cast a *const VTable(T) to a *const VTable(anyopaque). This will only work if T == anyopaque since *const VTable(anyopaque) == *const VTable(anyopaque).
But if for example T == Repeater, *const VTable(Repeater) != *const VTable(anyopaque).
So that doesn’t work. Different types.

If you have more questions, feel free to ask.

Thanks for the filter snippet.

1 Like

The example works. The question is whether

const vta: *const VTable(T) = ...;
const vtb: *const VTable(anyopaque) = @ptrCast(vta);

is a good and/or sound idea. (Written on the go, typos possible.)

Zig having alignment runtime safety is a good thing. I do agree though that something like @fnCast would be nice which checks if casting between function pointers is compatible so you can just use *MyType instead of *anyopaque as first argument.

3 Likes

One reason that the answer is “no” is that the VTable is a struct with function pointers at certain offsets. Your pointer cast relies on those function pointers being at the same offsets from the struct, which is not guaranteed when the struct has auto layout (i.e. is not extern and is not packed, soon to be renamed to bitpack or similar)

1 Like

Thanks for your answers.

The example works for now (fingers crossed), but I’ll put a “TODO” in it to remind me that I’m relying not on guaranteed but incidental behavior, and that I may have to do it properly using one of the alternative solutions provided here:

  • The getVTable() way.
  • The extern struct way.

In any case I don’t think I have to change anything either in main (the consumer of the interface) or in the implementors of the interface.

I had tried the extern struct way in a draft, but dismissed it as the struct contained the Allocator at that time, which itself is not an extern struct, and thus the compiler didn’t allow me to put it in an extern struct. The current version puts the Allocator in the implementor.

I hope that’s not true. Making stuff illegal should help compiler performance . A pointer is a pointer, what do you gain by preventing this?

1 Like

Last time someone brought the topic, someone shared that snippet that does “safe” function cast:

In general, ptr: *T when T is not anyopaque contains information about the alignment of ptr, so it is literally true that *anyopaque and *T are not handled the same.

As arguments to the function, probably you are correct that they are both just passed in registers. But by not specifying, the compiler is free to optimize them differently.

What if I just force all alignment around *anyopaque to equal the alignment of *usize? Because for all practical intents and purposes, I’m guaranteed to be only dealing with that kind of alignment anyway. (At least thanks to having the Allocator inside the implementors, these are not going to have any smaller alignment.)

1 Like

Different but similar idea: What if instead of *anyopaque I just use *usize as the “unknown pointer” type?

Alignment in Zig is different from alignment in C. Pointers may have essentially arbitrary alignment as part of their type information. The most common large alignments I see in the compiler and standard library is alignment to the system’s page size.

Maybe some day we will have an architecture were there are different registers for pointers based on their alignment. Until then I’ll continue casting *const fn(*T) R into *const fn(*anyopaque)

Is there ever a time where someone would use @alignCast and not have it paired with a call to @ptrCast? I personally have never encountered such a scenario, though neither have I been deep into anything requireing specific alignments of pointers beyond some interop with a C library needing alignment for SIMD.

I expect someone more experienced can explain the “why not”, but it seems like @alignCast should be more ergonomic if it also did the @ptrCast so that they didn’t need to be nested.

Here’s an example from my vulkan renderer

const graphics_pipeline = g.createGraphicsPipeline(.{
    .shader_code = @alignCast(@embedFile("shader")),
    ...
});
...
pub const CreateGraphicsPipelineOptions = struct {
    shader_code: []align(4) const u8,
    ...
};
...
const shader_module_ci: vk.ShaderModuleCreateInfo = .{
    .code_size = options.shader_code.len,
    .p_code = @ptrCast(options.shader_code.ptr),
};

it’s not strictly necessary but I think it’s nicer than

const graphics_pipeline = g.createGraphicsPipeline(.{
    .shader_code = @ptrCast(@alignCast(@embedFile("shader"))),
    ...
});
...
pub const CreateGraphicsPipelineOptions = struct {
    shader_code: []const u32,
    ...
};
...
const shader_module_ci: vk.ShaderModuleCreateInfo = .{
    .code_size = options.shader_code.len * 4,
    .p_code = options.shader_code.ptr,
};

Yes, but in this code, the cast is just implicit, there is still pointer cast, the language just doesn’t demand you do it explicitly.

The point of my question was wondering if @alignCast could just have the @ptrCast functionality built into it for the sake of casting anyopaque.