From 3898995c4c0d4a412b2ac104574e3656f0ebd32a Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Tue, 19 Mar 2024 21:32:36 -0700 Subject: [PATCH] all: get ECS running on revised module system All ECS `examples/` now run on the revised module system. Signed-off-by: Stephen Gutekanst --- examples/glyphs/Game.zig | 3 + examples/text/Game.zig | 84 +++++++++-------- src/ecs/main.zig | 10 +- src/ecs/systems.zig | 192 +++++++++++++++++---------------------- src/engine.zig | 39 ++++---- src/module.zig | 140 +++++++++++++++++++--------- 6 files changed, 249 insertions(+), 219 deletions(-) diff --git a/examples/glyphs/Game.zig b/examples/glyphs/Game.zig index 6d7d8326..024283c5 100644 --- a/examples/glyphs/Game.zig +++ b/examples/glyphs/Game.zig @@ -50,6 +50,7 @@ pub fn init( sprite_mod: *Sprite.Mod, text_mod: *Text.Mod, game: *Mod, + world: *mach.World, ) !void { // The Mach .core is where we set window options, etc. core.setTitle("gfx.Sprite example"); @@ -59,10 +60,12 @@ pub fn init( // Tell sprite_mod to use the texture sprite_mod.send(.init, .{}); + world.dispatchNoError(); // TODO: no dispatch in user code sprite_mod.send(.initPipeline, .{Sprite.PipelineOptions{ .pipeline = @intFromEnum(Pipeline.text), .texture = text_mod.state.texture, }}); + world.dispatchNoError(); // TODO: no dispatch in user code // We can create entities, and set components on them. Note that components live in a module // namespace, e.g. the `Sprite` module could have a 3D `.location` component with a different diff --git a/examples/text/Game.zig b/examples/text/Game.zig index 19d10010..c1f07832 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -49,10 +49,51 @@ pub const Pipeline = enum(u32) { const upscale = 1.0; +const style1 = Text.Style{ + .font_name = "Roboto Medium", // TODO + .font_size = 48 * gfx.px_per_pt, // 48pt + .font_weight = gfx.font_weight_normal, + .italic = false, + .color = vec4(0.6, 1.0, 0.6, 1.0), +}; +const style2 = blk: { + var v = style1; + v.italic = true; + break :blk v; +}; +const style3 = blk: { + var v = style1; + v.font_weight = gfx.font_weight_bold; + break :blk v; +}; + +const segment1: []const @import("mach").gfx.Text.Segment = &.{ + .{ + .string = "Text but with spaces 😊\nand\n", + .style = &style1, + }, + .{ + .string = "italics\nand\n", + .style = &style2, + }, + .{ + .string = "bold\nand\n", + .style = &style3, + }, +}; + +const segment2: []const @import("mach").gfx.Text.Segment = &.{ + .{ + .string = "!$?😊", + .style = &style1, + }, +}; + pub fn init( engine: *mach.Engine.Mod, text_mod: *Text.Mod, game: *Mod, + world: *mach.World, ) !void { // The Mach .core is where we set window options, etc. core.setTitle("gfx.Text example"); @@ -61,37 +102,13 @@ pub fn init( const player = try engine.newEntity(); try text_mod.set(player, .pipeline, @intFromEnum(Pipeline.default)); try text_mod.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0)))); - const style1 = Text.Style{ - .font_name = "Roboto Medium", // TODO - .font_size = 48 * gfx.px_per_pt, // 48pt - .font_weight = gfx.font_weight_normal, - .italic = false, - .color = vec4(0.6, 1.0, 0.6, 1.0), - }; - var style2 = style1; - style2.italic = true; - var style3 = style1; - style3.font_weight = gfx.font_weight_bold; - try text_mod.set(player, .text, &.{ - .{ - .string = "Text but with spaces 😊\nand\n", - .style = &style1, - }, - .{ - .string = "italics\nand\n", - .style = &style2, - }, - .{ - .string = "bold\nand\n", - .style = &style3, - }, - }); + try text_mod.set(player, .text, segment1); text_mod.send(.init, .{}); text_mod.send(.initPipeline, .{Text.PipelineOptions{ .pipeline = @intFromEnum(Pipeline.default), }}); - text_mod.send(.updated, .{@intFromEnum(Pipeline.default)}); + world.dispatchNoError(); // TODO: no dispatch in user code game.state = .{ .timer = try mach.Timer.start(), @@ -160,20 +177,7 @@ pub fn tick( const new_entity = try engine.newEntity(); try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default)); try text_mod.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos))); - - const style1 = Text.Style{ - .font_name = "Roboto Medium", // TODO - .font_size = 48 * gfx.px_per_pt, // 48pt - .font_weight = gfx.font_weight_normal, - .italic = false, - .color = vec4(0.6, 1.0, 0.6, 1.0), - }; - try text_mod.set(new_entity, .text, &.{ - .{ - .string = "!$?😊", - .style = &style1, - }, - }); + try text_mod.set(new_entity, .text, segment2); game.state.texts += 1; } diff --git a/src/ecs/main.zig b/src/ecs/main.zig index 198d13ec..2089cecb 100644 --- a/src/ecs/main.zig +++ b/src/ecs/main.zig @@ -50,7 +50,7 @@ test "example" { pub const id = u32; }; - pub fn tick(physics: *World(.{ Renderer, Physics }).Mod(Physics)) !void { + pub fn tick(physics: *World(.{ Renderer, Physics }).Mod(Physics)) void { _ = physics; } }); @@ -64,7 +64,7 @@ test "example" { pub fn tick( physics: *World(.{ Renderer, Physics }).Mod(Physics), renderer: *World(.{ Renderer, Physics }).Mod(Renderer), - ) !void { + ) void { _ = renderer; _ = physics; } @@ -72,7 +72,8 @@ test "example" { //------------------------------------------------------------------------- // Create a world. - var world = try World(.{ Renderer, Physics }).init(allocator); + var world: World(.{ Renderer, Physics }) = undefined; + try world.init(allocator); defer world.deinit(); // Initialize module state. @@ -113,5 +114,6 @@ test "example" { //------------------------------------------------------------------------- // Send events to modules - try world.send(null, .tick, .{}); + world.modules.send(.tick, .{}); + try world.dispatch(); } diff --git a/src/ecs/systems.zig b/src/ecs/systems.zig index b380edc7..74e7f07a 100644 --- a/src/ecs/systems.zig +++ b/src/ecs/systems.zig @@ -8,25 +8,30 @@ const EntityID = @import("entities.zig").EntityID; const comp = @import("comptime.zig"); pub fn World(comptime mods: anytype) type { - const Injectable = struct {}; // TODO - const modules = mach.Modules(mods, Injectable); - + const StateT = NamespacedState(mods); + const ns_components = NamespacedComponents(mods){}; return struct { allocator: mem.Allocator, entities: Entities(NamespacedComponents(mods){}), - mod: Mods(), + modules: Modules, + mod: Mods, - const Self = @This(); + const Modules = mach.Modules(mods); + pub const IsInjectedArgument = void; + + const WorldT = @This(); pub fn Mod(comptime Module: anytype) type { const module_tag = Module.name; - const State = @TypeOf(@field(@as(NamespacedState(mods), undefined), @tagName(module_tag))); - const components = @field(NamespacedComponents(mods){}, @tagName(module_tag)); + const State = @TypeOf(@field(@as(StateT, undefined), @tagName(module_tag))); + const components = @field(ns_components, @tagName(module_tag)); return struct { state: State, - entities: *Entities(NamespacedComponents(mods){}), + entities: *Entities(ns_components), allocator: mem.Allocator, + pub const IsInjectedArgument = void; + /// Sets the named component to the specified value for the given entity, /// moving the entity from it's current archetype table to the new archetype /// table if required. @@ -36,8 +41,8 @@ pub fn World(comptime mods: anytype) type { comptime component_name: std.meta.DeclEnum(components), component: @field(components, @tagName(component_name)), ) !void { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); try world.entities.setComponent(entity, module_tag, component_name, component); } @@ -48,8 +53,8 @@ pub fn World(comptime mods: anytype) type { entity: EntityID, comptime component_name: std.meta.DeclEnum(components), ) ?@field(components, @tagName(component_name)) { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); return world.entities.getComponent(entity, module_tag, component_name); } @@ -59,36 +64,36 @@ pub fn World(comptime mods: anytype) type { entity: EntityID, comptime component_name: std.meta.DeclEnum(components), ) !void { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); try world.entities.removeComponent(entity, module_tag, component_name); } - pub fn send(m: *@This(), comptime msg_tag: anytype, args: anytype) !void { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); - return world.sendStr(module_tag, @tagName(msg_tag), args); + pub inline fn send(m: *@This(), comptime event_name: anytype, args: anytype) void { + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); + world.modules.sendToModule(module_tag, event_name, args); } /// Returns a new entity. pub fn newEntity(m: *@This()) !EntityID { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); return world.entities.new(); } /// Removes an entity. pub fn removeEntity(m: *@This(), entity: EntityID) !void { - const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m)); - const world = @fieldParentPtr(Self, "mod", mod_ptr); + const mod_ptr: *Mods = @alignCast(@fieldParentPtr(Mods, @tagName(module_tag), m)); + const world = @fieldParentPtr(WorldT, "mod", mod_ptr); try world.entities.removeEntity(entity); } }; } - fn Mods() type { + pub const Mods = blk: { var fields: []const StructField = &[0]StructField{}; - inline for (modules.modules) |M| { + for (mods) |M| { fields = fields ++ [_]std.builtin.Type.StructField{.{ .name = @tagName(M.name), .type = Mod(M), @@ -97,7 +102,7 @@ pub fn World(comptime mods: anytype) type { .alignment = @alignOf(Mod(M)), }}; } - return @Type(.{ + break :blk @Type(.{ .Struct = .{ .layout = .Auto, .is_tuple = false, @@ -105,97 +110,64 @@ pub fn World(comptime mods: anytype) type { .decls = &[_]std.builtin.Type.Declaration{}, }, }); - } + }; - pub fn init(allocator: mem.Allocator) !Self { - return Self{ - .allocator = allocator, - .entities = try Entities(NamespacedComponents(mods){}).init(allocator), - .mod = undefined, - }; - } + const Injectable = blk: { + var types: []const type = &[0]type{}; + types = types ++ [_]type{*@This()}; + for (@typeInfo(Mods).Struct.fields) |field| { + const ModPtr = @TypeOf(@as(*field.type, undefined)); + types = types ++ [_]type{ModPtr}; + } + break :blk std.meta.Tuple(types); + }; + fn injectable(world: *@This()) Injectable { + var v: Injectable = undefined; + outer: inline for (@typeInfo(Injectable).Struct.fields) |field| { + if (field.type == *@This()) { + @field(v, field.name) = world; + continue :outer; + } else { + inline for (@typeInfo(Mods).Struct.fields) |injectable_field| { + if (*injectable_field.type == field.type) { + @field(v, field.name) = &@field(world.mod, injectable_field.name); - pub fn deinit(world: *Self) void { - world.entities.deinit(); - } - - /// Broadcasts an event to all modules that are subscribed to it. - /// - /// The message tag corresponds with the handler method name to be invoked. For example, - /// if `send(.tick)` is invoked, all modules which declare a `pub fn tick` will be invoked. - /// - /// Events sent by Mach itself, or the application itself, may be single words. To prevent - /// name conflicts, events sent by modules provided by a library should prefix their events - /// with their module name. For example, a module named `.ziglibs_imgui` should use event - /// names like `.ziglibsImguiClick`, `.ziglibsImguiFoobar`. - pub fn send(world: *Self, comptime optional_module_tag: anytype, comptime msg_tag: anytype, args: anytype) !void { - return world.sendStr(optional_module_tag, @tagName(msg_tag), args); - } - - pub fn sendStr(world: *Self, comptime optional_module_tag: anytype, comptime msg: anytype, args: anytype) !void { - // Check for any module that has a handler function named msg (e.g. `fn init` would match "init") - inline for (modules.modules) |M| { - const EventHandlers = blk: { - switch (@typeInfo(@TypeOf(optional_module_tag))) { - .Null => break :blk M, - .EnumLiteral => { - // Send this message only to the specified module - if (M.name != optional_module_tag) continue; - if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg); - if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg); - break :blk M.local; - }, - .Optional => if (optional_module_tag) |v| { - // Send this message only to the specified module - if (M.name != v) continue; - if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg); - if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg); - break :blk M.local; - }, - else => @panic("unexpected optional_module_tag type: " ++ @typeName(@TypeOf(optional_module_tag))), - } - }; - if (!@hasDecl(EventHandlers, msg)) continue; - - // Determine which parameters the handler function wants. e.g.: - // - // pub fn init(eng: *mach.Engine) !void - // pub fn init(eng: *mach.Engine, mach: *mach.Engine.Mod) !void - // - const handler = @field(EventHandlers, msg); - - // Build a tuple of parameters that we can pass to the function, based on what - // *mach.Mod(T) types it expects as arguments. - var params: std.meta.ArgsTuple(@TypeOf(handler)) = undefined; - comptime var argIndex = 0; - inline for (@typeInfo(@TypeOf(params)).Struct.fields) |param| { - comptime var found = false; - inline for (@typeInfo(Mods()).Struct.fields) |f| { - if (param.type == *f.type) { - // TODO: better initialization place for modules - @field(@field(world.mod, f.name), "entities") = &world.entities; - @field(@field(world.mod, f.name), "allocator") = world.allocator; - - @field(params, param.name) = &@field(world.mod, f.name); - found = true; - break; - } else if (param.type == *Self) { - @field(params, param.name) = world; - found = true; - break; - } else if (param.type == f.type) { - @compileError("Module handler " ++ @tagName(M.name) ++ "." ++ msg ++ " should be *T not T: " ++ @typeName(param.type)); + // TODO: better module initialization location + @field(v, field.name).entities = &world.entities; + @field(v, field.name).allocator = world.allocator; + continue :outer; } } - if (!found) { - @field(params, param.name) = args[argIndex]; - argIndex += 1; - } } - - // Invoke the handler - try @call(.auto, handler, params); + @compileError("failed to initialize Injectable field (this is a bug): " ++ field.name ++ " " ++ @typeName(field.type)); } + return v; + } + + pub fn dispatch(world: *@This()) !void { + try world.modules.dispatch(world.injectable()); + } + + pub fn dispatchNoError(world: *@This()) void { + world.modules.dispatch(world.injectable()) catch |err| @panic(@errorName(err)); + } + + pub fn init(world: *@This(), allocator: mem.Allocator) !void { + // TODO: switch Entities to stack allocation like Modules and World + var entities = try Entities(ns_components).init(allocator); + errdefer entities.deinit(); + world.* = @This(){ + .allocator = allocator, + .entities = entities, + .modules = undefined, + .mod = undefined, + }; + try world.modules.init(allocator); + } + + pub fn deinit(world: *@This()) void { + world.entities.deinit(); + world.modules.deinit(world.allocator); } }; } diff --git a/src/engine.zig b/src/engine.zig index 847ddb6a..b55f0be8 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -18,6 +18,8 @@ pub const Engine = struct { pub const name = .engine; pub const Mod = World.Mod(@This()); + pub const exit = fn () void; + pub const local = struct { pub fn init(world: *World) !void { core.allocator = allocator; @@ -30,32 +32,26 @@ pub const Engine = struct { .label = "engine.state.encoder", }); - try world.send(null, .init, .{}); + world.modules.send(.init, .{}); } - pub fn deinit( - world: *World, - engine: *Mod, - ) !void { + pub fn deinit(world: *World, engine: *Mod) void { // TODO: this triggers a device loss error, which we should handle correctly // engine.state.device.release(); engine.state.queue.release(); - try world.send(null, .deinit, .{}); + world.modules.send(.deinit, .{}); core.deinit(); world.deinit(); _ = gpa.deinit(); } // Engine module's exit handler - pub fn exit(world: *World) !void { - try world.send(null, .exit, .{}); + pub fn exit(world: *World) void { + world.modules.send(.exit, .{}); world.mod.engine.state.exit = true; } - pub fn beginPass( - engine: *Mod, - clear_color: gpu.Color, - ) !void { + pub fn beginPass(engine: *Mod, clear_color: gpu.Color) void { const back_buffer_view = core.swap_chain.getCurrentTextureView().?; defer back_buffer_view.release(); @@ -73,9 +69,7 @@ pub const Engine = struct { engine.state.pass = engine.state.encoder.beginRenderPass(&pass_info); } - pub fn endPass( - engine: *Mod, - ) !void { + pub fn endPass(engine: *Mod) void { // End this pass engine.state.pass.end(); engine.state.pass.release(); @@ -91,7 +85,7 @@ pub const Engine = struct { }); } - pub fn present() !void { + pub fn present() void { core.swap_chain.present(); } }; @@ -101,16 +95,21 @@ pub const App = struct { world: World, pub fn init(app: *@This()) !void { - app.* = .{ .world = try World.init(allocator) }; - try app.world.send(.engine, .init, .{}); + app.* = .{ .world = undefined }; + try app.world.init(allocator); + app.world.modules.sendToModule(.engine, .init, .{}); + try app.world.dispatch(); } pub fn deinit(app: *@This()) void { - try app.world.send(.engine, .deinit, .{}); + app.world.modules.sendToModule(.engine, .deinit, .{}); } pub fn update(app: *@This()) !bool { - try app.world.send(null, .tick, .{}); + // TODO: better dispatch implementation + app.world.modules.send(.tick, .{}); + try app.world.dispatch(); // dispatch .tick + try app.world.dispatch(); // dispatch any events produced by .tick return app.world.mod.engine.state.exit; } }; diff --git a/src/module.zig b/src/module.zig index 3eaddbb6..6c8d2b62 100644 --- a/src/module.zig +++ b/src/module.zig @@ -23,7 +23,7 @@ fn Serializable(comptime T: type) type { } /// Manages comptime .{A, B, C} modules and runtime modules. -pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { +pub fn Modules(comptime mods: anytype) type { // Verify that each module is valid. inline for (mods) |M| _ = Module(M); @@ -66,12 +66,20 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { /// Returns an args tuple representing the standard, uninjected, arguments which the given /// 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)), + inline for (modules) |M| { + if (M.name != module_name) continue; + if (!@hasDecl(M, "local")) @compileError("Module " ++ @tagName(module_name) ++ " has no `pub const local = struct { ... };` event handlers"); + if (!@hasDecl(M.local, @tagName(event_name))) @compileError("Module " ++ @tagName(module_name) ++ ".local has no event handler named: " ++ @tagName(event_name)); + const handler = @field(M.local, @tagName(event_name)); + switch (@typeInfo(@TypeOf(handler))) { + // TODO: passing std.meta.Tuple here instead of TupleHACK results in a compiler + // segfault. The only difference is that TupleHACk does not produce a real tuple, + // `@Type(.{.Struct = .{ .is_tuple = false }})` instead of `.is_tuple = true`. + .Fn => return UninjectedArgsTuple(TupleHACK, @TypeOf(handler)), + // Note: This means the module does have some other field by the same name, but it is not a function. + // TODO: allow pre-declarations + else => @compileError("Module " ++ @tagName(module_name) ++ ".local." ++ @tagName(event_name) ++ " is not a function"), + } } } @@ -89,7 +97,7 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { }, else => continue, }; - return UninjectedArgsTuple(Handler, Injectable); + return UninjectedArgsTuple(std.meta.Tuple, Handler); } } @compileError("No global event handler " ++ @tagName(event_name) ++ " is defined in any module."); @@ -144,7 +152,6 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { const args_bytes = std.mem.asBytes(&args); m.args_queue.appendSliceAssumeCapacity(args_bytes); - m.events.writeItemAssumeCapacity(.{ .module_name = module_name, .event_name = event_name, @@ -153,19 +160,28 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { } /// Dispatches pending events, invoking their event handlers. - pub fn dispatch(m: *@This(), injectable: Injectable) !void { + pub fn dispatch(m: *@This(), injectable: anytype) !void { // TODO: verify injectable arguments are valid, e.g. not comptime types // TODO: optimize to reduce send contention // TODO: parallel / multi-threaded dispatch // TODO: PGO - m.events_mu.lock(); - defer m.events_mu.unlock(); // TODO: this is wrong - defer m.args_queue.clearRetainingCapacity(); + defer { + m.events_mu.lock(); + m.args_queue.clearRetainingCapacity(); + m.events_mu.unlock(); + } + + while (true) { + m.events_mu.lock(); + const ev = m.events.readItem() orelse { + m.events_mu.unlock(); + break; + }; + m.events_mu.unlock(); - while (m.events.readItem()) |ev| { if (ev.module_name) |module_name| { // TODO: dispatch arguments try @This().callLocal(@enumFromInt(module_name), @enumFromInt(ev.event_name), ev.args_slice, injectable); @@ -222,7 +238,7 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { /// Invokes an event handler with optionally injected arguments. inline fn callHandler(handler: anytype, args_data: []u8, injectable: anytype) !void { const Handler = @TypeOf(handler); - const StdArgs = UninjectedArgsTuple(Handler, @TypeOf(injectable)); + const StdArgs = UninjectedArgsTuple(std.meta.Tuple, Handler); const std_args: *StdArgs = @alignCast(@ptrCast(args_data.ptr)); const args = injectArgs(Handler, @TypeOf(injectable), injectable, std_args.*); const Ret = @typeInfo(Handler).Fn.return_type orelse void; @@ -234,13 +250,43 @@ pub fn Modules(comptime mods: anytype, comptime Injectable: type) type { }; } +// TODO: see usage location +fn TupleHACK(comptime types: []const type) type { + return CreateUniqueTupleHACK(types.len, types[0..types.len].*); +} + +fn CreateUniqueTupleHACK(comptime N: comptime_int, comptime types: [N]type) type { + var tuple_fields: [types.len]std.builtin.Type.StructField = undefined; + inline for (types, 0..) |T, i| { + @setEvalBranchQuota(10_000); + var num_buf: [128]u8 = undefined; + tuple_fields[i] = .{ + .name = std.fmt.bufPrintZ(&num_buf, "{d}", .{i}) catch unreachable, + .type = T, + .default_value = null, + .is_comptime = false, + .alignment = if (@sizeOf(T) > 0) @alignOf(T) else 0, + }; + } + + return @Type(.{ + .Struct = .{ + // .is_tuple = true, + .is_tuple = false, + .layout = .Auto, + .decls = &.{}, + .fields = &tuple_fields, + }, + }); +} + // 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_args: UninjectedArgsTuple(std.meta.Tuple, Function), ) std.meta.ArgsTuple(Function) { var args: std.meta.ArgsTuple(Function) = undefined; comptime var std_args_index = 0; @@ -270,7 +316,10 @@ inline fn injectArgs( // 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 { +fn UninjectedArgsTuple( + comptime Tuple: fn (comptime types: []const type) type, + comptime Function: 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. @@ -278,19 +327,22 @@ fn UninjectedArgsTuple(comptime Function: type, comptime Injectable: type) type 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; - } + const is_injected = blk: { + switch (@typeInfo(arg.type)) { + .Struct => break :blk @hasDecl(arg.type, "IsInjectedArgument"), + .Pointer => { + switch (@typeInfo(std.meta.Child(arg.type))) { + .Struct => break :blk @hasDecl(std.meta.Child(arg.type), "IsInjectedArgument"), + else => break :blk false, + } + }, + else => break :blk false, } - break :blk false; }; - if (injectable) continue; // legitimate injected argument, ignore it + if (is_injected) continue; // legitimate injected argument, ignore it std_args = std_args ++ [_]type{arg.type}; } - return std.meta.Tuple(std_args); + return Tuple(std_args); } /// enum describing every possible comptime-known global event name. @@ -644,39 +696,37 @@ test injectArgs { } 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; + const Foo = struct { + foo: f32, + pub const IsInjectedArgument = void; + }; // No standard, no injected - TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn () void, @TypeOf(.{}))); - TupleTester.assertTuple(.{}, UninjectedArgsTuple(fn () void, @TypeOf(.{ i32_ptr, f32_ptr, foo_ptr }))); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn () void)); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn () void)); // 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 }))); + TupleTester.assertTuple(.{i32}, UninjectedArgsTuple(std.meta.Tuple, fn (a: i32) void)); + TupleTester.assertTuple(.{ i32, f32 }, UninjectedArgsTuple(std.meta.Tuple, fn (a: i32, b: f32) void)); // 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 }))); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn (a: *i32) void)); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn (a: *f32) void)); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn (a: *Foo) void)); + TupleTester.assertTuple(.{}, UninjectedArgsTuple(std.meta.Tuple, fn (a: *f32, b: *Foo, c: *i32) void)); // 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 }))); + TupleTester.assertTuple(.{f32}, UninjectedArgsTuple(std.meta.Tuple, fn (a: f32) void)); + TupleTester.assertTuple(.{ i32, *f32 }, UninjectedArgsTuple(std.meta.Tuple, fn (a: i32, b: *f32) void)); + TupleTester.assertTuple(.{ i32, *i32, *f32 }, UninjectedArgsTuple(std.meta.Tuple, fn (a: i32, b: *i32, c: *f32) void)); // 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 }))); + TupleTester.assertTuple(.{ i32, *i32, *f32 }, UninjectedArgsTuple(std.meta.Tuple, fn (a: *f32, b: i32, c: *i32, d: *f32) void)); // 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 }))); + TupleTester.assertTuple(.{ i32, *Foo, *i32, *f32 }, UninjectedArgsTuple(std.meta.Tuple, fn (a: *f32, b: i32, c: *Foo, d: *i32, e: *f32) void)); } test "event name calling" {