#zr - Simple, batteries included hot-reloading for Zig

1 messages · Page 1 of 1 (latest)

rare goblet
#

This is my first relatively-serious Zig project. It is a very easy to use library that helps you implement hot reloading for your application in just a couple of minutes. The dynamically loaded symbols are actually statically typed thanks to some meta-programming, so it's very safe to use, and it supports a "turn-off" switch that disables dynamic library shenanigans and just links the symbols statically.

Additionally it solves 2/4 of the biggest caveats of hot reloading with dynamic libraries, which are static/global variables and function pointers. With zr you can use them relatively seamlessly.

I would like to make this the best hot-reloading solution for Zig until the compiler gets support for hot-patching binaries on its own with eldritch magic. I think it already is that, but I would like some help if anyone's interested. I've tested this fully works on Linux, and it cross-compiles just fine to Windows but I'm not sure if it runs just fine. I would appreciate someone with a Windows computer to help me test if anything's wrong. MacOS should work fine but it's the same story.

I also think this can solve caveat 3 (threading) with some kind of thread tracker, and caveat 4 (struct size/layout changes) could potentially be something zr detects on its own with meta-programming shenanigans, to either crash or allow for custom handling. But that's in the future for now.
https://codeberg.org/zwynd/zr

rare goblet
#

I'll soon push some fixes that make this fully compatible with MacOS (in theory)

For Windows apparently dlfcn-win32 has a crash issue with alignment that I have to get sorted, most probably by doing away with the library and writing the code myself

fossil reef
rare goblet
#

Libc in POSIX has dladdr, which lets me get the export name of a function pointer with just the function's address

#

That lets registry.track_fnptr just take the function pointer itself

#

Seeing how many issues its causing in other operating systems though, I'm tempted to just remove it, use std.DynLib, and just require the export name as a parameter....

fossil reef
#

Yeah, tracking state of plugin is tricky. I've decided to always keep plugin's state in memory allocated by host app, and write pure functions in plugin whenever possible.

rare goblet
#

I have a semi-decent plan for tracking state:

  • Have the plugin export a function that returns the type info of its state
  • Store the type info of the previous dynamic library, obtain it again on reload, if they differ, the state layout/size has changed, notify the host

The host can then decide whether or not to crash immediately or maybe do a soft reset or do a fancy deserialization to keep going regardless

rare goblet
#

zr 0.2 is out!: https://codeberg.org/zwynd/zr/releases/tag/0.2

Changelog

  • Removed dependency on dlfcn-win32 and dladdr as a whole: zr should now work on Windows and MacOS without issues, hopefully. This, however, does mean that zr.Registry.track_fnptr has to take an extra parameter: the function the function pointer is pointing to’s export name.
  • Added support for multiple dynamic library APIs: zr can now use libc symbols directly as it did before, OR use std.DynLib (which unlocks Windows and Linux and no libc support for those operating systems), OR a completely custom backend provided by the host! This is controlled by the .dl_library field in zr.PluginCfg. It now defaults to Zig’s std.DynLib backend, though keep in mind that one has no support for loading null symbols without erroring out.
  • Added support for different link modes: zr now has an option to still link dynamically, but not perform hot reloading logic, by setting .link_mode = .dynamic_no_reload in PluginCfg. This is useful in case the host still wants to use dynamic libraries for its Plugins even on release mode, and it just wants to disable live reloading code. You can still link fully statically by doing .link_mode = .static.
  • Unified error tracking: Dynamic library APIs have very inconsistent errors since they come from the OS. Most of them are irrecoverable and come from programmer errors. zr now mimics libc’s approach of a global error variable (though atomic, so thread-safe) that describes what’s going wrong. Most functions return error.Unexpected and you are supposed to get more information via zr.err.load(.seq_cst).

It should hopefully fix Windows and MacOS issues since it no longer depends on dlfcn-win32.

#

I wanted to add a new example for using a custom backend, using SDL’s shared library API. However, I couldn’t find a way to add an SDL dependency to the project that’s only ever built if running an example. It’s very simple though, here’s how I do it in my own game:

const GamePlugin = zr.Plugin(@import("game"), .{
    // ...
    .dl_library = .{.custom = .{ 
        .open = sdl_dl_open,
        .sym = sdl_dl_sym,
        .close = sdl_dl_close,
    }},
    .syms = // ...
});

fn sdl_dl_open(path: [:0]const u8) error{Unexpected}!*anyopaque {
    const so = sdl.SharedObject.load(path) catch {
        if (sdl.errors.get()) |err| {
            zr.err.store(err.ptr, .seq_cst);
        }
        return error.Unexpected;
    };
    return so.value;
}

fn sdl_dl_sym(handle: *anyopaque, name: [:0]const u8) error{Unexpected}!?*const anyopaque {
    var so = sdl.SharedObject { .value = @ptrCast(handle) };
    const sym = so.loadFunction(name) catch {
        if (sdl.errors.get()) |err| {
            zr.err.store(err.ptr, .seq_cst);
        }
        return error.Unexpected;
    };
    return sym;
}

