// modules for managing loading dynamic libraries // const ModuleInterface = struct { startup: *const fn (*anyopaque, ?*anyopaque) callconv(.C) bool = undefined, shutdown: *const fn () callconv(.C) void = undefined, }; pub const ModuleLoaderArgs = extern struct { firstLoad: bool, engine: *core.Engine, nameRegistry: *p2.NameRegistry, packerFS: *core.PackerFS, // luaState: *anyopaque, // luaAllocator: *anyopaque, debugDrawInterface: *debug.DebugDrawInterface, }; pub const LoadedModule = struct { baseModulePath: []const u8, moduleName: []const u8, stagedPaths: std.ArrayListUnmanaged([]u8) = .{}, loaded: std.ArrayListUnmanaged(ModuleInterface) = .{}, activePath: ?[]const u8 = null, stagingCount: u32 = 0, lastLoad: i64 = 0, lastModification: i64 = 0, pdbPath: ?[]const u8 = null, startOnLoad: bool = true, started: bool = false, initialLoad: bool = true, autoLoad: bool = true, loadNextFrame: bool = false, pub fn getInterface(self: @This()) ?ModuleInterface { return self.loaded.getLastOrNull(); } pub fn stageModule(self: *@This(), allocator: std.mem.Allocator) !void { //const stagingDirectory = ".modulecache//0/"; const stagingDirectoryPath = try std.fmt.allocPrint(allocator, ".modulecache/{d}/{s}/{d}", .{ core.getSessionStamp(), self.moduleName, self.stagedPaths.items.len, }); defer allocator.free(stagingDirectoryPath); try std.fs.cwd().makePath(stagingDirectoryPath); const stagedPath = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ stagingDirectoryPath, try p2.sharedLibNameAlloc(allocator, self.moduleName), }); try std.fs.cwd().copyFile(self.baseModulePath, std.fs.cwd(), stagedPath, .{}); if (self.pdbPath) |pdbPath| { const stagedPdbPath = try std.fmt.allocPrint(allocator, "{s}/{s}.pdb", .{ stagingDirectoryPath, self.moduleName }); try std.fs.cwd().copyFile(pdbPath, std.fs.cwd(), stagedPdbPath, .{}); } self.stagedPaths.append(allocator, stagedPath) catch unreachable; } pub fn loadStagedModule(self: *@This(), allocator: std.mem.Allocator) !void { const stagedPath = self.stagedPaths.getLast(); var lib = try std.DynLib.open(stagedPath); var mod = ModuleInterface{}; mod.startup = lib.lookup(@TypeOf(mod.startup), "startup") orelse return error.BadModule; mod.shutdown = lib.lookup(@TypeOf(mod.shutdown), "shutdown") orelse return error.BadModule; try self.loaded.append(allocator, mod); } pub fn load(self: *@This(), allocator: std.mem.Allocator) !void { core.engine_log("[ModuleLoader] loading module: ", .{}); self.lastLoad = std.time.microTimestamp(); try self.stageModule(allocator); try self.loadStagedModule(allocator); } // logic for testing if we should stage and load // // 1. modification check time > last load time // 2. current time > modification check time + debounce pub fn maybeStageAndLoad(self: *@This(), allocator: std.mem.Allocator) !bool { var shouldLoad: bool = false; if (self.lastModification > self.lastLoad) { const currentTime = std.time.microTimestamp(); // give 100ms for debouncing if (currentTime > self.lastModification + 50_000) { shouldLoad = true; } } if (self.initialLoad) { self.initialLoad = false; shouldLoad = true; } if (shouldLoad and (self.autoLoad or self.loadNextFrame)) { self.loadNextFrame = false; try self.load(allocator); return true; } return false; } }; fn dllChangedCallback(pathChanged: []const u8, ctx: ?*anyopaque) void { const loaded: *LoadedModule = @ptrCast(@alignCast(ctx.?)); loaded.lastModification = std.time.microTimestamp(); core.engine_log("{s} change detected", .{pathChanged}); } pub const ModuleLoader = struct { backingAllocator: std.mem.Allocator, arena: std.heap.ArenaAllocator, loadedModules: std.ArrayListUnmanaged(*LoadedModule) = .{}, watchInitialized: bool = false, pub var NeonObjectTable = core.EngineObjectVTable.from(@This(), "core.ModuleLoader"); pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .backingAllocator = allocator, .arena = std.heap.ArenaAllocator.init(allocator), }; return self; } pub fn addModule(self: *@This(), moduleName: []const u8) !void { // modules are always looked for under zig-out/modules if (core.BuildOption("static_build")) { core.engine_log("[ModuleLoader]: skipping dynamic load, hot loading is not available", .{}); return; } core.engine_log("[ModuleLoader]: adding module to watch '{s}'", .{moduleName}); const libFileName = try p2.sharedLibNameAlloc(self.arena.allocator(), moduleName); const loaded = try self.arena.allocator().create(LoadedModule); if (self.watchInitialized == false) { core.fs().watchPath("zig-out/modules/"); self.watchInitialized = true; } loaded.* = LoadedModule{ .baseModulePath = try std.fmt.allocPrint(self.arena.allocator(), "zig-out/modules/{s}", .{libFileName}), .moduleName = moduleName, .lastModification = std.time.microTimestamp(), }; if (@import("builtin").os.tag == .windows) { loaded.pdbPath = try std.fmt.allocPrint(self.arena.allocator(), "zig-out/modules/{s}.pdb", .{libFileName[0 .. libFileName.len - 4]}); } try core.fs().addFileChangedCallback(libFileName, dllChangedCallback, loaded); try self.loadedModules.append(self.arena.allocator(), loaded); } pub fn activateModuleVersion(self: *@This(), mod: *LoadedModule, index: usize) void { mod.autoLoad = false; // once we load a specific version we don't want to autoload anymore // if (index < mod.loaded.items.len) { var a = self.backingAllocator; var args = getModuleLoaderArgs(false); if (mod.loaded.items[index].startup(&a, &args)) { core.engine_log("module startup done", .{}); } } } pub fn tick(self: *@This(), dt: f64) void { _ = dt; for (self.loadedModules.items) |loaded| { const didLoad = loaded.maybeStageAndLoad(self.arena.allocator()) catch |err| { core.engine_log("unable to load module error: {any}", .{err}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } continue; }; if (didLoad) { const i = loaded.getInterface(); if (i) |interface| { if (loaded.startOnLoad) { if (!loaded.started) { var a = self.backingAllocator; var args = getModuleLoaderArgs(loaded.loaded.items.len == 1); if (interface.startup(&a, &args)) { core.engine_log("[ModuleLoader] module startup done", .{}); } } } } } } } pub fn destroy(self: *@This()) void { // std.fs.cwd().deleteTree(".modulecache") catch {}; self.arena.deinit(); self.backingAllocator.destroy(self); } }; pub fn getModuleLoaderArgs(first: bool) ModuleLoaderArgs { return .{ .firstLoad = first, .engine = core.getEngine(), .nameRegistry = core.names.gRegistry, .packerFS = core.fs(), // .luaState = @ptrCast(core.script.gLuaState.l), // .luaAllocator = &core.script.gLuaAllocator, .debugDrawInterface = debug.gDebugDrawInterface.?, }; } const std = @import("std"); const core = @import("../core.zig"); const p2 = @import("p2"); const debug = @import("../debug_draw.zig");