DebugAllocator in shared library

Is there some way to use DebugAllocator (the artist formerly known as GeneralPurposeAllocator) inside a shared library? I am using an exe + shared library setup for hot reloading purposes and the shared library has it’s own allocator. The library returns a pointer to the game state on initialization and the exe then passes it to each call into the library (tick, draw etc). This works great but seems to force me into using std.heap.c_allocator as my allocator.

I tried using a DebugAllocator in the library, but it segfaults when allocating on subsequent calls (initial allocations work, but that is before the allocator pointer has passed over the exe/library boundary. I also tried passing an instance of DebugAllocator directly from the exe to the library, but the allocator type isn’t allowed in the C calling convention, which makes sense.

My interest in DebugAllocator is to access allocation statistics and to get the safety checks. Is my option to make a wrapper around the std.heap.c_allocator and implement my own statistics gathering and safety checks?

I’m not sure, but what you are trying to do should be possible. How is the allocator being passed between the exe and the shared lib? Is it a member of the game state? Are threads involved at all?

The current approach with std.heap.c_allocator stores the allocator on the game state:

pub const State = struct {
    allocator: std.mem.Allocator,
    // ...
};

A pointer to this game state is held by the exe and passed into the library on all subsequent calls. No additional threads involved yet.

1 Like

maybe State should have an optional pointer to the allocator instead of the allocator directly ?, maybe the State struct needs to be defined as extern or something like this ?

Storing a pointer to the allocator is a big refactor so I can’t test that quickly. But since all std functions accept an instance rather than a pointer, that doesn’t seem like the right direction.

Adding extern to the State struct has the same issue as trying to pass the allocator directly to a function with C calling convention. As I understand it the C ABI doesn’t support structs that use zig-only features. So an extern struct can’t contain an allocator.

1 Like

Hum you are right, hard to say what’s causing this issue, have you tried to look at the debugger ? are they compiled in the same mode ? recently I have seen a few issues on github related to compiler_rt ? maybe something to look for ? Hard to say from far away. Maybe you could try to copy paste the debug allocator, and changing it’s base allocator to be the c_allocator ?

I think you may have yourself a stack allocation issue. Usually the DebugAllocator is created as a stack variable. When you try to pass it into the State and return that, you may be passing back a copy which is referencing the stack. I can’t know for sure with out seeing how your game state is being created.

One option would be to use the c_allocator to heap allocate a spot for a Debug allocator, and init that. Then you can copy the allocator freely as it will always point to the heap allocated debug state. Otherwise you will need to store the debugallocator as part of the state to try and copy it and keep it alive.

The debugger gives me pretty strange results. With thread_safe to false it segfaults in part of my code trying to read from memory after an allocation. Looks like something overwrote the memory that was working fine before the allocation. If I don’t disable thread_safe it segfaults deep in the allocator on the same allocation. All of this works fine with std.heap.c_allocator.

Yeah, wrapping the c_allocator with code similar to DebugAllocator is what I’m thinking also, just trying to understand why c_allocator is needed in this case.

1 Like

Yeah, that might be possible. But wouldn’t I have the same issue when using the c_allocator if that were the case?

This is how I am allocating the state, this is working unless I replace c_allocator with DebugAllocator: gamedev-playground/src/game.zig at c145e39f027ffc11ad13bbac78297c596e0100ce · zoeesilcock/gamedev-playground · GitHub

I don’t think this will do what you want:

pub export fn reloaded(state_ptr: *anyopaque) void {
    const state: *State = @ptrCast(@alignCast(state_ptr));

State is a struct with undefined memory layout, you call this function with a pointer that points to an old state that was created in a shared library that was part of another compilation unit, once the newly compiled version of the library casts the *anyopaque back to *State the memory in that struct can be arranged according to the previous layout, but now the program is using it from the new layout, thus completely mixing up the bytes, reinterpreting them in invalid ways.

I think you would need to have some sort of serialization step to a defined layout of plain old data, for example convert the State struct to a ExternState struct and once you are reloading convert the ExternState back to a State.

With allocators it may be a bit tricky but in theory you should be able to write serialization and deserialization routines for those too, the most tricky part about that would be to update all the old pointers to new pointers.

Because it is tricky, I instead would suggest that you use indicies instead of pointers, if you have something that handles the memory for a group of things (some sort of instance-table or manager) than you can tell that thing to write out all those instances with a defined memory layout (serialize them) and on the side of the newly loaded library you can create a new instance of that thing and deserialize it to restore the instances. And because they just retain their indices you don’t have complicated pointer update traversals you need to do (if you have tree or graph relationships between instances).

De/-serialization is mostly needed if you have things that can’t be represented with external structs directly like normal structs. You also could have a hybrid scheme where most things are modeled with extern structs and only the things that can’t be get de-/serialized to and from extern structs.


Actually for allocators you might be able to de-/serialize the meta data of the allocator and thus make the newly constructed allocator the owner of the existing memory and if that memory only contains extern structs it could be kept as is.

2 Likes

Wow, that’s really interesting, thanks! I must have been very lucky because hot reloading has been working rock solid for months. Perhaps this issue only happens when a change in the structure of the State struct triggers the reload. Seems like I have bigger issues to work on before I get to replacing the c_allocator.

I’ll leave this opens because I am still curious if the DebugAllocator is compatible with this type of workflow or if it won’t work due to C ABI or similar.

I am not sure whether Zig’s fuzzer already tests randomized layouts, maybe that could be a way to create a repeatable test for this sort of hot-reloading code, but I think normal debug builds give you often the same layout, until you touch a specific struct, which then causes the compiler to prefer a different layout.

1 Like

Wow. I wasn’t even considering that this might become a problem. I’m mere days from repeating your problem. I knew I had to super careful about memory management. But I never expected not being able to transparently crossing the boundaries.

Yeah, it looks like you are stack allocating a DebugAllocator, creating the state with it, and then assigning the Allocator interface in state.

What is happening is that the std.mem.Allocator interface keeps a pointer to the stack variable for the DebugAllocator. When you eventually return the state, the DebugAllocator goes out of scope and is no longer available. Hence the Segfaults you were seeing when you tried to allocate with it later.

It works with the c_allocator because this is a global and references global state, so it will always be valid.

To use the DebugAllocator that you create inside your library, you will have to heap allocate it. If you tried to use a module scoped variable, you would run into a similar issue to what Sze described about your state: once your module reloads, you will change where that memory is pointing and could have another Segfault “randomly”.

1 Like

I hear the words and yet their meaning eluded me. I guess I’m tainted by years of C# development. The concept that the debug allocator is stack allocated when in my mind it’s a global thing. My mind keeps asking which thread’s stack is it allocated on, how can such black magic work?

And then I go back and read the code… oups… it’s not a global thing at all, stack allocated on the main thread on the very first line of code in the main function. And all I have to do is heap allocate it and make sure to pass the pointer along… awesome, my sanity is restored. Thanks for the heads up!

2 Likes

In Zig, the main allocator are stack allocated:

pub fn main() !void {
    // This is stack allocated
    const gpa: std.heap.DebugAllocator(.{}) = .init;

    // Same with ArenaAllocator
    const arena = std.heap.ArenaAllocator.init(gpa.allocator());

    // This interface stores a pointer to the stack value
    const allocator = gpa.allocator();  // Or arena.allocator()
}

If you use one of the others (i.e. c_allocator, wasm_allocator, page_allocator), the allocator references externally available functions that don’t require states the way that the Zig allocators do. So they are safe to use in any context.

1 Like

Since I have your attention and you seem to know your way around this topic. What I really want is to have multiple debug allocators. Essentially what I want is to be able to test that something cleans up properly after itself by manually calling deinit a debug allocator. Would that work?

My suggestion would be to take advantage of std.testing.allocator in test blocks. Each test gets a new instance of DebugAllocator provided by the test runner, and leak checking is done for you.

If you want to go a step further, consider std.testing.checkAllAllocationFailures

4 Likes

I don’t see why not. You could have the debug allocator be part of the struct and in the deinit for the struct call the debug allocator’s deinit.

But I agree with squeek502’s take. Test leak checking is the more idiomatic way to do it.

1 Like

Thanks both of you. I agree testing is the proper way to do it and the way I’ll be dealing with my own code base. However that is not an option for the plugins that my system will allow. I’m trying to protect the integrity of the core system from people who have no clue what they’re doing. :smiley:

Think of it as a rough way to determine how a plugin is performing. I love how the debug allocator is just there in plain code, the leak checking code and all… I should probably make my own version that outputs the data into data structures instead of the console.