diff --git a/src/lib.zig b/src/lib.zig index a16a521..58041b2 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -4343,7 +4343,74 @@ pub const Lua = opaque { /// Works with ints, floats, booleans, structs, /// tagged unions, optionals, and strings pub fn pushAny(lua: *Lua, value: anytype) !void { - switch (@typeInfo(@TypeOf(value))) { + const T = @TypeOf(value); + const type_info = @typeInfo(T); + + if (type_info == .@"struct" or type_info == .@"union" or type_info == .@"enum") { + if (@hasDecl(T, "toLua")) { + const toLuaArgs = .{ value, lua }; + const fnSignature = comptime fn_sign: { + var b: []const u8 = "pub fn toLua("; + + for (0..toLuaArgs.len) |i| { + b = b ++ std.fmt.comptimePrint("{s}{s}", .{ @typeName(@TypeOf(toLuaArgs[i])), if (i == (toLuaArgs.len - 1)) "" else ", " }); + } + + b = b ++ ") !void"; + + break :fn_sign b; + }; + + const fl = @field(T, "toLua"); + const flt = @TypeOf(fl); + const fli = @typeInfo(flt); + switch (fli) { + .@"fn" => |f| { + const args_ok = comptime args_ok: { + const f_params = f.params; + + if (f_params.len != toLuaArgs.len) break :args_ok false; + + for (0..toLuaArgs.len) |i| { + if (f_params[i].type != @TypeOf(toLuaArgs[i])) break :args_ok false; + } + + break :args_ok true; + }; + + if (args_ok) { + if (f.return_type) |rt| { + const rti = @typeInfo(rt); + switch (rti) { + .error_union => { + if (rti.error_union.payload == void) { + return try @call(.auto, fl, toLuaArgs); + } else { + @compileError("toLua invalid return type, required fn signature: " ++ fnSignature); + } + }, + .void => { + return @call(.auto, fl, toLuaArgs); + }, + else => { + @compileError("toLua invalid return type, required fn signature: " ++ fnSignature); + }, + } + } else { + return @call(.auto, fl, toLuaArgs); + } + } else { + @compileError("toLua has invalid args, required fn signature: " ++ fnSignature); + } + }, + else => { + @compileError("toLua is not a function, required fn signature: " ++ fnSignature); + }, + } + } + } + + switch (type_info) { .int, .comptime_int => { lua.pushInteger(@intCast(value)); }, @@ -4398,10 +4465,18 @@ pub const Lua = opaque { }, .@"struct" => |info| { lua.createTable(0, 0); - inline for (info.fields) |field| { - try lua.pushAny(field.name); - try lua.pushAny(@field(value, field.name)); - lua.setTable(-3); + if (info.is_tuple) { + inline for (0..info.fields.len) |i| { + try lua.pushAny(i + 1); + try lua.pushAny(value[i]); + lua.setTable(-3); + } + } else { + inline for (info.fields) |field| { + try lua.pushAny(field.name); + try lua.pushAny(@field(value, field.name)); + lua.setTable(-3); + } } }, .@"union" => |info| { @@ -4458,7 +4533,7 @@ pub const Lua = opaque { /// Converts the specified index of the lua stack to the specified /// type if possible and returns it /// optional allocator - fn toAnyInternal(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, index: i32) !T { + pub fn toAnyInternal(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, index: i32) !T { const stack_size_on_entry = lua.getTop(); defer { if (lua.getTop() != stack_size_on_entry) { @@ -4468,7 +4543,74 @@ pub const Lua = opaque { } } - switch (@typeInfo(T)) { + const type_info = @typeInfo(T); + + if (type_info == .@"struct" or type_info == .@"union" or type_info == .@"enum") { + if (@hasDecl(T, "fromLua")) { + const fromLuaArgs = .{ lua, a, index }; + const fnSignature = comptime fn_sign: { + var b: []const u8 = "pub fn fromLua("; + + for (0..fromLuaArgs.len) |i| { + b = b ++ std.fmt.comptimePrint("{s}{s}", .{ @typeName(@TypeOf(fromLuaArgs[i])), if (i == (fromLuaArgs.len - 1)) "" else ", " }); + } + + b = b ++ ") !" ++ @typeName(T); + + break :fn_sign b; + }; + + const fl = @field(T, "fromLua"); + const flt = @TypeOf(fl); + const fli = @typeInfo(flt); + switch (fli) { + .@"fn" => |f| { + const args_ok = comptime args_ok: { + const f_params = f.params; + + if (f_params.len != fromLuaArgs.len) break :args_ok false; + + for (0..fromLuaArgs.len) |i| { + if (f_params[i].type != @TypeOf(fromLuaArgs[i])) break :args_ok false; + } + + break :args_ok true; + }; + + if (args_ok) { + if (f.return_type) |rt| { + if (rt == T) { + return @call(.auto, fl, fromLuaArgs); + } else { + const rti = @typeInfo(rt); + switch (rti) { + .error_union => { + if (rti.error_union.payload == T) { + return try @call(.auto, fl, fromLuaArgs); + } else { + @compileError("fromLua invalid return type, required fn signature: " ++ fnSignature); + } + }, + else => { + @compileError("fromLua invalid return type, required fn signature: " ++ fnSignature); + }, + } + } + } else { + @compileError("fromLua require a fn signature: " ++ fnSignature); + } + } else { + @compileError("fromLua has invalid args, required fn signature: " ++ fnSignature); + } + }, + else => { + @compileError("fromLua is not a function, required fn signature: " ++ fnSignature); + }, + } + } + } + + switch (type_info) { .int => { const result = try lua.toInteger(index); return @as(T, @intCast(result)); @@ -4542,7 +4684,11 @@ pub const Lua = opaque { return error.LuaInvalidEnumTagName; }, .@"struct" => { - return try lua.toStruct(T, a, allow_alloc, index); + if (type_info.@"struct".is_tuple) { + return try lua.toTuple(T, a, allow_alloc, index); + } else { + return try lua.toStruct(T, a, allow_alloc, index); + } }, .@"union" => |u| { if (u.tag_type == null) @compileError("Parameter type is not a tagged union"); @@ -4588,7 +4734,7 @@ pub const Lua = opaque { } /// Converts a lua array to a zig slice, memory is owned by the caller - fn toSlice(lua: *Lua, comptime ChildType: type, a: std.mem.Allocator, raw_index: i32) ![]ChildType { + pub fn toSlice(lua: *Lua, comptime ChildType: type, a: std.mem.Allocator, raw_index: i32) ![]ChildType { const index = lua.absIndex(raw_index); if (!lua.isTable(index)) { @@ -4608,8 +4754,51 @@ pub const Lua = opaque { return result; } + /// Converts value at given index to a zig struct tuple if possible + pub fn toTuple(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, raw_index: i32) !T { + const stack_size_on_entry = lua.getTop(); + defer std.debug.assert(lua.getTop() == stack_size_on_entry); + + const info = @typeInfo(T).@"struct"; + const index = lua.absIndex(raw_index); + + var result: T = undefined; + + if (lua.isTable(index)) { + lua.pushValue(index); + defer lua.pop(1); + + inline for (info.fields, 0..) |field, i| { + if (lua.getMetaField(-1, "__index")) |_| { + lua.pushValue(-2); + lua.pushInteger(@intCast(i + 1)); + lua.call(.{ .args = 1, .results = 1 }); + } else |_| { + _ = lua.rawGetIndex(-1, @intCast(i + 1)); + } + defer lua.pop(1); + result[i] = try lua.toAnyInternal(field.type, a, allow_alloc, -1); + } + } else { + // taking it as vararg + const in_range = if (raw_index < 0) (index - @as(i32, info.fields.len)) >= 0 else ((index + @as(i32, info.fields.len)) - 1) <= stack_size_on_entry; + if (in_range) { + inline for (info.fields, 0..) |field, i| { + const stack_size_before_call = lua.getTop(); + const idx = if (raw_index < 0) index - @as(i32, @intCast(i)) else index + @as(i32, @intCast(i)); + result[i] = try lua.toAnyInternal(field.type, a, allow_alloc, idx); + std.debug.assert(stack_size_before_call == lua.getTop()); + } + } else { + return error.NotInRange; + } + } + + return result; + } + /// Converts value at given index to a zig struct if possible - fn toStruct(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, raw_index: i32) !T { + pub fn toStruct(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, raw_index: i32) !T { const stack_size_on_entry = lua.getTop(); defer std.debug.assert(lua.getTop() == stack_size_on_entry); diff --git a/src/tests.zig b/src/tests.zig index d9ff381..29352f1 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2382,6 +2382,90 @@ test "toAny struct" { )); } +test "toAny tuple from vararg" { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + + const Tuple = std.meta.Tuple(&.{ i32, bool, i32 }); + + lua.pushInteger(100); + lua.pushBoolean(true); + lua.pushInteger(300); + + const result = try lua.toAny(Tuple, 1); + try testing.expect(std.meta.eql(result, .{ 100, true, 300 })); + + const result_reverse = try lua.toAny(Tuple, -1); + try testing.expect(std.meta.eql(result_reverse, .{ 300, true, 100 })); + + const result_error = lua.toAny(Tuple, 2); + try testing.expectError(error.NotInRange, result_error); + + const result_reverse_error = lua.toAny(Tuple, -2); + try testing.expectError(error.NotInRange, result_reverse_error); +} + +test "toAny tuple from struct" { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + + const MyType = struct { + foo: i32, + bar: bool, + tuple: std.meta.Tuple(&.{ i32, bool, struct { foo: bool } }), + }; + + try lua.doString( + \\ value = { + \\ ["foo"] = 10, + \\ ["bar"] = false, + \\ ["tuple"] = {100, false, {["foo"] = true}} + \\ } + ); + + const lua_type = try lua.getGlobal("value"); + try testing.expect(lua_type == .table); + const my_struct = try lua.toAny(MyType, 1); + try testing.expect(std.meta.eql( + my_struct, + MyType{ .foo = 10, .bar = false, .tuple = .{ 100, false, .{ .foo = true } } }, + )); +} + +test "toAny from struct with fromLua" { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + + const MyType = struct { + foo: bool, + bar: struct { + const Self = @This(); + foo: i32, + + pub fn fromLua(l: *Lua, a: ?std.mem.Allocator, i: i32) !Self { + return try l.toStruct(Self, a, false, i); + } + }, + }; + + try lua.doString( + \\ value = { + \\ ["foo"] = true, + \\ ["bar"] = { + \\ ["foo"] = 12 + \\ } + \\ } + ); + + const lua_type = try lua.getGlobal("value"); + try testing.expect(lua_type == .table); + const my_struct = try lua.toAny(MyType, 1); + try testing.expect(std.meta.eql( + my_struct, + MyType{ .foo = true, .bar = .{ .foo = 12 } }, + )); +} + test "toAny mutable string" { var lua = try Lua.init(testing.allocator); defer lua.deinit(); @@ -2591,6 +2675,49 @@ test "pushAny struct" { try testing.expect(value.bar == (MyType{}).bar); } +test "pushAny tuple" { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + + const Tuple = std.meta.Tuple(&.{ i32, bool, i32 }); + const value: Tuple = .{ 500, false, 600 }; + + try lua.pushAny(value); + + const result = try lua.toAny(Tuple, 1); + try testing.expect(std.meta.eql(result, .{ 500, false, 600 })); +} + +test "pushAny from struct with toLua" { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + + const MyType = struct { + const Self = @This(); + foo: i32, + tuple: std.meta.Tuple(&.{ i32, i32 }), + + pub fn toLua(self: Self, l: *Lua) void { + l.newTable(); + + inline for (@typeInfo(Self).@"struct".fields) |f| { + try l.pushAny(f.name); + try l.pushAny(@field(self, f.name)); + l.setTable(-3); + } + } + }; + + const value: MyType = .{ .foo = 15, .tuple = .{ 1, 2 } }; + + try lua.pushAny(value); + const my_struct = try lua.toAny(MyType, 1); + try testing.expect(std.meta.eql( + my_struct, + MyType{ .foo = 15, .tuple = .{ 1, 2 } }, + )); +} + test "pushAny tagged union" { var lua = try Lua.init(testing.allocator); defer lua.deinit();