module: add event arguments & dependency injection support

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-15 15:26:28 -07:00 committed by Stephen Gutekanst
parent e4e834e054
commit e25281cc64
2 changed files with 283 additions and 37 deletions

View file

@ -8,7 +8,8 @@ const EntityID = @import("entities.zig").EntityID;
const comp = @import("comptime.zig"); const comp = @import("comptime.zig");
pub fn World(comptime mods: anytype) type { pub fn World(comptime mods: anytype) type {
const modules = mach.Modules(mods); const Injectable = struct {}; // TODO
const modules = mach.Modules(mods, Injectable);
return struct { return struct {
allocator: mem.Allocator, allocator: mem.Allocator,

View file

@ -23,8 +23,8 @@ fn Serializable(comptime T: type) type {
return T; return T;
} }
// Manages comptime .{A, B, C} modules and runtime modules. /// Manages comptime .{A, B, C} modules and runtime modules.
pub fn Modules(comptime mods: anytype) type { pub fn Modules(comptime mods: anytype, comptime Injectable: type) type {
// Verify that each module is valid. // Verify that each module is valid.
inline for (mods) |M| _ = Module(M); inline for (mods) |M| _ = Module(M);
@ -67,31 +67,84 @@ pub fn Modules(comptime mods: anytype) type {
m.events.deinit(); m.events.deinit();
} }
// Send a global event /// Returns an args tuple representing the standard, uninjected, arguments which the given
pub fn send(m: *@This(), event_name: EventName(mods), args: anytype) void { /// local event handler requires.
fn LocalArgs(module_name: ModuleName(mods), event_name: EventName(mods)) type {
const M = @field(NamespacedModules(@This().modules){}, @tagName(module_name));
const handler = @field(M.local, @tagName(event_name));
switch (@typeInfo(@TypeOf(handler))) {
.Fn => return UninjectedArgsTuple(@TypeOf(handler), Injectable),
// Note: This means the module does have some other field by the same name, but it is not a function.
else => @compileError("Module " ++ @tagName(M.name) ++ " has no global event handler " ++ @tagName(event_name)),
}
}
/// Returns an args tuple representing the standard, uninjected, arguments which the given
/// global event handler requires.
///
/// If the returned type would differ from EventArgs, a compile-time error will occur.
///
/// If no module currently has a global event handler of this name, then its argument type
/// is currently undefined and assumed to be EventArgs.
fn Args(comptime EventArgs: type, event_name: EventName(mods)) type {
inline for (modules) |M| {
if (@hasDecl(M, @tagName(event_name))) {
switch (@typeInfo(@TypeOf(@field(M, @tagName(event_name))))) {
.Fn => {
const handler = @field(M, @tagName(event_name));
// TODO: worth checking if the return type is == EventArgs here? Could
// that lead to better UX?
return UninjectedArgsTuple(@TypeOf(handler), Injectable);
},
else => {},
}
}
}
return EventArgs;
}
/// Send a global event
pub fn send(
m: *@This(),
// TODO: is a variant of this function where event_name is not comptime known, but asserted to be a valid enum, useful?
comptime event_name: EventName(mods),
comptime EventArgs: type,
args: Args(EventArgs, event_name),
) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
m.sendInternal(null, @intFromEnum(event_name), args); m.sendInternal(null, @intFromEnum(event_name), args);
} }
// Send an event to a specific module /// Send an event to a specific module
pub fn sendToModule(m: *@This(), module_name: ModuleName(mods), event_name: EventName(mods), args: anytype) void { pub fn sendToModule(
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?
comptime module_name: ModuleName(mods),
comptime event_name: EventName(mods),
args: LocalArgs(module_name, event_name),
) void {
// TODO: comptime safety/debugging // TODO: comptime safety/debugging
m.sendInternal(@intFromEnum(module_name), @intFromEnum(event_name), args); m.sendInternal(@intFromEnum(module_name), @intFromEnum(event_name), 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.
pub fn sendDynamic(m: *@This(), event_name: EventID, args: anytype) void { pub fn sendDynamic(m: *@This(), event_name: EventID, args: anytype) void {
// TODO: runtime safety/debugging // TODO: runtime safety/debugging
// TODO: check args do not have obviously wrong things, like comptime values
// TODO: if module_name and event_name are valid enums, can we type-check args at comptime?
m.sendInternal(null, event_name, args); m.sendInternal(null, event_name, args);
} }
// Send an event to a specific module, using a dynamic (not known to the compiled program) module and event name. /// Send an event to a specific module, using a dynamic (not known to the compiled program) module and event name.
pub fn sendToModuleDynamic(m: *@This(), module_name: ModuleID, event_name: EventID, args: anytype) void { pub fn sendToModuleDynamic(m: *@This(), module_name: ModuleID, event_name: EventID, args: anytype) void {
// TODO: runtime safety/debugging // TODO: runtime safety/debugging
// TODO: check args do not have obviously wrong things, like comptime values
// TODO: if module_name and event_name are valid enums, can we type-check args at comptime?
m.sendInternal(module_name, event_name, args); m.sendInternal(module_name, event_name, args);
} }
fn sendInternal(m: *@This(), module_name: ?ModuleID, event_name: EventID, args: anytype) void { fn sendInternal(m: *@This(), module_name: ?ModuleID, event_name: EventID, args: anytype) void {
// TODO: verify arguments are valid, e.g. not comptime types
_ = Serializable(@TypeOf(args)); _ = Serializable(@TypeOf(args));
// TODO: debugging // TODO: debugging
@ -108,7 +161,10 @@ pub fn Modules(comptime mods: anytype) type {
}); });
} }
pub fn dispatch(m: *@This()) void { /// Dispatches pending events, invoking their event handlers.
pub fn dispatch(m: *@This(), injectable: Injectable) void {
// TODO: verify injectable arguments are valid, e.g. not comptime types
// TODO: optimize to reduce send contention // TODO: optimize to reduce send contention
// TODO: parallel / multi-threaded dispatch // TODO: parallel / multi-threaded dispatch
// TODO: PGO // TODO: PGO
@ -116,20 +172,19 @@ pub fn Modules(comptime mods: anytype) type {
defer m.events_mu.unlock(); defer m.events_mu.unlock();
while (m.events.readItem()) |ev| { while (m.events.readItem()) |ev| {
_ = ev.args_slice; // TODO: dispatch arguments
if (ev.module_name) |module_name| { if (ev.module_name) |module_name| {
// TODO: dispatch arguments // TODO: dispatch arguments
@This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), .{}); @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable);
} else { } else {
// TODO: dispatch arguments // TODO: dispatch arguments
@This().call(@enumFromInt(ev.event_name), .{}); @This().call(@enumFromInt(ev.event_name), ev.args_slice, injectable);
} }
} }
m.args_queue.clearRetainingCapacity(); m.args_queue.clearRetainingCapacity();
} }
// Call global event handler with the specified name in all modules /// Call global event handler with the specified name in all modules
inline fn call(event_name: EventName(mods), args: anytype) void { inline fn call(event_name: EventName(mods), args: []u8, injectable: anytype) void {
switch (event_name) { switch (event_name) {
inline else => |name| { inline else => |name| {
inline for (modules) |M| { inline for (modules) |M| {
@ -137,7 +192,7 @@ pub fn Modules(comptime mods: anytype) type {
switch (@typeInfo(@TypeOf(@field(M, @tagName(name))))) { switch (@typeInfo(@TypeOf(@field(M, @tagName(name))))) {
.Fn => { .Fn => {
const handler = @field(M, @tagName(name)); const handler = @field(M, @tagName(name));
callHandler(handler, args); callHandler(handler, args, injectable);
}, },
else => {}, else => {},
} }
@ -147,18 +202,20 @@ pub fn Modules(comptime mods: anytype) type {
} }
} }
// Call local event handler with the specified name in the specified module /// Call local event handler with the specified name in the specified module
inline fn callLocal(module_name: ModuleName(mods), event_name: EventName(mods), args: anytype) void { inline fn callLocal(module_name: ModuleName(mods), event_name: EventName(mods), args: []u8, injectable: anytype) void {
// TODO: invert switch case for hypothetically better branch prediction
switch (module_name) { switch (module_name) {
inline else => |mod_name| { inline else => |mod_name| {
switch (event_name) { switch (event_name) {
inline else => |ev_name| { inline else => |ev_name| {
const M = @field(NamespacedModules(@This().modules){}, @tagName(mod_name)); const M = @field(NamespacedModules(@This().modules){}, @tagName(mod_name));
// TODO: no need for hasDecl, assertion should be event can be sent at send() time.
if (@hasDecl(M.local, @tagName(ev_name))) { if (@hasDecl(M.local, @tagName(ev_name))) {
const handler = @field(M.local, @tagName(ev_name)); const handler = @field(M.local, @tagName(ev_name));
switch (@typeInfo(@TypeOf(handler))) { switch (@typeInfo(@TypeOf(handler))) {
.Fn => { .Fn => {
callHandler(handler, args); callHandler(handler, args, injectable);
}, },
else => {}, else => {},
} }
@ -169,12 +226,75 @@ pub fn Modules(comptime mods: anytype) type {
} }
} }
inline fn callHandler(handler: anytype, args: anytype) void { /// Invokes an event handler with optionally injected arguments.
inline fn callHandler(handler: anytype, args_data: []u8, injectable: Injectable) void {
const StdArgs = UninjectedArgsTuple(@TypeOf(handler), Injectable);
const std_args: *StdArgs = @alignCast(@ptrCast(args_data.ptr));
const args = injectArgs(@TypeOf(handler), Injectable, injectable, std_args.*);
@call(.auto, handler, args); @call(.auto, handler, args);
} }
}; };
} }
// Given a function, its standard arguments and injectable arguments, performs injection and
// returns the actual argument tuple which would be used to call the function.
inline fn injectArgs(
comptime Function: type,
comptime Injectable: type,
injectable_args: Injectable,
std_args: UninjectedArgsTuple(Function, Injectable),
) std.meta.ArgsTuple(Function) {
var args: std.meta.ArgsTuple(Function) = undefined;
comptime var std_args_index = 0;
outer: inline for (@typeInfo(std.meta.ArgsTuple(Function)).Struct.fields) |arg| {
// Injected arguments always go first, then standard (non-injected) arguments.
if (std_args_index > 0) {
@field(args, arg.name) = std_args[std_args_index];
std_args_index += 1;
continue;
}
// Is this argument matching the type of an argument we could inject?
inline for (@typeInfo(Injectable).Struct.fields) |inject_field| {
if (inject_field.type == arg.type and @alignOf(inject_field.type) == @alignOf(arg.type)) {
// Inject argument
@field(args, arg.name) = @field(injectable_args, inject_field.name);
continue :outer;
}
}
// First standard argument
@field(args, arg.name) = std_args[std_args_index];
std_args_index += 1;
}
return args;
}
// Given a function type, and an args tuple of injectable parameters, returns the set of function
// parameters which would **not** be injected.
fn UninjectedArgsTuple(comptime Function: type, comptime Injectable: type) type {
var std_args: []const type = &[0]type{};
inline for (@typeInfo(std.meta.ArgsTuple(Function)).Struct.fields) |arg| {
// Injected arguments always go first, then standard (non-injected) arguments.
if (std_args.len > 0) {
std_args = std_args ++ [_]type{arg.type};
continue;
}
// Is this argument matching the type of an argument we could inject?
const injectable = blk: {
inline for (@typeInfo(Injectable).Struct.fields) |inject| {
if (inject.type == arg.type and @alignOf(inject.type) == arg.alignment) {
break :blk true;
}
}
break :blk false;
};
if (injectable) continue; // legitimate injected argument, ignore it
std_args = std_args ++ [_]type{arg.type};
}
return std.meta.Tuple(std_args);
}
/// enum describing every possible comptime-known event name /// enum describing every possible comptime-known event name
fn EventName(comptime mods: anytype) type { fn EventName(comptime mods: anytype) type {
var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{}; var enum_fields: []const std.builtin.Type.EnumField = &[0]std.builtin.Type.EnumField{};
@ -383,11 +503,12 @@ test Modules {
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
}); });
const Injectable = struct {};
var modules: Modules(.{ var modules: Modules(.{
Physics, Physics,
Renderer, Renderer,
Sprite2D, Sprite2D,
}) = undefined; }, Injectable) = undefined;
try modules.init(testing.allocator); try modules.init(testing.allocator);
defer modules.deinit(testing.allocator); defer modules.deinit(testing.allocator);
testing.refAllDeclsRecursive(Physics); testing.refAllDeclsRecursive(Physics);
@ -434,11 +555,12 @@ test EventName {
}; };
}); });
const Injectable = struct {};
const Mods = Modules(.{ const Mods = Modules(.{
Physics, Physics,
Renderer, Renderer,
Sprite2D, Sprite2D,
}); }, Injectable);
const info = @typeInfo(EventName(Mods.modules)).Enum; const info = @typeInfo(EventName(Mods.modules)).Enum;
try testing.expect(type, u3).eql(info.tag_type); try testing.expect(type, u3).eql(info.tag_type);
@ -461,11 +583,12 @@ test ModuleName {
const Sprite2D = Module(struct { const Sprite2D = Module(struct {
pub const name = .engine_sprite2d; pub const name = .engine_sprite2d;
}); });
const Injectable = struct {};
const Mods = Modules(.{ const Mods = Modules(.{
Physics, Physics,
Renderer, Renderer,
Sprite2D, Sprite2D,
}); }, Injectable);
const info = @typeInfo(ModuleName(Mods.modules)).Enum; const info = @typeInfo(ModuleName(Mods.modules)).Enum;
try testing.expect(type, u2).eql(info.tag_type); try testing.expect(type, u2).eql(info.tag_type);
@ -475,6 +598,106 @@ test ModuleName {
try testing.expect([]const u8, "engine_sprite2d").eql(info.fields[2].name); try testing.expect([]const u8, "engine_sprite2d").eql(info.fields[2].name);
} }
const TupleTester = struct {
fn assertTypeEqual(comptime Expected: type, comptime Actual: type) void {
if (Expected != Actual) @compileError("Expected type " ++ @typeName(Expected) ++ ", but got type " ++ @typeName(Actual));
}
fn assertTuple(comptime expected: anytype, comptime Actual: type) void {
const info = @typeInfo(Actual);
if (info != .Struct) @compileError("Expected struct type");
if (!info.Struct.is_tuple) @compileError("Struct type must be a tuple type");
const fields_list = std.meta.fields(Actual);
if (expected.len != fields_list.len) @compileError("Argument count mismatch");
inline for (fields_list, 0..) |fld, i| {
if (expected[i] != fld.type) {
@compileError("Field " ++ fld.name ++ " expected to be type " ++ @typeName(expected[i]) ++ ", but was type " ++ @typeName(fld.type));
}
}
}
};
test injectArgs {
// Injected arguments should generally be *struct types to avoid conflicts with any user-passed
// parameters, though we do not require it - so we test with other types here.
var i: i32 = 1234;
const i32_ptr: *i32 = &i;
var f: f32 = 0.1234;
const f32_ptr: *f32 = &f;
const Foo = struct { foo: f32 };
var foo: Foo = .{ .foo = 1234 };
const foo_ptr: *Foo = &foo;
// No standard, no injected
try testing.expect(struct {}, .{}).eql(injectArgs(fn () void, @TypeOf(.{}), .{}, .{}));
const injectable = .{ i32_ptr, f32_ptr, foo_ptr };
try testing.expect(struct {}, .{}).eql(injectArgs(fn () void, @TypeOf(injectable), injectable, .{}));
// Standard parameters only, no injected
try testing.expect(std.meta.Tuple(&.{i32}), .{0}).eql(injectArgs(fn (a: i32) void, @TypeOf(injectable), injectable, .{0}));
try testing.expect(std.meta.Tuple(&.{ i32, f32 }), .{ 1, 0.5 }).eql(injectArgs(fn (a: i32, b: f32) void, @TypeOf(injectable), injectable, .{ 1, 0.5 }));
// Injected parameters only, no standard
try testing.expect(std.meta.Tuple(&.{*i32}), .{i32_ptr}).eql(injectArgs(fn (a: *i32) void, @TypeOf(injectable), injectable, .{}));
try testing.expect(std.meta.Tuple(&.{*f32}), .{f32_ptr}).eql(injectArgs(fn (a: *f32) void, @TypeOf(injectable), injectable, .{}));
try testing.expect(std.meta.Tuple(&.{*Foo}), .{foo_ptr}).eql(injectArgs(fn (a: *Foo) void, @TypeOf(injectable), injectable, .{}));
try testing.expect(std.meta.Tuple(&.{ *i32, *f32, *Foo }), .{ i32_ptr, f32_ptr, foo_ptr }).eql(injectArgs(fn (a: *i32, b: *f32, c: *Foo) void, @TypeOf(injectable), injectable, .{}));
// Once a standard parameter is encountered, all parameters after that are considered standard
// and not injected.
var my_f32: f32 = 0.1337;
var my_i32: i32 = 1337;
try testing.expect(std.meta.Tuple(&.{f32}), .{1234}).eql(injectArgs(fn (a: f32) void, @TypeOf(injectable), injectable, .{1234}));
try testing.expect(std.meta.Tuple(&.{ i32, *f32 }), .{ 1234, &my_f32 }).eql(injectArgs(fn (a: i32, b: *f32) void, @TypeOf(injectable), injectable, .{ 1234, &my_f32 }));
try testing.expect(std.meta.Tuple(&.{ i32, *i32, *f32 }), .{ 1234, &my_i32, &my_f32 }).eql(injectArgs(fn (a: i32, b: *i32, c: *f32) void, @TypeOf(injectable), injectable, .{ 1234, &my_i32, &my_f32 }));
// First parameter (*f32) matches an injectable parameter type, so it is injected.
try testing.expect(std.meta.Tuple(&.{ *f32, i32, *i32, *f32 }), .{ f32_ptr, 1234, &my_i32, &my_f32 }).eql(injectArgs(fn (a: *f32, b: i32, c: *i32, d: *f32) void, @TypeOf(injectable), injectable, .{ 1234, &my_i32, &my_f32 }));
// First parameter (*f32) matches an injectable parameter type, so it is injected. 2nd
// parameter is not injectable, so all remaining parameters are not injected.
var my_foo = foo;
try testing.expect(std.meta.Tuple(&.{ *f32, i32, *Foo, *i32, *f32 }), .{ f32_ptr, 1234, &my_foo, &my_i32, &my_f32 }).eql(injectArgs(fn (a: *f32, b: i32, c: *Foo, d: *i32, e: *f32) void, @TypeOf(injectable), injectable, .{ 1234, &my_foo, &my_i32, &my_f32 }));
}
test UninjectedArgsTuple {
// Injected arguments should generally be *struct types to avoid conflicts with any user-passed
// parameters, though we do not require it - so we test with other types here.
const i32_ptr: *i32 = undefined;
const f32_ptr: *f32 = undefined;
const Foo = struct { foo: f32 };
const foo_ptr: *Foo = undefined;
// No standard, no injected
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn () void, @TypeOf(.{})));
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn () void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
// Standard parameters only, no injected
TupleTester.assertTuple(.{i32}, UninjectedArgsTuple(fn (a: i32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{ i32, f32 }, UninjectedArgsTuple(fn (a: i32, b: f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
// Injected parameters only, no standard
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn (a: *i32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn (a: *f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn (a: *Foo) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn (a: *f32, b: *Foo, c: *i32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
// Once a standard parameter is encountered, all parameters after that are considered standard
// and not injected.
TupleTester.assertTuple(.{f32}, UninjectedArgsTuple(fn (a: f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{ i32, *f32 }, UninjectedArgsTuple(fn (a: i32, b: *f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
TupleTester.assertTuple(.{ i32, *i32, *f32 }, UninjectedArgsTuple(fn (a: i32, b: *i32, c: *f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
// First parameter (*f32) matches an injectable parameter type, so it is injected.
TupleTester.assertTuple(.{ i32, *i32, *f32 }, UninjectedArgsTuple(fn (a: *f32, b: i32, c: *i32, d: *f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
// First parameter (*f32) matches an injectable parameter type, so it is injected. 2nd
// parameter is not injectable, so all remaining parameters are not injected.
TupleTester.assertTuple(.{ i32, *Foo, *i32, *f32 }, UninjectedArgsTuple(fn (a: *f32, b: i32, c: *Foo, d: *i32, e: *f32) void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr })));
}
test "event name calling" { test "event name calling" {
// TODO: verify that event handlers error return signatures are correct // TODO: verify that event handlers error return signatures are correct
const global = struct { const global = struct {
@ -516,14 +739,15 @@ test "event name calling" {
}; };
}); });
const Injectable = struct {};
var modules: Modules(.{ var modules: Modules(.{
Physics, Physics,
Renderer, Renderer,
}) = undefined; }, Injectable) = undefined;
try modules.init(testing.allocator); try modules.init(testing.allocator);
defer modules.deinit(testing.allocator); defer modules.deinit(testing.allocator);
@TypeOf(modules).call(.tick, .{}); @TypeOf(modules).call(.tick, &.{}, .{});
try testing.expect(usize, 2).eql(global.ticks); try testing.expect(usize, 2).eql(global.ticks);
// Check we can use .call() with a runtime-known event name. // Check we can use .call() with a runtime-known event name.
@ -533,13 +757,13 @@ test "event name calling" {
alloc.* = @intFromEnum(@as(E, .tick)); alloc.* = @intFromEnum(@as(E, .tick));
var event_name = @as(E, @enumFromInt(alloc.*)); var event_name = @as(E, @enumFromInt(alloc.*));
@TypeOf(modules).call(event_name, .{}); @TypeOf(modules).call(event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
// Check call() behavior with a valid event name enum, but not a valid global event handler name // Check call() behavior with a valid event name enum, but not a valid global event handler name
alloc.* = @intFromEnum(@as(E, .update)); alloc.* = @intFromEnum(@as(E, .update));
event_name = @as(E, @enumFromInt(alloc.*)); event_name = @as(E, @enumFromInt(alloc.*));
@TypeOf(modules).call(event_name, .{}); @TypeOf(modules).call(event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 0).eql(global.physics_updates); try testing.expect(usize, 0).eql(global.physics_updates);
try testing.expect(usize, 0).eql(global.renderer_updates); try testing.expect(usize, 0).eql(global.renderer_updates);
@ -551,8 +775,8 @@ test "event name calling" {
m_alloc.* = @intFromEnum(@as(M, .engine_renderer)); m_alloc.* = @intFromEnum(@as(M, .engine_renderer));
alloc.* = @intFromEnum(@as(E, .update)); alloc.* = @intFromEnum(@as(E, .update));
var module_name = @as(M, @enumFromInt(m_alloc.*)); var module_name = @as(M, @enumFromInt(m_alloc.*));
@TypeOf(modules).callLocal(module_name, event_name, .{}); @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{});
@TypeOf(modules).callLocal(module_name, event_name, .{}); @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 0).eql(global.physics_updates); try testing.expect(usize, 0).eql(global.physics_updates);
try testing.expect(usize, 2).eql(global.renderer_updates); try testing.expect(usize, 2).eql(global.renderer_updates);
@ -561,14 +785,14 @@ test "event name calling" {
alloc.* = @intFromEnum(@as(E, .update)); alloc.* = @intFromEnum(@as(E, .update));
module_name = @as(M, @enumFromInt(m_alloc.*)); module_name = @as(M, @enumFromInt(m_alloc.*));
event_name = @as(E, @enumFromInt(alloc.*)); event_name = @as(E, @enumFromInt(alloc.*));
@TypeOf(modules).callLocal(module_name, event_name, .{}); @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{});
try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_updates);
m_alloc.* = @intFromEnum(@as(M, .engine_physics)); m_alloc.* = @intFromEnum(@as(M, .engine_physics));
alloc.* = @intFromEnum(@as(E, .calc)); alloc.* = @intFromEnum(@as(E, .calc));
module_name = @as(M, @enumFromInt(m_alloc.*)); module_name = @as(M, @enumFromInt(m_alloc.*));
event_name = @as(E, @enumFromInt(alloc.*)); event_name = @as(E, @enumFromInt(alloc.*));
@TypeOf(modules).callLocal(module_name, event_name, .{}); @TypeOf(modules).callLocal(module_name, event_name, &.{}, .{});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
try testing.expect(usize, 1).eql(global.physics_calc); try testing.expect(usize, 1).eql(global.physics_calc);
try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_updates);
@ -581,7 +805,11 @@ test "dispatch" {
var physics_updates: usize = 0; var physics_updates: usize = 0;
var physics_calc: usize = 0; var physics_calc: usize = 0;
var renderer_updates: usize = 0; var renderer_updates: usize = 0;
var basic_args_sum: usize = 0;
}; };
var foo = struct {
injected_args_sum: usize = 0,
}{};
const Physics = Module(struct { const Physics = Module(struct {
pub const name = .engine_physics; pub const name = .engine_physics;
pub const components = struct {}; pub const components = struct {};
@ -612,13 +840,22 @@ test "dispatch" {
pub fn update() void { pub fn update() void {
global.renderer_updates += 1; global.renderer_updates += 1;
} }
pub fn basicArgs(a: u32, b: u32) void {
global.basic_args_sum = a + b;
}
pub fn injectedArgs(foo_ptr: *@TypeOf(foo), a: u32, b: u32) void {
foo_ptr.*.injected_args_sum = a + b;
}
}; };
}); });
const injectable = .{&foo};
var modules: Modules(.{ var modules: Modules(.{
Physics, Physics,
Renderer, Renderer,
}) = undefined; }, @TypeOf(injectable)) = undefined;
try modules.init(testing.allocator); try modules.init(testing.allocator);
defer modules.deinit(testing.allocator); defer modules.deinit(testing.allocator);
@ -626,17 +863,18 @@ test "dispatch" {
const M = ModuleName(@TypeOf(modules).modules); const M = ModuleName(@TypeOf(modules).modules);
// Global events // Global events
modules.send(.tick, .{}); modules.send(.tick, struct {}, .{});
try testing.expect(usize, 0).eql(global.ticks); try testing.expect(usize, 0).eql(global.ticks);
modules.dispatch(); modules.dispatch(.{&foo});
try testing.expect(usize, 2).eql(global.ticks); try testing.expect(usize, 2).eql(global.ticks);
// TODO: make sendDynamic take an args type to avoid footguns with comptime values, etc.
modules.sendDynamic(@intFromEnum(@as(E, .tick)), .{}); modules.sendDynamic(@intFromEnum(@as(E, .tick)), .{});
modules.dispatch(); modules.dispatch(.{&foo});
try testing.expect(usize, 4).eql(global.ticks); try testing.expect(usize, 4).eql(global.ticks);
// Local events // Local events
modules.sendToModule(.engine_renderer, .update, .{}); modules.sendToModule(.engine_renderer, .update, .{});
modules.dispatch(); modules.dispatch(.{&foo});
try testing.expect(usize, 1).eql(global.renderer_updates); try testing.expect(usize, 1).eql(global.renderer_updates);
modules.sendToModule(.engine_physics, .update, .{}); modules.sendToModule(.engine_physics, .update, .{});
modules.sendToModuleDynamic( modules.sendToModuleDynamic(
@ -644,7 +882,14 @@ test "dispatch" {
@intFromEnum(@as(E, .calc)), @intFromEnum(@as(E, .calc)),
.{}, .{},
); );
modules.dispatch(); modules.dispatch(.{&foo});
try testing.expect(usize, 1).eql(global.physics_updates); try testing.expect(usize, 1).eql(global.physics_updates);
try testing.expect(usize, 1).eql(global.physics_calc); try testing.expect(usize, 1).eql(global.physics_calc);
// Local events
modules.sendToModule(.engine_renderer, .basicArgs, .{ @as(u32, 1), @as(u32, 2) }); // TODO: match arguments against fn ArgsTuple, for correctness and type inference
modules.sendToModule(.engine_renderer, .injectedArgs, .{ @as(u32, 1), @as(u32, 2) });
modules.dispatch(.{&foo});
try testing.expect(usize, 3).eql(global.basic_args_sum);
try testing.expect(usize, 3).eql(foo.injected_args_sum);
} }