From 2289490ca8a0a8bcc5a87fbbe3438cd977238d2f Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sun, 20 Jul 2025 11:52:24 -0400 Subject: [PATCH 1/6] initial commit. working prototype. --- .gitignore | 5 ++ build.zig | 47 +++++++++++++ build.zig.zon | 86 +++++++++++++++++++++++ src/bar.zig | 24 +++++++ src/core.zig | 166 +++++++++++++++++++++++++++++++++++++++++++ src/foo.zig | 25 +++++++ src/main.zig | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 543 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/bar.zig create mode 100644 src/core.zig create mode 100644 src/foo.zig create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104cddc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea +/.envrc + +/zig-out/ +/.zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..0df8a7d --- /dev/null +++ b/build.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +pub fn installModule(b: *std.Build, plug: *std.Build.Step.Compile) void { + std.debug.assert(plug.isDynamicLibrary()); + const install = b.addInstallArtifact(plug, .{ + .dest_dir = .{ .override = .{ .custom = "mods" } }, + }); + b.getInstallStep().dependOn(&install.step); +} + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exedriver = b.addExecutable(.{ + .name = "driver", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + b.installArtifact(exedriver); + + const libfoo = b.addSharedLibrary(.{ + .name = "foo", + .root_source_file = b.path("src/foo.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + installModule(b, libfoo); + + const libbar = b.addSharedLibrary(.{ + .name = "bar", + .root_source_file = b.path("src/bar.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + installModule(b, libbar); + + const run = b.addRunArtifact(exedriver); + run.step.dependOn(b.getInstallStep()); + run.setEnvironmentVariable("MODS_DIR", b.getInstallPath(.{ .custom = "mods" }, "")); + + b.step("run", "").dependOn(&run.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..6027e5e --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,86 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .zig_hotswap, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0xa7823a599d65a583, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/bar.zig b/src/bar.zig new file mode 100644 index 0000000..ca32e3b --- /dev/null +++ b/src/bar.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const core = @import("core.zig"); + +const Self = @This(); + +export const MODULE = core.module(Self); + +pub fn startup(_: std.mem.Allocator) !Self { + std.log.debug("!! startup {s} !!", .{@typeName(@This())}); + return .{}; +} + +pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { + std.log.debug("!! unload {s} !!", .{@typeName(@This())}); + return &.{}; +} + +pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); +} + +pub fn shutdown(_: *Self, _: std.mem.Allocator) void { + std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); +} diff --git a/src/core.zig b/src/core.zig new file mode 100644 index 0000000..06dc8a0 --- /dev/null +++ b/src/core.zig @@ -0,0 +1,166 @@ +const std = @import("std"); + +// Notes: +// +// "init" is an overloaded term. Try not to use it. Instead, have hooks: +// - startup / shutdown. executed once each per program lifecycle. +// - load / unload. executed once per module load cycle. +// +// So the flow is: +// - startup. prepares long-lived state. +// - load. deserializes and prepares short-lived state. +// - unload. serializes and releases short-lived state. +// - shutdown. releases long-lived state. +// +// So when the hotplug system reloads the module, it performs an unload/load cycle. +// Normal operation without any reloads includes all four stages. +// +// Some considerations: +// - a module should be able to indicate specific failure: +// - startup (preparing state) +// - load (deserialization or preparing state) +// - unload (serialization) +// - a module should be able to indicate it cannot be reloaded. +// - serialization data might come from disk, or from a reload, or be missing. +// +// On reloading and rollback: +// - when loading a module, copy the dynlib to a versioned temporary file, and load that temporary file. +// - procedure for reloading a module: +// - current old version unload (and serialize) +// - attempt new version load (and deserialize) +// - if success: release old version and delete lib +// - if failure: +// - release new version and delete lib. +// - attempt old version load (and deserialize) (catch fatal error) +// - Configure signal handlers to always clean up the temporary files on errors like segfault? +// - temp files needs to gracefully handle multiple instances of the app running at once with shared hotloading. +// +// Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the +// module signal when it can safely be unloaded. + +pub fn ModuleType(Self: type) type { + return extern struct { + pub const Startup = fn (alloc: std.mem.Allocator) anyerror!Self; + pub const CStartup = fn (alloc: *const std.mem.Allocator) callconv(.c) ?*anyopaque; + + pub const Unload = fn (self: *Self, alloc: std.mem.Allocator) anyerror![]u8; + pub const CUnload = fn (self: *Self, alloc: *const std.mem.Allocator, ptr: *[*]u8, len: *usize) callconv(.c) bool; + + pub const Reload = fn (self: *Self, alloc: std.mem.Allocator, data: []u8) anyerror!void; + pub const CReload = fn (self: *Self, alloc: *const std.mem.Allocator, ptr: [*]u8, len: usize) callconv(.c) bool; + + pub const Shutdown = fn (self: *Self, alloc: std.mem.Allocator) void; + pub const CShutdown = fn (self: *Self, alloc: *const std.mem.Allocator) callconv(.c) void; + + cstartup: *const CStartup, + creload: *const CReload, + cunload: *const CUnload, + cshutdown: *const CShutdown, + + pub fn startup(mod: @This(), alloc: std.mem.Allocator) !*Self { + return mod.cstartup(&alloc) orelse error.ModuleInitFailed; + } + + pub fn unload(mod: @This(), self: *Self, alloc: std.mem.Allocator) ![]u8 { + var data: []u8 = undefined; + if (mod.cunload(self, &alloc, &data.ptr, &data.len)) { + return data; + } else { + return error.ModuleUnloadFailed; + } + } + + pub fn reload(mod: @This(), self: *Self, alloc: std.mem.Allocator, data: []u8) !void { + if (mod.creload(self, &alloc, data.ptr, data.len)) {} else { + return error.ModuleReloadFailed; + } + } + + pub fn shutdown(mod: @This(), self: *Self, alloc: std.mem.Allocator) void { + mod.cshutdown(self, &alloc); + } + }; +} + +pub fn module(Self: type) ModuleType(Self) { + const Module = ModuleType(Self); + + const impl = struct { + pub const startup: Module.Startup = Self.startup; + + pub fn cstartup( + alloc: *const std.mem.Allocator, + ) callconv(.c) ?*anyopaque { + const self: *Self = alloc.create(Self) catch |err| { + std.log.err( + "Module {s} startup failed: {!}", + .{ @typeName(Self), err }, + ); + return null; + }; + self.* = startup(alloc.*) catch |err| { + alloc.destroy(self); + std.log.err( + "Module {s} startup failed: {!}", + .{ @typeName(Self), err }, + ); + return null; + }; + return self; + } + + pub const unload: Module.Unload = Self.unload; + + pub fn cunload( + self: *Self, + alloc: *const std.mem.Allocator, + ptr: *[*]u8, + len: *usize, + ) callconv(.c) bool { + const data = unload(self, alloc.*) catch |err| { + std.log.err( + "Module {s} unload failed: {!}", + .{ @typeName(Self), err }, + ); + return false; + }; + ptr.* = data.ptr; + len.* = data.len; + return true; + } + + pub const reload: Module.Reload = Self.reload; + + pub fn creload( + self: *Self, + alloc: *const std.mem.Allocator, + ptr: [*]u8, + len: usize, + ) callconv(.c) bool { + reload(self, alloc.*, ptr[0..len]) catch |err| { + std.log.err( + "Module {s} reload failed: {!}", + .{ @typeName(Self), err }, + ); + return false; + }; + return true; + } + + pub const shutdown: Module.Shutdown = Self.shutdown; + + pub fn cshutdown( + self: *Self, + alloc: *const std.mem.Allocator, + ) callconv(.c) void { + shutdown(self, alloc.*); + } + }; + + return .{ + .cstartup = &impl.cstartup, + .creload = &impl.creload, + .cunload = &impl.cunload, + .cshutdown = &impl.cshutdown, + }; +} diff --git a/src/foo.zig b/src/foo.zig new file mode 100644 index 0000000..1dbd818 --- /dev/null +++ b/src/foo.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const core = @import("core.zig"); + +const Self = @This(); + +export const MODULE = core.module(Self); + +pub fn startup(_: std.mem.Allocator) !Self { + std.log.debug("!! startup {s} !!", .{@typeName(@This())}); + return .{}; +} + +pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { + std.log.debug("!! unload {s} !!", .{@typeName(@This())}); + return &.{}; +} + +pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); + std.log.debug("!! NEW CONTENT !!", .{}); +} + +pub fn shutdown(_: *Self, _: std.mem.Allocator) void { + std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..29af81c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,190 @@ +const std = @import("std"); +const linux = std.os.linux; +const core = @import("core.zig"); + +const ModLib = struct { + path: []const u8, + lib: std.DynLib, + mod: *core.ModuleType(anyopaque), + + fn open(realpath: []const u8, version: u32, alloc: std.mem.Allocator) !ModLib { + const basename = std.fs.path.basename(realpath); + const name = try std.fmt.allocPrint( + alloc, + ".{s}.{d}", + .{ basename, version }, + ); + defer alloc.free(name); + + const path = if (std.fs.path.dirname(realpath)) |dirname| + try std.fs.path.join(alloc, &.{ dirname, name }) + else + try alloc.dupe(u8, name); + errdefer alloc.free(path); + + try std.fs.copyFileAbsolute(realpath, path, .{}); + + var lib = try std.DynLib.open(path); + errdefer lib.close(); + + const mod = lib.lookup(*core.ModuleType(anyopaque), "MODULE") orelse + return error.MissingModuleDefinition; + + return .{ + .path = path, + .lib = lib, + .mod = mod, + }; + } + + fn close(self: *ModLib, alloc: std.mem.Allocator) void { + self.lib.close(); + // std.fs.deleteFileAbsolute(self.path) catch std.log.warn("Failed to delete {s}", .{self.path}); + alloc.free(self.path); + } +}; + +const DynamicModule = struct { + gpa: std.heap.GeneralPurposeAllocator(.{}), + realpath: []const u8, + curr: ModLib, + state: *anyopaque, + next: u32, + + pub fn init(path: []const u8, alloc: std.mem.Allocator) !DynamicModule { + const realpath = try std.fs.cwd().realpathAlloc(alloc, path); + errdefer alloc.free(realpath); + + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + errdefer if (gpa.deinit() == .leak) { + std.log.warn("Module {s} leaked.", .{realpath}); + }; + + var self: DynamicModule = .{ + .gpa = gpa, + .realpath = realpath, + .curr = undefined, + .state = undefined, + .next = 0, + }; + + self.curr = try ModLib.open(realpath, self.next, alloc); + errdefer self.curr.close(alloc); + + self.state = try self.curr.mod.startup(self.gpa.allocator()); + errdefer self.curr.mod.shutdown(self.state, self.gpa.allocator()); + + // todo deserialize from disk. + try self.curr.mod.reload(self.state, self.gpa.allocator(), &.{}); + + self.next += 1; + + return self; + } + + pub fn reload(self: *@This(), alloc: std.mem.Allocator) !void { + var next = try ModLib.open(self.realpath, self.next, alloc); + errdefer next.close(alloc); + + const data = try self.curr.mod.unload(self.state, self.gpa.allocator()); + errdefer self.curr.mod.reload(self.state, self.gpa.allocator(), data) catch + std.debug.panic("Failed to rollback to {s}", .{self.curr.path}); + + try next.mod.reload(self.state, self.gpa.allocator(), data); + + self.curr.close(alloc); + self.next += 1; + self.curr = next; + } + + pub fn deinit(self: *DynamicModule, alloc: std.mem.Allocator) void { + const data = self.curr.mod.unload(self.state, self.gpa.allocator()) catch + std.debug.panic("Failed to unload {s}", .{self.curr.path}); + + _ = data; // todo serialize to disk. + + self.curr.mod.shutdown(self.state, self.gpa.allocator()); + + self.curr.close(alloc); + + if (self.gpa.deinit() == .leak) { + std.log.warn("Module {s} leaked.", .{self.realpath}); + } + + alloc.free(self.realpath); + } +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; + defer _ = gpa.deinit(); + const alloc = gpa.allocator(); + + const self = try std.fs.selfExeDirPathAlloc(alloc); + defer alloc.free(self); + std.log.debug("running from '{s}'", .{self}); + + var env = try std.process.getEnvMap(alloc); + defer env.deinit(); + + const mods_path = if (env.get("MODS_DIR")) |envvar| blk: { + break :blk try alloc.dupe(u8, envvar); + } else blk: { + const dir = try std.fs.selfExeDirPathAlloc(alloc); + defer alloc.free(dir); + break :blk try std.fs.path.resolve(alloc, &.{ dir, "..", "mods" }); + }; + defer alloc.free(mods_path); + + var mods: std.ArrayListUnmanaged(DynamicModule) = .{}; + defer { + for (mods.items) |*mod| { + mod.deinit(alloc); + } + mods.deinit(alloc); + } + + std.log.debug("loading mods in '{s}'", .{mods_path}); + { + var dir = try std.fs.openDirAbsolute(mods_path, .{ .iterate = true }); + defer dir.close(); + + // const fd = linux.inotify_init1(linux.IN.NONBLOCK); + // defer linux.close(fd); + // var fds = [_]linux.pollfd{ + // .{ .fd = fd, .events = linux.IN.ALL_EVENTS, .revents = undefined }, + // }; + // const n = linux.poll(&fds, fds.len, 0); + + var it = dir.iterate(); + while (try it.next()) |entry| { + const mod_path = try std.fs.path.resolve(alloc, &.{ mods_path, entry.name }); + defer alloc.free(mod_path); + + if (std.mem.startsWith(u8, std.fs.path.basename(mod_path), ".")) + continue; + + var mod = try DynamicModule.init(mod_path, alloc); + errdefer mod.deinit(alloc); + try mods.append(alloc, mod); + } + } + + std.log.debug("-" ** 80, .{}); + for (0..10) |x| { + std.log.debug("{d}...", .{x}); + std.time.sleep(std.time.ns_per_s); + } + std.log.debug("reloading.", .{}); + std.time.sleep(std.time.ns_per_s); + + for (mods.items) |*mod| { + if (mod.reload(alloc)) |_| { + std.log.debug("loaded {s}", .{mod.curr.path}); + } else |_| { + std.log.debug("rolled back to {s}", .{mod.curr.path}); + } + } + + std.log.debug("-" ** 80, .{}); +} From caaad2fb0508a47e94349700739695e6695bf876 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sun, 20 Jul 2025 12:54:22 -0400 Subject: [PATCH 2/6] experimenting with inotify --- src/foo.zig | 1 - src/main.zig | 100 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/foo.zig b/src/foo.zig index 1dbd818..ca32e3b 100644 --- a/src/foo.zig +++ b/src/foo.zig @@ -17,7 +17,6 @@ pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { std.log.debug("!! reload {s} !!", .{@typeName(@This())}); - std.log.debug("!! NEW CONTENT !!", .{}); } pub fn shutdown(_: *Self, _: std.mem.Allocator) void { diff --git a/src/main.zig b/src/main.zig index 29af81c..df0589a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -149,13 +149,6 @@ pub fn main() !void { var dir = try std.fs.openDirAbsolute(mods_path, .{ .iterate = true }); defer dir.close(); - // const fd = linux.inotify_init1(linux.IN.NONBLOCK); - // defer linux.close(fd); - // var fds = [_]linux.pollfd{ - // .{ .fd = fd, .events = linux.IN.ALL_EVENTS, .revents = undefined }, - // }; - // const n = linux.poll(&fds, fds.len, 0); - var it = dir.iterate(); while (try it.next()) |entry| { const mod_path = try std.fs.path.resolve(alloc, &.{ mods_path, entry.name }); @@ -170,21 +163,90 @@ pub fn main() !void { } } - std.log.debug("-" ** 80, .{}); - for (0..10) |x| { - std.log.debug("{d}...", .{x}); - std.time.sleep(std.time.ns_per_s); - } - std.log.debug("reloading.", .{}); - std.time.sleep(std.time.ns_per_s); + const fd: i32 = blk: { + const ret: isize = @bitCast(linux.inotify_init1(linux.IN.NONBLOCK)); + if (ret < 0) return error.inotify_init_error; + break :blk @intCast(ret); + }; + defer _ = linux.close(fd); - for (mods.items) |*mod| { - if (mod.reload(alloc)) |_| { - std.log.debug("loaded {s}", .{mod.curr.path}); - } else |_| { - std.log.debug("rolled back to {s}", .{mod.curr.path}); + const wd: i32 = blk: { + const pathz = try alloc.dupeZ(u8, mods_path); + defer alloc.free(pathz); + const ret: isize = @bitCast(linux.inotify_add_watch(fd, pathz, linux.IN.ALL_EVENTS)); + if (ret < 0) return error.inotify_add_watch_error; + break :blk @intCast(ret); + }; + defer _ = linux.inotify_rm_watch(fd, wd); + + var fds = [_]linux.pollfd{ + .{ .fd = fd, .events = linux.POLL.IN, .revents = undefined }, + }; + + const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX)); + defer alloc.free(eventbuf); + + for (0..100) |_| { + std.time.sleep(std.time.ns_per_s); + + const n = linux.poll(&fds, fds.len, std.time.ms_per_s); + std.log.debug("polled {d}", .{n}); + + if (n > 0) { + const c = linux.read(fd, eventbuf.ptr, eventbuf.len); + + var i: usize = 0; + while (i < c) { + const event: *linux.inotify_event = @alignCast(@ptrCast(eventbuf.ptr + i)); + + std.log.info("event: {any} {?s}", .{ event, event.getName() }); + + const names = .{ + "ACCESS", + "MODIFY", + "ATTRIB", + "CLOSE_WRITE", + "CLOSE_NOWRITE", + "OPEN", + "MOVED_FROM", + "MOVED_TO", + "CREATE", + "DELETE", + "DELETE_SELF", + "MOVE_SELF", + }; + + // Zig builds the lib in a temp file and then moves that into the destination. + // Think i need to handle "MOVE_TO" on the inode... but that might not work right because that inode + // is not what gets modified. + // Probably want a hashmap keyed by each mod's realpath basename. + + inline for (names) |name| { + if (@field(linux.IN, name) & event.mask != 0) { + std.log.debug(" {s}", .{name}); + } + } + + i += @sizeOf(linux.inotify_event) + event.len; + } } } + // std.log.debug("-" ** 80, .{}); + // for (0..10) |x| { + // std.log.debug("{d}...", .{x}); + // std.time.sleep(std.time.ns_per_s); + // } + // std.log.debug("reloading.", .{}); + // std.time.sleep(std.time.ns_per_s); + // + // for (mods.items) |*mod| { + // if (mod.reload(alloc)) |_| { + // std.log.debug("loaded {s}", .{mod.curr.path}); + // } else |_| { + // std.log.debug("rolled back to {s}", .{mod.curr.path}); + // } + // } + std.log.debug("-" ** 80, .{}); } From 466d614d75111b99998b3fc39781f9d5554279b6 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sun, 20 Jul 2025 21:10:25 -0400 Subject: [PATCH 3/6] inotify reloading works --- src/main.zig | 68 +++++++++++++++------------------------------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/src/main.zig b/src/main.zig index df0589a..a031efd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -136,9 +136,10 @@ pub fn main() !void { }; defer alloc.free(mods_path); - var mods: std.ArrayListUnmanaged(DynamicModule) = .{}; + var mods: std.StringHashMapUnmanaged(DynamicModule) = .{}; defer { - for (mods.items) |*mod| { + var it = mods.valueIterator(); + while (it.next()) |mod| { mod.deinit(alloc); } mods.deinit(alloc); @@ -159,7 +160,8 @@ pub fn main() !void { var mod = try DynamicModule.init(mod_path, alloc); errdefer mod.deinit(alloc); - try mods.append(alloc, mod); + + try mods.putNoClobber(alloc, entry.name, mod); } } @@ -173,7 +175,11 @@ pub fn main() !void { const wd: i32 = blk: { const pathz = try alloc.dupeZ(u8, mods_path); defer alloc.free(pathz); - const ret: isize = @bitCast(linux.inotify_add_watch(fd, pathz, linux.IN.ALL_EVENTS)); + const ret: isize = @bitCast(linux.inotify_add_watch( + fd, + pathz, + linux.IN.MOVED_TO, + )); if (ret < 0) return error.inotify_add_watch_error; break :blk @intCast(ret); }; @@ -186,11 +192,8 @@ pub fn main() !void { const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX)); defer alloc.free(eventbuf); - for (0..100) |_| { - std.time.sleep(std.time.ns_per_s); - + for (0..300) |_| { const n = linux.poll(&fds, fds.len, std.time.ms_per_s); - std.log.debug("polled {d}", .{n}); if (n > 0) { const c = linux.read(fd, eventbuf.ptr, eventbuf.len); @@ -198,33 +201,16 @@ pub fn main() !void { var i: usize = 0; while (i < c) { const event: *linux.inotify_event = @alignCast(@ptrCast(eventbuf.ptr + i)); + const name = event.getName().?; // Impossible as IN.MOVE_TO includes a name. - std.log.info("event: {any} {?s}", .{ event, event.getName() }); - - const names = .{ - "ACCESS", - "MODIFY", - "ATTRIB", - "CLOSE_WRITE", - "CLOSE_NOWRITE", - "OPEN", - "MOVED_FROM", - "MOVED_TO", - "CREATE", - "DELETE", - "DELETE_SELF", - "MOVE_SELF", - }; - - // Zig builds the lib in a temp file and then moves that into the destination. - // Think i need to handle "MOVE_TO" on the inode... but that might not work right because that inode - // is not what gets modified. - // Probably want a hashmap keyed by each mod's realpath basename. - - inline for (names) |name| { - if (@field(linux.IN, name) & event.mask != 0) { - std.log.debug(" {s}", .{name}); + if (mods.getPtr(name)) |mod| { + if (mod.reload(alloc)) |_| { + std.log.info("Reloaded {s}.", .{name}); + } else |_| { + std.log.err("Failed to reload {s}. Rolled back.", .{mod.realpath}); } + } else { + // Ignore unloaded module. } i += @sizeOf(linux.inotify_event) + event.len; @@ -232,21 +218,5 @@ pub fn main() !void { } } - // std.log.debug("-" ** 80, .{}); - // for (0..10) |x| { - // std.log.debug("{d}...", .{x}); - // std.time.sleep(std.time.ns_per_s); - // } - // std.log.debug("reloading.", .{}); - // std.time.sleep(std.time.ns_per_s); - // - // for (mods.items) |*mod| { - // if (mod.reload(alloc)) |_| { - // std.log.debug("loaded {s}", .{mod.curr.path}); - // } else |_| { - // std.log.debug("rolled back to {s}", .{mod.curr.path}); - // } - // } - std.log.debug("-" ** 80, .{}); } From cb27084693c252d818db18d9a2b1ab2fbb75062e Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 21 Jul 2025 08:37:32 -0400 Subject: [PATCH 4/6] threading tests --- src/foo.zig | 38 +++++++++++++++++++++++++++++++++----- src/main.zig | 2 +- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/foo.zig b/src/foo.zig index ca32e3b..44b373d 100644 --- a/src/foo.zig +++ b/src/foo.zig @@ -5,18 +5,46 @@ const Self = @This(); export const MODULE = core.module(Self); +_unload: std.Thread.ResetEvent = .{}, +looper: ?std.Thread = null, + pub fn startup(_: std.mem.Allocator) !Self { std.log.debug("!! startup {s} !!", .{@typeName(@This())}); return .{}; } -pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { - std.log.debug("!! unload {s} !!", .{@typeName(@This())}); - return &.{}; +pub fn reload(self: *Self, alloc: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); + self._unload.reset(); + self.looper = try std.Thread.spawn( + .{ .allocator = alloc }, + _loop, + .{self}, + ); + errdefer { + self._unload.set(); + self.looper.?.join(); + } } -pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { - std.log.debug("!! reload {s} !!", .{@typeName(@This())}); +fn _loop(self: *Self) void { + std.log.debug("START", .{}); + for (0..10) |i| { + std.log.debug("LOOP {d}", .{i}); + self._unload.timedWait(std.time.ns_per_s) catch continue; + std.log.debug("HALT", .{}); + break; + } +} + +pub fn unload(self: *Self, _: std.mem.Allocator) ![]u8 { + std.log.debug("!! unload {s} !!", .{@typeName(@This())}); + if (self.looper) |looper| { + self._unload.set(); + looper.join(); + self.looper = null; + } + return &.{}; } pub fn shutdown(_: *Self, _: std.mem.Allocator) void { diff --git a/src/main.zig b/src/main.zig index a031efd..8d908e9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -192,7 +192,7 @@ pub fn main() !void { const eventbuf = try alloc.alloc(u8, 5 * (@sizeOf(linux.inotify_event) + linux.NAME_MAX)); defer alloc.free(eventbuf); - for (0..300) |_| { + for (0..60 * 10) |_| { const n = linux.poll(&fds, fds.len, std.time.ms_per_s); if (n > 0) { From f010478e59f3258399597a345e413548c6234449 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Fri, 25 Jul 2025 17:17:17 -0400 Subject: [PATCH 5/6] unify "slot" construct --- src/bar.zig | 7 +- src/core.zig | 183 ++++++++++++++++++++------------------------------- src/foo.zig | 47 +++---------- src/main.zig | 20 +++--- 4 files changed, 97 insertions(+), 160 deletions(-) diff --git a/src/bar.zig b/src/bar.zig index ca32e3b..3fca266 100644 --- a/src/bar.zig +++ b/src/bar.zig @@ -5,9 +5,9 @@ const Self = @This(); export const MODULE = core.module(Self); -pub fn startup(_: std.mem.Allocator) !Self { +pub fn startup(alloc: std.mem.Allocator) !*Self { std.log.debug("!! startup {s} !!", .{@typeName(@This())}); - return .{}; + return try alloc.create(Self); } pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { @@ -19,6 +19,7 @@ pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { std.log.debug("!! reload {s} !!", .{@typeName(@This())}); } -pub fn shutdown(_: *Self, _: std.mem.Allocator) void { +pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); + alloc.destroy(self); } diff --git a/src/core.zig b/src/core.zig index 06dc8a0..e095305 100644 --- a/src/core.zig +++ b/src/core.zig @@ -38,129 +38,92 @@ const std = @import("std"); // Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the // module signal when it can safely be unloaded. -pub fn ModuleType(Self: type) type { +pub fn CFunc(Function: type) type { + const Arguments = std.meta.ArgsTuple(Function); + const Return = @typeInfo(Function).@"fn".return_type.?; + return fn (*const Arguments, *Return) callconv(.c) void; +} + +pub fn hook(impl: anytype) CFunc(@TypeOf(impl)) { + const Function = @TypeOf(impl); + const Arguments = std.meta.ArgsTuple(Function); + const Return = @typeInfo(Function).@"fn".return_type.?; + + return struct { + pub fn cimpl(args: *const Arguments, ret: *Return) callconv(.c) void { + ret.* = @call(.auto, impl, .{args.*}); + } + }.cimpl; +} + +// pub fn Func(PFunction: type) type { +// const CFunction = @typeInfo(PFunction).pointer.child; +// const F = @typeInfo(CFunction).@"fn"; +// const PArgs = F.params[0].type.?; +// const PRet = F.params[1].type.?; +// const Args = @typeInfo(PArgs).pointer.child; +// const Ret = @typeInfo(PRet).pointer.child; +// return fn (Args) Ret; +// } + +// pub fn slot(cimpl: anytype) Func(@TypeOf(cimpl)) { +// return struct{ +// pub fn impl(args: ) +// }.impl; +// } + +pub fn Slot(Function: type) type { + const Arguments = std.meta.ArgsTuple(Function); + const Return = @typeInfo(Function).@"fn".return_type.?; + return extern struct { - pub const Startup = fn (alloc: std.mem.Allocator) anyerror!Self; - pub const CStartup = fn (alloc: *const std.mem.Allocator) callconv(.c) ?*anyopaque; + ptr: *const fn (*const Arguments, *Return) callconv(.c) void, - pub const Unload = fn (self: *Self, alloc: std.mem.Allocator) anyerror![]u8; - pub const CUnload = fn (self: *Self, alloc: *const std.mem.Allocator, ptr: *[*]u8, len: *usize) callconv(.c) bool; - - pub const Reload = fn (self: *Self, alloc: std.mem.Allocator, data: []u8) anyerror!void; - pub const CReload = fn (self: *Self, alloc: *const std.mem.Allocator, ptr: [*]u8, len: usize) callconv(.c) bool; - - pub const Shutdown = fn (self: *Self, alloc: std.mem.Allocator) void; - pub const CShutdown = fn (self: *Self, alloc: *const std.mem.Allocator) callconv(.c) void; - - cstartup: *const CStartup, - creload: *const CReload, - cunload: *const CUnload, - cshutdown: *const CShutdown, - - pub fn startup(mod: @This(), alloc: std.mem.Allocator) !*Self { - return mod.cstartup(&alloc) orelse error.ModuleInitFailed; + pub fn wrap(impl: Function) @This() { + return .{ + .ptr = &struct { + pub fn cimpl(args: *const Arguments, ret: *Return) callconv(.c) void { + ret.* = @call(.auto, impl, args.*); + } + }.cimpl, + }; } - pub fn unload(mod: @This(), self: *Self, alloc: std.mem.Allocator) ![]u8 { - var data: []u8 = undefined; - if (mod.cunload(self, &alloc, &data.ptr, &data.len)) { - return data; - } else { - return error.ModuleUnloadFailed; - } - } - - pub fn reload(mod: @This(), self: *Self, alloc: std.mem.Allocator, data: []u8) !void { - if (mod.creload(self, &alloc, data.ptr, data.len)) {} else { - return error.ModuleReloadFailed; - } - } - - pub fn shutdown(mod: @This(), self: *Self, alloc: std.mem.Allocator) void { - mod.cshutdown(self, &alloc); + pub fn invoke(self: @This(), args: Arguments) Return { + var ret: Return = undefined; + self.ptr(&args, &ret); + return ret; } }; } -pub fn module(Self: type) ModuleType(Self) { - const Module = ModuleType(Self); +pub fn Module(State: type) type { + return extern struct { + pub const Error = std.mem.Allocator.Error; - const impl = struct { - pub const startup: Module.Startup = Self.startup; + pub const StartupError = Error; + pub const UnloadError = Error; + pub const ReloadError = Error; - pub fn cstartup( - alloc: *const std.mem.Allocator, - ) callconv(.c) ?*anyopaque { - const self: *Self = alloc.create(Self) catch |err| { - std.log.err( - "Module {s} startup failed: {!}", - .{ @typeName(Self), err }, - ); - return null; - }; - self.* = startup(alloc.*) catch |err| { - alloc.destroy(self); - std.log.err( - "Module {s} startup failed: {!}", - .{ @typeName(Self), err }, - ); - return null; - }; - return self; - } + const Startup = Slot(fn (std.mem.Allocator) StartupError!*State); + const Unload = Slot(fn (*State, std.mem.Allocator) UnloadError![]u8); + const Reload = Slot(fn (*State, std.mem.Allocator, []u8) ReloadError!void); + const Shutdown = Slot(fn (*State, std.mem.Allocator) void); - pub const unload: Module.Unload = Self.unload; - - pub fn cunload( - self: *Self, - alloc: *const std.mem.Allocator, - ptr: *[*]u8, - len: *usize, - ) callconv(.c) bool { - const data = unload(self, alloc.*) catch |err| { - std.log.err( - "Module {s} unload failed: {!}", - .{ @typeName(Self), err }, - ); - return false; - }; - ptr.* = data.ptr; - len.* = data.len; - return true; - } - - pub const reload: Module.Reload = Self.reload; - - pub fn creload( - self: *Self, - alloc: *const std.mem.Allocator, - ptr: [*]u8, - len: usize, - ) callconv(.c) bool { - reload(self, alloc.*, ptr[0..len]) catch |err| { - std.log.err( - "Module {s} reload failed: {!}", - .{ @typeName(Self), err }, - ); - return false; - }; - return true; - } - - pub const shutdown: Module.Shutdown = Self.shutdown; - - pub fn cshutdown( - self: *Self, - alloc: *const std.mem.Allocator, - ) callconv(.c) void { - shutdown(self, alloc.*); - } + startup: Startup, + unload: Unload, + reload: Reload, + shutdown: Shutdown, }; +} +pub fn module(State: type) Module(State) { return .{ - .cstartup = &impl.cstartup, - .creload = &impl.creload, - .cunload = &impl.cunload, - .cshutdown = &impl.cshutdown, + .startup = Module(State).Startup.wrap(State.startup), + .unload = Module(State).Unload.wrap(State.unload), + .reload = Module(State).Reload.wrap(State.reload), + .shutdown = Module(State).Shutdown.wrap(State.shutdown), }; } + +pub const AnyModule = Module(anyopaque); diff --git a/src/foo.zig b/src/foo.zig index 44b373d..3fca266 100644 --- a/src/foo.zig +++ b/src/foo.zig @@ -5,48 +5,21 @@ const Self = @This(); export const MODULE = core.module(Self); -_unload: std.Thread.ResetEvent = .{}, -looper: ?std.Thread = null, - -pub fn startup(_: std.mem.Allocator) !Self { +pub fn startup(alloc: std.mem.Allocator) !*Self { std.log.debug("!! startup {s} !!", .{@typeName(@This())}); - return .{}; + return try alloc.create(Self); } -pub fn reload(self: *Self, alloc: std.mem.Allocator, _: []u8) !void { - std.log.debug("!! reload {s} !!", .{@typeName(@This())}); - self._unload.reset(); - self.looper = try std.Thread.spawn( - .{ .allocator = alloc }, - _loop, - .{self}, - ); - errdefer { - self._unload.set(); - self.looper.?.join(); - } -} - -fn _loop(self: *Self) void { - std.log.debug("START", .{}); - for (0..10) |i| { - std.log.debug("LOOP {d}", .{i}); - self._unload.timedWait(std.time.ns_per_s) catch continue; - std.log.debug("HALT", .{}); - break; - } -} - -pub fn unload(self: *Self, _: std.mem.Allocator) ![]u8 { +pub fn unload(_: *Self, _: std.mem.Allocator) ![]u8 { std.log.debug("!! unload {s} !!", .{@typeName(@This())}); - if (self.looper) |looper| { - self._unload.set(); - looper.join(); - self.looper = null; - } return &.{}; } -pub fn shutdown(_: *Self, _: std.mem.Allocator) void { - std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); +pub fn reload(_: *Self, _: std.mem.Allocator, _: []u8) !void { + std.log.debug("!! reload {s} !!", .{@typeName(@This())}); +} + +pub fn shutdown(self: *Self, alloc: std.mem.Allocator) void { + std.log.debug("!! shutdown {s} !!", .{@typeName(@This())}); + alloc.destroy(self); } diff --git a/src/main.zig b/src/main.zig index 8d908e9..c492e83 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,7 +5,7 @@ const core = @import("core.zig"); const ModLib = struct { path: []const u8, lib: std.DynLib, - mod: *core.ModuleType(anyopaque), + mod: *core.Module(anyopaque), fn open(realpath: []const u8, version: u32, alloc: std.mem.Allocator) !ModLib { const basename = std.fs.path.basename(realpath); @@ -27,7 +27,7 @@ const ModLib = struct { var lib = try std.DynLib.open(path); errdefer lib.close(); - const mod = lib.lookup(*core.ModuleType(anyopaque), "MODULE") orelse + const mod = lib.lookup(*core.Module(anyopaque), "MODULE") orelse return error.MissingModuleDefinition; return .{ @@ -71,11 +71,11 @@ const DynamicModule = struct { self.curr = try ModLib.open(realpath, self.next, alloc); errdefer self.curr.close(alloc); - self.state = try self.curr.mod.startup(self.gpa.allocator()); - errdefer self.curr.mod.shutdown(self.state, self.gpa.allocator()); + self.state = try self.curr.mod.startup.invoke(.{self.gpa.allocator()}); + errdefer self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); // todo deserialize from disk. - try self.curr.mod.reload(self.state, self.gpa.allocator(), &.{}); + try self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), &.{} }); self.next += 1; @@ -86,11 +86,11 @@ const DynamicModule = struct { var next = try ModLib.open(self.realpath, self.next, alloc); errdefer next.close(alloc); - const data = try self.curr.mod.unload(self.state, self.gpa.allocator()); - errdefer self.curr.mod.reload(self.state, self.gpa.allocator(), data) catch + const data = try self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }); + errdefer self.curr.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }) catch std.debug.panic("Failed to rollback to {s}", .{self.curr.path}); - try next.mod.reload(self.state, self.gpa.allocator(), data); + try next.mod.reload.invoke(.{ self.state, self.gpa.allocator(), data }); self.curr.close(alloc); self.next += 1; @@ -98,12 +98,12 @@ const DynamicModule = struct { } pub fn deinit(self: *DynamicModule, alloc: std.mem.Allocator) void { - const data = self.curr.mod.unload(self.state, self.gpa.allocator()) catch + const data = self.curr.mod.unload.invoke(.{ self.state, self.gpa.allocator() }) catch std.debug.panic("Failed to unload {s}", .{self.curr.path}); _ = data; // todo serialize to disk. - self.curr.mod.shutdown(self.state, self.gpa.allocator()); + self.curr.mod.shutdown.invoke(.{ self.state, self.gpa.allocator() }); self.curr.close(alloc); From 5487901d10f5a7ed200b2fbfe0a5b5cd730ba3a9 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Fri, 25 Jul 2025 17:17:20 -0400 Subject: [PATCH 6/6] unify "slot" construct --- src/core.zig | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/core.zig b/src/core.zig index e095305..db19cca 100644 --- a/src/core.zig +++ b/src/core.zig @@ -38,40 +38,6 @@ const std = @import("std"); // Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the // module signal when it can safely be unloaded. -pub fn CFunc(Function: type) type { - const Arguments = std.meta.ArgsTuple(Function); - const Return = @typeInfo(Function).@"fn".return_type.?; - return fn (*const Arguments, *Return) callconv(.c) void; -} - -pub fn hook(impl: anytype) CFunc(@TypeOf(impl)) { - const Function = @TypeOf(impl); - const Arguments = std.meta.ArgsTuple(Function); - const Return = @typeInfo(Function).@"fn".return_type.?; - - return struct { - pub fn cimpl(args: *const Arguments, ret: *Return) callconv(.c) void { - ret.* = @call(.auto, impl, .{args.*}); - } - }.cimpl; -} - -// pub fn Func(PFunction: type) type { -// const CFunction = @typeInfo(PFunction).pointer.child; -// const F = @typeInfo(CFunction).@"fn"; -// const PArgs = F.params[0].type.?; -// const PRet = F.params[1].type.?; -// const Args = @typeInfo(PArgs).pointer.child; -// const Ret = @typeInfo(PRet).pointer.child; -// return fn (Args) Ret; -// } - -// pub fn slot(cimpl: anytype) Func(@TypeOf(cimpl)) { -// return struct{ -// pub fn impl(args: ) -// }.impl; -// } - pub fn Slot(Function: type) type { const Arguments = std.meta.ArgsTuple(Function); const Return = @typeInfo(Function).@"fn".return_type.?;