I the past week I put together very quickly a working prototype for a game framework that hot-reloads both code and assets, losely based on the handmade aproach (https://youtu.be/WMSBRk5WG58?si=oHDSAOOzF-2e0PoM).
I has a bootstrap.exe, core.dll and game.dll.
The bootstrap is responsible to loads both the code and game dlls and to reload the later when it changes, the core.dll is imutable it can not be reloaded because it holds global state comming from libraries such as raylib, imgui… The game.dll is linked against core.dll and where all the game logic resides, pretty standard sutff.
Now the trick part, I developed a zig framework that I wish to use instead of raylib and the only way I know to make this work is by exporting all of my functions, and to make all structs extern and then write zig bindings for my zig lib 
Is there any other way of consuming a zig shared library from zig without needing to export symbols?
1 Like
export and extern(on decls) are for cross object exposing and using symbols. pub and @import only work within an object, not across multiple.
And zig doesnt have a stable abi, so you have to wrap that aswell, usually you’d use the c abi, which is the default.
you dont have to make everything compatible with the abi, only the exposed api needs to be, and that can just be a wrapper over the zig api
1 Like
Keeping a header file and a c file was always one of my biggest pain points when using c/c++ and also one the most important reasons why I switched to something else.
I often make my globals extern and let the functions as is, the globals have to be defined and exported in a separated file, but this is the quickest way.
For functions, I usually implement them normally as if I was just writing zig, then in a separated file I export them and in another file I reimplement the zig interface but calling the exported function, the exported functions have to be redefined using the extern keyword.
This sucks, it duplicates quite a bit of code and adds tons of boilerplate, not to mention that there is nothing from keeping the export and extern definitions the same, any change to the api requires the modification in 5 different places…
It sounds like you are reimplementing things in c, which for simple things sure. But if it is annoying, a c wrapper around a zig api should be less so.
Unfortunately zig’s ability to generate headers of exported things from a zig project has long been broken :(. You’d have to write it yourself or use code generation.
I found a better solution, it’s way easier, no boiler plate, no bindings:
Declare every global as pointer, this will add a layer of redirection but it can be optimized away when static linking.
Pick a single global (game in the example) variable to contain all the others, on init and on reload assign the pointers to each global.
If static linking is desired, like in my case, can use a “macro” to declare game as value, with this the compiler will optimize away the extra redirection.
You will need to take extra care to kill threads before reloading.
pub export fn game_setup() callconv(.c) ?*anyopaque {
if (hot_reloadable) {
// allocated memory survives between reloads
game = gpa.create(Game) catch return null;
}
setupGlobalPointers()
return game.ref();
}
pub export fn game_pre_reload() callconv(.c) void {
if (hot_reloadable) {
// threads procs will be invalided on reload so kill then
jobs.kill();
audio.kill();
}
}
pub export fn game_reload(ptr: *anyopaque) callconv(.c) void {
if (hot_reloadable) {
game = @ptrCast(@alignCast(ptr)); // restore allocated pointer
setupGlobalPointers()
jobs.restart();
audio.restart();
}
}
fn setupGlobalPointers() void {
jobs.sys = &game.jobs;
audio.sys = &game.audio;
ui.sys = &game.ui;
Tex.con = &game.textures;
Mesh.con = &game.meshes;
}
const Game = struct {
// global declarations
ui: ui.Sys,
audio: audio.Sys,
jobs: jobs.Sys,
textures: Tex.Container,
meshes: Mesh.Container,
}
var game: Global(Game) = undefined;
fn Global(comptime T: type) type {
// when static linking the compiler can optimize away the extra redirction
// required by other globals in jobs, audio, ui, Tex, and Mesh
return if (hot_reloadable) *T else T;
}
const hot_reloadable = builtin.link_mode == .dynamic;
const builtin = @import("builtin");
Without taking into consideration all the extra work the preivous method had some limitations, I couldn’t iterate in the subsystems ui/audio/jobs as fast as I could in the game code, 1 they couldn’t be hot-reloaded, 2 they will trigger a compilation of both core.dll and game.dll making the compile times much slower.