module: enable Module to analyze event handler signatures

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-04-05 05:01:58 -07:00 committed by Stephen Gutekanst
parent ce0b764a6d
commit b3663f7899
4 changed files with 148 additions and 107 deletions

View file

@ -53,7 +53,7 @@ test "example" {
.tick = .{ .handler = tick }, .tick = .{ .handler = tick },
}; };
fn tick(physics: *Modules(modules).Mod(Physics)) void { fn tick(physics: *mach.ModSet(modules).Mod(Physics)) void {
_ = physics; _ = physics;
} }
}; };
@ -68,8 +68,8 @@ test "example" {
}; };
fn tick( fn tick(
physics: *Modules(modules).Mod(Physics), physics: *mach.ModSet(modules).Mod(Physics),
renderer: *Modules(modules).Mod(Renderer), renderer: *mach.ModSet(modules).Mod(Renderer),
) void { ) void {
_ = renderer; _ = renderer;
_ = physics; _ = physics;

View file

@ -9,6 +9,7 @@ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator(); const allocator = gpa.allocator();
/// The main Mach engine ECS module. /// The main Mach engine ECS module.
// TODO: move this to Engine.zig
pub const Engine = struct { pub const Engine = struct {
device: *gpu.Device, device: *gpu.Device,
queue: *gpu.Queue, queue: *gpu.Queue,
@ -17,7 +18,7 @@ pub const Engine = struct {
encoder: *gpu.CommandEncoder, encoder: *gpu.CommandEncoder,
pub const name = .engine; pub const name = .engine;
pub const Mod = Modules.Mod(@This()); pub const Mod = mach.Mod(@This());
pub const global_events = .{ pub const global_events = .{
.init = .{ .handler = fn () void }, .init = .{ .handler = fn () void },
@ -102,7 +103,7 @@ pub const Engine = struct {
}; };
pub const App = struct { pub const App = struct {
modules: Modules, modules: mach.Modules,
pub fn init(app: *@This()) !void { pub fn init(app: *@This()) !void {
app.* = .{ .modules = undefined }; app.* = .{ .modules = undefined };
@ -127,10 +128,3 @@ pub const App = struct {
return app.modules.mod.engine.state.should_exit; return app.modules.mod.engine.state.should_exit;
} }
}; };
pub const Modules = module.Modules(blk: {
if (!@hasDecl(@import("root"), "modules")) {
@compileError("expected `pub const modules = .{};` in root file");
}
break :blk @import("root").modules;
});

View file

@ -21,8 +21,17 @@ pub const sysgpu = if (build_options.want_sysgpu) @import("sysgpu/main.zig") els
// Engine exports // Engine exports
pub const App = @import("engine.zig").App; pub const App = @import("engine.zig").App;
pub const Engine = @import("engine.zig").Engine; pub const Engine = @import("engine.zig").Engine;
pub const Modules = @import("engine.zig").Modules; pub const ModSet = @import("module.zig").ModSet;
pub const Mod = Modules.Mod;
// TODO: perhaps this could be a comptime var rather than @import("root")?
const modules = blk: {
if (!@hasDecl(@import("root"), "modules")) {
@compileError("expected `pub const modules = .{};` in root file");
}
break :blk @import("root").modules;
};
pub const Modules = @import("module.zig").Modules(modules);
pub const Mod = ModSet(modules).Mod;
test { test {
const std = @import("std"); const std = @import("std");

View file

@ -5,15 +5,15 @@ const testing = @import("testing.zig");
const Entities = @import("ecs/entities.zig").Entities; const Entities = @import("ecs/entities.zig").Entities;
const EntityID = @import("ecs/entities.zig").EntityID; const EntityID = @import("ecs/entities.zig").EntityID;
// TODO: make sendToModule the default name for sending events? and sendGlobal always secondary? Or vice-versa?
/// Verifies that M matches the basic layout of a Mach module /// Verifies that M matches the basic layout of a Mach module
fn ModuleInterface(comptime M: type) type { fn ModuleInterface(comptime M: type) type {
if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M)); if (@typeInfo(M) != .Struct) @compileError("mach: expected module struct, found: " ++ @typeName(M));
if (!@hasDecl(M, "name")) @compileError("mach: module must have `pub const name = .foobar;`"); if (!@hasDecl(M, "name")) @compileError("mach: module must have `pub const name = .foobar;`");
if (@typeInfo(@TypeOf(M.name)) != .EnumLiteral) @compileError("mach: module must have `pub const name = .foobar;`, found type:" ++ @typeName(M.name)); if (@typeInfo(@TypeOf(M.name)) != .EnumLiteral) @compileError("mach: module must have `pub const name = .foobar;`, found type:" ++ @typeName(M.name));
if (@hasDecl(M, "global_events")) validateEvents("mach: module ." ++ @tagName(M.name) ++ " global_events ", M.global_events);
// TODO: enable once parameter dependency loop has been resolved if (@hasDecl(M, "local_events")) validateEvents("mach: module ." ++ @tagName(M.name) ++ " local_events ", M.global_events);
// if (@hasDecl(M, "global_events")) validateEvents("mach: module ." ++ @tagName(M.name) ++ " global_events ", M.global_events);
// if (@hasDecl(M, "local_events")) validateEvents("mach: module ." ++ @tagName(M.name) ++ " local_events ", M.global_events);
_ = ComponentTypesM(M); _ = ComponentTypesM(M);
return M; return M;
} }
@ -47,7 +47,6 @@ pub fn Modules(comptime modules2: anytype) type {
/// e.g. @field(@field(ComponentTypesByName, "module_name"), "component_name") /// e.g. @field(@field(ComponentTypesByName, "module_name"), "component_name")
pub const component_types_by_name = ComponentTypesByName(modules){}; pub const component_types_by_name = ComponentTypesByName(modules){};
const ModulesT = @This();
const Event = struct { const Event = struct {
module_name: ?ModuleID, module_name: ?ModuleID,
event_name: EventID, event_name: EventID,
@ -58,15 +57,10 @@ pub fn Modules(comptime modules2: anytype) type {
events_mu: std.Thread.RwLock = .{}, events_mu: std.Thread.RwLock = .{},
args_queue: std.ArrayListUnmanaged(u8) = .{}, args_queue: std.ArrayListUnmanaged(u8) = .{},
events: EventQueue, events: EventQueue,
mod: ModsByName(modules, ModulesT), mod: ModsByName(modules),
// TODO: pass mods directly instead of ComponentTypesByName? // TODO: pass mods directly instead of ComponentTypesByName?
entities: Entities(component_types_by_name), entities: Entities(component_types_by_name),
pub fn Mod(comptime M: type) type {
const NSComponents = ComponentTypesByName(ModulesT.modules);
return Module(M, ModulesT, NSComponents);
}
pub fn init(m: *@This(), allocator: std.mem.Allocator) !void { pub fn init(m: *@This(), allocator: std.mem.Allocator) !void {
// TODO: switch Entities to stack allocation like Modules is // TODO: switch Entities to stack allocation like Modules is
var entities = try Entities(component_types_by_name).init(allocator); var entities = try Entities(component_types_by_name).init(allocator);
@ -110,16 +104,36 @@ pub fn Modules(comptime modules2: anytype) type {
} }
} }
fn moduleToGlobalEvent(
comptime M: type,
comptime EventEnumM: anytype,
comptime EventEnum: anytype,
comptime event_name: EventEnumM(M),
) EventEnum(modules) {
for (@typeInfo(EventEnum(modules)).Enum.fields) |gfield| {
if (std.mem.eql(u8, @tagName(event_name), gfield.name)) {
return @enumFromInt(gfield.value);
}
}
unreachable;
}
/// Send a global event which the specified module defines /// Send a global event which the specified module defines
pub fn sendGlobal( pub fn sendGlobal(
m: *@This(), m: *@This(),
// TODO: is a variant of this function where event_name is not comptime known, but asserted to be a valid enum, useful? // TODO: is a variant of this function where event_name is not comptime known, but asserted to be a valid enum, useful?
comptime module_name: ModuleName(modules), comptime module_name: ModuleName(modules),
comptime event_name: GlobalEvent, comptime event_name: GlobalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).state)),
args: GlobalArgs(module_name, event_name), args: GlobalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).state), event_name),
) void { ) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
m.sendInternal(null, @intFromEnum(event_name), args); const event_name_g: GlobalEvent = comptime moduleToGlobalEvent(
@TypeOf(@field(m.mod, @tagName(module_name)).state),
GlobalEventEnumM,
GlobalEventEnum,
event_name,
);
m.sendInternal(null, @intFromEnum(event_name_g), args);
} }
/// Send an event to a specific module /// Send an event to a specific module
@ -127,11 +141,17 @@ pub fn Modules(comptime modules2: anytype) type {
m: *@This(), m: *@This(),
// TODO: is a variant of this function where module_name/event_name is not comptime known, but asserted to be a valid enum, useful? // TODO: is a variant of this function where module_name/event_name is not comptime known, but asserted to be a valid enum, useful?
comptime module_name: ModuleName(modules), comptime module_name: ModuleName(modules),
comptime event_name: LocalEvent, comptime event_name: LocalEventEnumM(@TypeOf(@field(m.mod, @tagName(module_name)).state)),
args: LocalArgs(module_name, event_name), args: LocalArgsM(@TypeOf(@field(m.mod, @tagName(module_name)).state), event_name),
) void { ) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
m.sendInternal(@intFromEnum(module_name), @intFromEnum(event_name), args); const event_name_g: LocalEvent = comptime moduleToGlobalEvent(
@TypeOf(@field(m.mod, @tagName(module_name)).state),
LocalEventEnumM,
LocalEventEnum,
event_name,
);
m.sendInternal(@intFromEnum(module_name), @intFromEnum(event_name_g), args);
} }
/// Send a global event, using a dynamic (not known to the compiled program) event name. /// Send a global event, using a dynamic (not known to the compiled program) event name.
@ -171,7 +191,7 @@ pub fn Modules(comptime modules2: anytype) type {
pub fn dispatch(m: *@This()) !void { pub fn dispatch(m: *@This()) !void {
const Injectable = comptime blk: { const Injectable = comptime blk: {
var types: []const type = &[0]type{}; var types: []const type = &[0]type{};
for (@typeInfo(ModsByName(modules, ModulesT)).Struct.fields) |field| { for (@typeInfo(ModsByName(modules)).Struct.fields) |field| {
const ModPtr = @TypeOf(@as(*field.type, undefined)); const ModPtr = @TypeOf(@as(*field.type, undefined));
types = types ++ [_]type{ModPtr}; types = types ++ [_]type{ModPtr};
} }
@ -179,7 +199,7 @@ pub fn Modules(comptime modules2: anytype) type {
}; };
var injectable: Injectable = undefined; var injectable: Injectable = undefined;
outer: inline for (@typeInfo(Injectable).Struct.fields) |field| { outer: inline for (@typeInfo(Injectable).Struct.fields) |field| {
inline for (@typeInfo(ModsByName(modules, ModulesT)).Struct.fields) |injectable_field| { inline for (@typeInfo(ModsByName(modules)).Struct.fields) |injectable_field| {
if (*injectable_field.type == field.type) { if (*injectable_field.type == field.type) {
@field(injectable, field.name) = &@field(m.mod, injectable_field.name); @field(injectable, field.name) = &@field(m.mod, injectable_field.name);
@ -291,17 +311,16 @@ pub fn Modules(comptime modules2: anytype) type {
}; };
} }
pub fn ModsByName(comptime modules: anytype, comptime ModulesT: type) type { pub fn ModsByName(comptime modules: anytype) type {
var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{}; var fields: []const std.builtin.Type.StructField = &[0]std.builtin.Type.StructField{};
for (modules) |M| { for (modules) |M| {
const NSComponents = ComponentTypesByName(modules); const ModT = ModSet(modules).Mod(M);
const Mod = Module(M, ModulesT, NSComponents);
fields = fields ++ [_]std.builtin.Type.StructField{.{ fields = fields ++ [_]std.builtin.Type.StructField{.{
.name = @tagName(M.name), .name = @tagName(M.name),
.type = Mod, .type = ModT,
.default_value = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = @alignOf(Mod), .alignment = @alignOf(ModT),
}}; }};
} }
return @Type(.{ return @Type(.{
@ -314,11 +333,25 @@ pub fn ModsByName(comptime modules: anytype, comptime ModulesT: type) type {
}); });
} }
pub fn Module( // Note: Modules() causes analysis of event handlers' function signatures, whose parameters include
comptime M: anytype, // references to ModSet(modules).Mod(). As a result, the type returned here may never invoke Modules()
comptime ModulesT: type, // or depend on its result. However, it can analyze the global set of modules on its own, since no
comptime NSComponents: type, // module's type should embed the result of Modules().
) type { //
// In short, these calls are fine:
//
// Modules() -> ModSet()
// Modules() -> ModSet() -> Mod()
//
// But these are never permissible:
//
// ModSet() -> Modules()
// Mod() -> Modules()
//
pub fn ModSet(comptime modules: anytype) type {
const NSComponents = ComponentTypesByName(modules);
return struct {
pub fn Mod(comptime M: anytype) type {
const module_tag = M.name; const module_tag = M.name;
const components = ComponentTypesM(M){}; const components = ComponentTypesM(M){};
return struct { return struct {
@ -373,26 +406,31 @@ pub fn Module(
try m.entities.removeComponent(entity, module_tag, component_name); try m.entities.removeComponent(entity, module_tag, component_name);
} }
pub inline fn send(m: *@This(), comptime event_name: ModulesT.LocalEvent, args: LocalArgsM(M, event_name)) void { pub inline fn send(m: *@This(), comptime event_name: LocalEventEnumM(M), args: LocalArgsM(M, event_name)) void {
const MByName = ModsByName(ModulesT.modules, ModulesT); const ModulesT = Modules(modules);
const MByName = ModsByName(ModulesT.modules);
const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m));
const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr);
modules.sendToModule(module_tag, event_name, args); mods.sendToModule(module_tag, event_name, args);
} }
pub inline fn sendGlobal(m: *@This(), comptime event_name: ModulesT.GlobalEvent, args: GlobalArgsM(M, event_name)) void { pub inline fn sendGlobal(m: *@This(), comptime event_name: GlobalEventEnumM(M), args: GlobalArgsM(M, event_name)) void {
const MByName = ModsByName(ModulesT.modules, ModulesT); const ModulesT = Modules(modules);
const MByName = ModsByName(ModulesT.modules);
const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m));
const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr);
modules.sendGlobal(module_tag, event_name, args); mods.sendGlobal(module_tag, event_name, args);
} }
// TODO: eliminate this // TODO: eliminate this
pub fn dispatchNoError(m: *@This()) void { pub fn dispatchNoError(m: *@This()) void {
const MByName = ModsByName(ModulesT.modules, ModulesT); const ModulesT = Modules(modules);
const MByName = ModsByName(ModulesT.modules);
const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m)); const mod_ptr: *MByName = @alignCast(@fieldParentPtr(MByName, @tagName(module_tag), m));
const modules = @fieldParentPtr(ModulesT, "mod", mod_ptr); const mods = @fieldParentPtr(ModulesT, "mod", mod_ptr);
modules.dispatch() catch |err| @panic(@errorName(err)); mods.dispatch() catch |err| @panic(@errorName(err));
}
};
} }
}; };
} }