Hotreloading in zig

I am trying to get hotreloading of code to work in zig. I implemented a small example, and it currently works, but I have a few questions / issues.

Link to repo: GitHub - samhattangady/hotreload: experiments with hotreloading in zig

Current Structure

There are two files; main.zig and game.zig. Both of these files have access
to the Game struct which is in the game.zig file.

To implement hot reloading, we compile an executable and a dynamic library.
main.zig is used to create an executable called reload.exe
and game.zig is used to create a dynamic library called hotreload.dll.

The dll only exposes the Game.update method, so the surface is very
minimal.

The executable loads the dll on startup, and can reload the dll whenever
the r key is pressed in the application. After we reload the dll, we update
the pointer to the update_game method.

To load the dll, we copy it from zig-out to another location to avoid access
clashes when we want to rebuild the library.
We rename the dll to avoid file name clashes. This is the dll that we then
load in.

When we want to build statically, we can turn off the HOTRELOAD flag,
and the whole project will compile statically. The changes between the two
modes are minimal. The state for storing dll data in main.zig is no longer
required, and we call game.update() directly rather than through the dll.

Current Issues

  1. Is this the correct approach? I am new to this, and I don’t know if there are any obvious issues that I am unaware of in this space.

  2. Is hot code swapping planned to release real soon? I think Andrew had streamed about it a while ago, but I don’t know if there have been any updates on that front.

  3. What is the best way to handle recompiling and reloading? The current approach involves copying the dll file out of zig-out/bin. If we use the dll directly from zig-out, then the next time we try to build the library, the application is already accessing the file, and we get an AccessDenied error.

  4. Debugging doesn’t work cleanly. I use RemedyBG, and I can attach to the running process. But the breakpoints don’t get correctly identified. This might be because we aren’t correctly handling the .pdb files, but I am not sure the correct way to do that.

According to the RemedyBG docs,

Note that RemedyBG will first try to load the PDB that is specified in the binary's header (PE32+).
If this cannot be found, then RemedyBG will make a second attempt to load the PDB file in same
directory as the binary.

I don’t know how to handle this issue, and which of the two attempts should be looked into.

  1. Adding more fields to the Game struct seems like it is not memory safe. It has worked
    a few times, but I think that might just be lucky.
    I would like to know if there is a way to make that more reliable. The way that I think about it is if I can alloc a large chunk of memory and then use the head of that to store the game struct, so that any additional fields will all be in memory that is safe to touch.
    Additionally, I think the zig spec does not preserve the ordering of elements in the struct, so
    there may be changes needed there, possibly using extern struct or packed struct.

  2. We want to automatically detect when a new dll has been compiled and reload it. Ideally, we would do this using std.fs.Watch, but that requires async which is not yet ready in self hosted. I tried to directly call the windows API, but didn’t get too far.
    I think I am okay waiting for the async feature to arrive in zig.

  3. The build.zig file is ugly, so when we rebuild just the dll, it tries to rebuild the executable as well, and fails. This is a smaller issue in the grand scheme.


I would appreciate any pointers on any of these issues.
Thanks.

6 Likes

Since you’re making a game, you’re on Windows, and you’re using RemedyBG, I assume you’re familiar with Handmade Hero. In case you aren’t, Casey Muratori covers hot reloading in this and the following video: https://youtu.be/WMSBRk5WG58?si=1NGAWDSw7lyLWv32

As for building the dll for hotreloading with the zig build system, I think I would try the following:

// build.zig
// ... more code ...
const hot = b.option(bool, "hot", "build for hotreloading");
if (hot) {
    const relative_path = b.fmt("libs/{}", .{std.time.timestamp()});
    b.resolveInstallPrefix(relative_path, .{}); // Docs say it's not supposed to be run build.zig... :shrug:
}
// ... more code ...

Now you can do zig build -Dhot and it’ll build directly to something like libs/1694763503 instead of zig-out. Your reload.exe could now scan the libs/ dir manually and keep a list of directories it has already seen. In the main loop, it could periodically scan the directory again to detect new entries and then load them on the spot.

Make sure you have the latest version of RemedyBG. I know there has been some issue with this kind of thing in the past, but RemedyBG was brought to life with hotreloading dlls in mind, so it should definitely work.

I hope any of this helps. All of these are just my thoughts on your issue. I don’t have a ton of experience with all of this myself.

3 Likes

I just wanted to let you know that Zig will support hot-code swapping in the compiler in our self-hosted backends. It’s not very usable yet but it’s under active development. I did two blog posts a while back describing what this will look like in Zig: Part 1 and Part 2.

8 Likes

Hey all… so is this more or less like a dynamic plugin system… e.g. if I build a main application that supported some sort of say… audio plugins (since audio apps with things like VST are pretty well known)… would you basically be able to provide a struct in the main application, and the dynamic runtime loaded library once loaded would be able to use that struct or provide an instance of it back to the main app? The, per hot reload… I could quickly reload an updated library without having to shut down the main application and the reload takes care of releasing memory/pointers/etc (in some manner) to the previously loaded module?

Does this work (or will work) on linux and mac (and arm)?

Hey. Thanks for your response.

I ended up updating to the newest version of remedy bg, and copying all 3 files (dll, pdb and lib) into a separate folder. I’ve not yet tested very deeply, but I think the debugging stuff is working correctly now.

1 Like

this is mind boggingly cool; is it reasonable to expect that this will support changes in data structures?

3 Likes

Let me get back to you on this when I’m back home, so tomorrow or so.

2 Likes

Hey Jakub, I hope you’re doing well. You wanted to share some knowledge on the hot code swapping in the compiler. Would be really great to hear about it. :slight_smile: Thanks!

1 Like

Sorry I didn’t reply earlier but got sucked into upstreaming ELF linker into Zig. We are very close now to being able to start experimenting with in-place binary updates and hot-code swapping on x86_64-linux. FWIW here’s the PR that brings us a lot closer to a functional HCS: elf: port 99% of zld ELF linker to Zig proper by kubkon · Pull Request #17556 · ziglang/zig · GitHub

On the topic of supporting changes in data structures, I spent some time thinking about this and generally we can easily swap in/out virtual (mapped) memory references to structures and such by doing in-memory relocation updates. However, if a struct was copied onto the stack, then we are stuck - an old value will linger there. I think circumventing this could be done via a smart use of functions that would return a pointer to such a struct or something, and you would only store a pointer to the said function on the stack. This is fine since we reference all functions via an offset table meaning the pointer to a cell in the offset table stays put for a function, while the actual location of the function (should it grow or shrink) can be fluid.

This is the best answer I can currently provide and I hope it’s not utter nonsense! I will have more concrete answers soon as we start playing with HCS on real projects such as Andrew’s tetris game and whatnot.

4 Likes