How to downcast an allocator?

Let’s say I have an std.mem.Allocator, which might or might not be a concrete MyAllocator. Is there some way I can “downcast” to MyAllocator (with appropriate runtime checks), so that I can take advantage of extra APIs provided by MyAllocator without necessary making the type of the allocator concrete throughout the stack?

I think I can do that by comparing whether vtable is the same as the one I have, using the code roughly like this:

const std = @import("std");
const assert = std.debug.assert;

const MyAllocator = @This();

inner: std.mem.Allocator,

pub fn wrap(inner: std.mem.Allocator) MyAllocator {
    return .{ .inner = inner };
}

pub fn downcast(a: std.mem.Allocator) ?*MyAllocator {
    if (!std.meta.eql(a.vtable.*, my_vtable)) return null; // <- The Trick
    return @ptrCast(@alignCast(a.ptr));
}

pub fn allocator(my_allocator: *MyAllocator) std.mem.Allocator {
    return .{ .ptr = my_allocator, .vtable = &my_vtable };
}

const my_vtable: std.mem.Allocator.VTable = .{
    .alloc = alloc,
    .resize = resize,
    .free = free,
};

It seems to work in my very light testing.

But is this pattern sound? I know that comparing function pointers for equality is fraught with peril, at lest in Rust. What about Zig? Would the code above do what I want it to do, or are there any sleeping dragons?

3 Likes

What are you going to do if the allocator isn’t what you’re expecting? Presumably, just crash, at runtime, for something that should have been caught at compile time. I recomend creating your own interface, with the methods that you need. Make it so this interface can create a std’s Allocator. When you need to interface with code that expects the regular std’s Allocator, you just create one and pass it.

1 Like

In my case, it’s fine to do nothing — the extra functionality is for recording extra metadata for debugging, and, if the allocator is different, the correct behavior is indeed to do nothing.

And I want the code to be polymorphic in the allocator, so that it can be used with my debug allocatore, but also with any other allocator if the caller can’t be bothered to use mine.

3 Likes

I think you would still want your own interface. Make the function pointers that you don’t require as optional. Make a convenience function that simply takes a normal allocator and creates your custom allocator, with the optional fields set to null.
Another possibility is to receive your allocator as anytype and check at compile time if it is your custom allocator or the std Allocator.

Why compare the whole value of the vtable structure? I would just compare the vtable pointers. As long as there is only ever one value for my_vtable it shouldn’t be problematic.

vtable is a field and its address is different in each Allocator instance. But you need a pointer that is different for each allocator concrete type but is the same for different instances of that same type. This is why @matklad is comparing all function pointers assigned in the vtable.

That doesn’t make sense. std.mem.Allocator.vtable is a pointer and it’s value will always be equal to &my_vtable for instances of MyAllocator.

1 Like

Here is an example from std GPA:

On every call a new Allocator instance is created and returned vtable does not point to any fixed or static value, right?

1 Like

Interesting example. Taking the address of a literal must be static. It’s not allocated and it better not be on the stack, so it must be static. I’ve never used this pattern myself though.

Is it truly global or static per CU? In general it is safer to compare values than addresses. Values here are addresses of alloc, resize and free and if all of them match hopefully it is not a false positive.

alloc, resize and free are also just pointers to static symbols, so it’s pretty much exactly the same thing. Comparing three pointers instead of one doesn’t really make it any safer.

1 Like

11 posts were merged into an existing topic: Diving deep into anonymous struct literals