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

**URL:** <https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388>\
**Category:** Explain\
**Created:** [August 27, 2026, 9:31am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388 "2026-08-27T09:31:36Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 9:31am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/1 "2026-08-27T09:31:36Z")

</div>

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](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](https://ziggit.dev/t/vtable-interfaces-and-the-role-of-ptrcast-and-aligncast/2062/11) )

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)

```zig
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?

---

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 9:38am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/2 "2026-08-27T09:38:11Z")

</div>

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;)`

---

<div class="post-metadata">

**Author:** ![PinieP](https://ziggit.dev/letter_avatar_proxy/v4/letter/p/58956e/32.png) [@PinieP](https://ziggit.dev/u/PinieP)\
**Post date:** [August 27, 2026, 9:56am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/3 "2026-08-27T09:56:19Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![PinieP](https://ziggit.dev/letter_avatar_proxy/v4/letter/p/58956e/32.png) [@PinieP](https://ziggit.dev/u/PinieP)\
**Post date:** [August 27, 2026, 10:22am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/4 "2026-08-27T10:22:15Z")

</div>

Something like this should work though

```zig
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

---

<div class="post-metadata">

**Author:** ![KilianHanich](https://ziggit.dev/user_avatar/ziggit.dev/kilianhanich/32/8254_2.png) [@KilianHanich](https://ziggit.dev/u/KilianHanich)\
**Post date:** [August 27, 2026, 10:42am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/5 "2026-08-27T10:42:33Z")

</div>

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 `*VTable`has 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.

---

<div class="post-metadata">

**Author:** ![KilianHanich](https://ziggit.dev/user_avatar/ziggit.dev/kilianhanich/32/8254_2.png) [@KilianHanich](https://ziggit.dev/u/KilianHanich)\
**Post date:** [August 27, 2026, 10:43am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/6 "2026-08-27T10:43:06Z")

</div>

Thanks for the filter snippet.

---

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 11:17am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/7 "2026-08-27T11:17:05Z")

</div>

The example works. The question is whether

```zig
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.)

---

<div class="post-metadata">

**Author:** ![Cloudef](https://ziggit.dev/user_avatar/ziggit.dev/cloudef/32/1370_2.png) [@Cloudef](https://ziggit.dev/u/Cloudef)\
**Post date:** [August 27, 2026, 11:59am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/8 "2026-08-27T11:59:34Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![alanza](https://ziggit.dev/user_avatar/ziggit.dev/alanza/32/6006_2.png) [@alanza](https://ziggit.dev/u/alanza)\
**Post date:** [August 27, 2026, 1:45pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/9 "2026-08-27T13:45:57Z")

</div>

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)

---

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 2:17pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/10 "2026-08-27T14:17:31Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [August 27, 2026, 7:26pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/11 "2026-08-27T19:26:56Z")

</div>

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?

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [August 27, 2026, 7:28pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/12 "2026-08-27T19:28:56Z")

</div>

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

> <https://github.com/PixelGuys/Cubyz/blob/6fb2d14462e8cdb0d52ecfd0fd37bd8ae3823b5f/src/meta.zig#L3-L79>

---

<div class="post-metadata">

**Author:** ![alanza](https://ziggit.dev/user_avatar/ziggit.dev/alanza/32/6006_2.png) [@alanza](https://ziggit.dev/u/alanza)\
**Post date:** [August 27, 2026, 8:12pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/13 "2026-08-27T20:12:43Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 8:25pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/14 "2026-08-27T20:25:42Z")

</div>

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.)

---

<div class="post-metadata">

**Author:** ![felixrabe](https://ziggit.dev/user_avatar/ziggit.dev/felixrabe/32/5355_2.png) [@felixrabe](https://ziggit.dev/u/felixrabe)\
**Post date:** [August 27, 2026, 8:27pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/15 "2026-08-27T20:27:38Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![alanza](https://ziggit.dev/user_avatar/ziggit.dev/alanza/32/6006_2.png) [@alanza](https://ziggit.dev/u/alanza)\
**Post date:** [August 27, 2026, 8:32pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/16 "2026-08-27T20:32:12Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![gwenzek](https://ziggit.dev/user_avatar/ziggit.dev/gwenzek/32/8137_2.png) [@gwenzek](https://ziggit.dev/u/gwenzek)\
**Post date:** [August 27, 2026, 9:06pm UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/17 "2026-08-27T21:06:36Z")

</div>

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)`

---

<div class="post-metadata">

**Author:** ![ForeverZer0](https://ziggit.dev/user_avatar/ziggit.dev/foreverzer0/32/2443_2.png) [@ForeverZer0](https://ziggit.dev/u/ForeverZer0)\
**Post date:** [August 28, 2026, 5:58am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/18 "2026-08-28T05:58:25Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![jmstevers](https://ziggit.dev/user_avatar/ziggit.dev/jmstevers/32/9230_2.png) [@jmstevers](https://ziggit.dev/u/jmstevers)\
**Post date:** [August 28, 2026, 6:55am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/19 "2026-08-28T06:55:48Z")

</div>

> [@ForeverZer0](#):
>
> Is there ever a time where someone would use `@alignCast` and not have it paired with a call to `@ptrCast`?

Here’s an example from my vulkan renderer

```zig
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

```zig
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,
};

```

---

<div class="post-metadata">

**Author:** ![ForeverZer0](https://ziggit.dev/user_avatar/ziggit.dev/foreverzer0/32/2443_2.png) [@ForeverZer0](https://ziggit.dev/u/ForeverZer0)\
**Post date:** [August 28, 2026, 8:30am UTC](https://ziggit.dev/t/why-not-just-vtable-ptrcast-vtable/17388/20 "2026-08-28T08:30:50Z")

</div>

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`.