fn sdl_dl_close(handle: *anyopaque) void {
    var so = sdl.SharedObject { .value = @ptrCast(handle) };
    so.unload();
}```

Hopefully this addition makes `zr` usable basically anywhere.
rare goblet
# rare goblet I have a semi-decent plan for tracking state: - Have the plugin export a functio...

Next stop, doing this.

Also, if anyone here has done live reloading on an application with threads, I would appreciate some input on how they got it to work and what it needs. I've only read the "all threads must be paused before live reload" bit on obscure HandmadeHero forum posts, but I haven't actually tried it myself. Ideally zr should have some kind of thread tracking to be truly perfect!

umbral patrol
#

Is this working on Linux? I keep on getting segfaults all the time (posted an issue).

#

On another computer it didn't watch for file changes for some reason so I had to build manually (at which point it segfaulted there as well).

umbral patrol
#

So it seems to be working if I use llvm for both the host and the plugin. On my Linux laptop I get a reload when I save the plugin file whereas on my Linux desktop I have to zig build every time.

rare goblet
#

I actually get that weird behaviour too - with it reloading every save. I have no idea what causes that. Having to run zig build every time is the expected behaviour. I assume some weird watch thing is going on with the compiler

rare goblet
umbral patrol
umbral patrol
umbral patrol
#

I've played around quite a lot with this now (with the help of Claude). Custom backend is working for me now. Basically what was needed

  • Create a new tmp file each reload. I tried unlinking and that works except then a function like @returnAddress doesn't work. The only thing I find actually working is waiting until the end of the program, ie. CTRL+C, to remove the files. Everything else seems to run into issues (also tried having two tmp files and swap between them but that segfaults as well).
  • Plugin.symbol needs to return a pointer. Otherwise the same dll will be reloaded instead of the new one.
  • You may know this already but bleep and bloop don't have to be exposed. They reload without exporting. You only need to export something that should be directly called by the host, as well as globals that should be restored. Maybe the example should reflect that by bleep being registered and bloop not registered. I don't really see what's gained by exposing an internal function but there might be something I'm missing.

I'm still curious why you cannot have the host as LLVM and the plugin with the custom backend. The segfault talks about something being unaligned. Is this a permanent issue?

#

I'm still playing around with getting things working well but this is much more promising than a week ago. The custom backend is a lot quicker than LLVM so it seems totally worth it to have it working. Not sure what I've done so far is valuable for other people. I could create a PR I guess but I have no idea if any of this works outside of Linux and specifically using posix to handle CTRL+C won't work on Windows of course.

rare goblet
#

I suppose creating a new temporary plugin file every reload is the brute force approach that works on all linkers yeah. Might consider adding that. Another thing you could try that has worked for me before is to create a .done file when the compiler is finished and to check the last write time on that.
Function pointers have to be exported, because they are reloaded by calling dlsym on them. They can't be reloaded like any other globals, because pointers are ephemeral and they will change on every reload. That's why they have to be handled specially in the first place. Though maybe it's different on Linux?
The two different modules can't be compiled with different backends because that probably means the data layout / calling conventions change in minute hard to diagnose ways. I don't really get why you'd want that anyway? What is your usecase for compiling the host with one backend and the plugin with another?

#

Thank you for your help and interest, but I refuse to have LLM death machine slop in any of my projects, so I wouldn't accept the PR. I'll probably work on this on my own time when I find interest, or you can just fork the project.

ebon pier
#

I recently added hot reload to my game, I didn’t use zr but I have a bootstrap.exe that loads a game.dll (all my logic) and a core.dll (libs any any code that can’t be hot reloaded, this includes libc) the reason why I load both of them is because the bootstrap.exe doesn’t depends on any of the dependencies of game.dll so unloading game.dll also unloads the core.dll, not sure if this is a windows thing…

I saw some folks using https://github.com/adabz/PE-Parser to list dependencies of a dll. it can do much more like maybe listing all the exported globals… maybe it could be used to create some kinda of metadata that can automatically copy the globals on reload. Pointers to constant data like string can’t be solved easily so I just dupe them when it matters.

GitHub

PE-Parser, a small project I wrote for parsing format of executable files and dynamic libraries under Windows. It is able to parse DLL or EXE files, extract and produce information about the file, ...

fossil reef
ebon pier
fossil reef
#

No issue really. From what I see, std.DynLib's Windows support was removed along with a bunch of modifications to further improve std.Io stuff.

rare goblet
#

That is quite dumb---

rare goblet
ebon pier
#

Yes, but others don’t, including my code 😛 I mean yes a bit overkill but I do create a lot of global state

#

I created a proof of concept that LoadLibrary parse their contents (super simple a couple of lines of code) and it’s able to memcpy the .data section across DLL reloads, but I didn’t fully tested it