96 lines
3.5 KiB
Zig
96 lines
3.5 KiB
Zig
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 Slot(Function: type) type {
|
|
const Arguments = std.meta.ArgsTuple(Function);
|
|
const Return = @typeInfo(Function).@"fn".return_type.?;
|
|
|
|
return extern struct {
|
|
ptr: *const fn (*const Arguments, *Return) callconv(.c) void,
|
|
|
|
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 invoke(self: @This(), args: Arguments) Return {
|
|
var ret: Return = undefined;
|
|
self.ptr(&args, &ret);
|
|
return ret;
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn Module(State: type) type {
|
|
return extern struct {
|
|
pub const Error = std.mem.Allocator.Error;
|
|
|
|
pub const StartupError = Error;
|
|
pub const UnloadError = Error;
|
|
pub const ReloadError = Error;
|
|
|
|
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);
|
|
|
|
startup: Startup,
|
|
unload: Unload,
|
|
reload: Reload,
|
|
shutdown: Shutdown,
|
|
};
|
|
}
|
|
|
|
pub fn module(State: type) Module(State) {
|
|
return .{
|
|
.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);
|