diff --git a/src/lib.zig b/src/lib.zig index 0451392..8556ba7 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -647,7 +647,19 @@ pub const Lua = struct { /// Returns the acceptable index index converted into an equivalent absolute index /// See https://www.lua.org/manual/5.4/manual.html#lua_absindex pub fn absIndex(lua: *Lua, index: i32) i32 { - return c.lua_absindex(lua.state, index); + switch (lang) { + .lua51, .luajit => { + if (index > 0 or index <= registry_index) { + return index; + } else { + const result = lua.getTop() + 1 + index; + return @intCast(result); + } + }, + else => { + return c.lua_absindex(lua.state, index); + }, + } } /// Performs an arithmetic or bitwise operation over the value(s) at the top of the stack, @@ -3013,6 +3025,252 @@ pub const Lua = struct { pub fn openBit32(lua: *Lua) void { _ = c.luaopen_bit32(lua.state); } + + /// Pushes any valid zig value onto the stack, + /// Works with ints, floats, booleans, structs, + /// optionals, and strings + pub fn pushAny(lua: *Lua, value: anytype) !void { + switch (@typeInfo(@TypeOf(value))) { + .Int, .ComptimeInt => { + lua.pushInteger(@intCast(value)); + }, + .Float, .ComptimeFloat => { + lua.pushNumber(@floatCast(value)); + }, + .Pointer => |info| { + switch (info.size) { + .One => { + if (@typeInfo(info.child) == .Array) { + if (@typeInfo(info.child).Array.child != u8) { + @compileError("only u8 arrays can be pushed"); + } + _ = lua.pushString(&(value.*)); + } else { + if (info.is_const) { + @compileLog(value); + @compileError("Pointer must not be const"); + } + lua.pushLightUserdata(@ptrCast(value)); + } + }, + .C, .Many, .Slice => { + if (info.child != u8) { + @compileError("Only u8 slices (strings) are valid slice types"); + } + if (info.sentinel) |sentinel| { + const casted: *info.child = @ptrCast(@constCast(sentinel)); + if (casted.* != 0) { + @compileError("Sentinel of slice must be a null terminator"); + } + _ = lua.pushString(value); + } else { + const null_terminated = try lua.allocator().dupeZ(u8, value); + defer lua.allocator().free(null_terminated); + _ = lua.pushString(null_terminated); + } + }, + } + }, + .Bool => { + lua.pushBoolean(value); + }, + .Optional, .Null => { + if (value == null) { + lua.pushNil(); + } else { + try lua.pushAny(value.?); + } + }, + .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); + } + }, + .Fn => { + lua.autoPushFunction(value); + }, + .Void => {}, + else => { + @compileLog(value); + @compileError("Invalid type given"); + }, + } + } + + /// Converts the specified index of the lua stack to the specified + /// type if possible and returns it + pub fn toAny(lua: *Lua, comptime T: type, index: i32) !T { + + //TODO implement enums + switch (@typeInfo(T)) { + .Int => { + switch (comptime lang) { + .lua51, .luajit => { + const result = lua.toInteger(index); + return @as(T, @intCast(result)); + }, + else => { + const result = try lua.toInteger(index); + return @as(T, @intCast(result)); + }, + } + }, + .Float => { + switch (comptime lang) { + .lua51, .luajit => { + const result = lua.toNumber(index); + return @as(T, @floatCast(result)); + }, + else => { + const result = try lua.toNumber(index); + return @as(T, @floatCast(result)); + }, + } + }, + .Pointer => |param_info| { + switch (param_info.size) { + .Slice, .Many => { + if (param_info.child != u8) { + @compileError("Only u8 arrays (strings) may be parameters"); + } + if (!param_info.is_const) { + @compileError("Slice must be a const slice"); + } + const string: [*:0]const u8 = try lua.toString(index); + const end = std.mem.indexOfSentinel(u8, 0, string); + + if (param_info.sentinel == null) { + return string[0..end]; + } else { + return string[0..end :0]; + } + }, + else => { + return try lua.toUserdata(param_info.child, index); + }, + } + }, + .Bool => { + return lua.toBoolean(index); + }, + .Struct => { + return try lua.toStruct(T, index); + }, + .Optional => { + if (lua.isNil(index)) { + lua.pop(1); + return null; + } else { + return try lua.toAny(@typeInfo(T).Optional.child, index); + } + }, + else => { + @compileError("Invalid parameter type"); + }, + } + } + + /// Converts value at given index to a zig struct if possible + fn toStruct(lua: *Lua, comptime T: type, raw_index: i32) !T { + const index = lua.absIndex(raw_index); + + if (!lua.isTable(index)) { + return error.ValueNotATable; + } + std.debug.assert(lua.typeOf(index) == .table); + + var result: T = undefined; + inline for (@typeInfo(T).Struct.fields) |field| { + const field_name = comptime field.name ++ ""; + _ = lua.pushString(field_name); + std.debug.assert(lua.typeOf(index) == .table); + const lua_field_type = lua.getTable(index); + if (lua_field_type == .nil) { + if (field.default_value) |default_value| { + @field(result, field.name) = @as(*const field.type, @ptrCast(@alignCast(default_value))).*; + } else { + return error.LuaTableMissingValue; + } + } else { + @field(result, field.name) = try lua.toAny(field.type, -1); + } + } + + return result; + } + + ///automatically calls a lua function with the given arguments + pub fn autoCall(lua: *Lua, comptime ReturnType: type, func_name: [:0]const u8, args: anytype) !ReturnType { + if (try lua.getGlobal(func_name) != LuaType.function) return error.InvalidFunctionName; + + inline for (args) |arg| { + try lua.pushAny(arg); + } + + const num_results = if (ReturnType == void) 0 else 1; + try lua.protectedCall(args.len, num_results, 0); + defer lua.setTop(0); + + return lua.toAny(ReturnType, -1); + } + + //automatically generates a wrapper function + fn GenerateInterface(comptime function: anytype) type { + const info = @typeInfo(@TypeOf(function)); + if (info != .Fn) { + @compileLog(info); + @compileLog(function); + @compileError("function pointer must be passed"); + } + return struct { + pub fn interface(lua: *Lua) i32 { + var parameters: std.meta.ArgsTuple(@TypeOf(function)) = undefined; + + inline for (info.Fn.params, 0..) |param, i| { + parameters[i] = lua.toAny(param.type.?, (i + 1)) catch |err| { + lua.raiseErrorStr(@errorName(err), .{}); + }; + } + + if (@typeInfo(info.Fn.return_type.?) == .ErrorUnion) { + const result = @call(.auto, function, parameters) catch |err| { + lua.raiseErrorStr(@errorName(err), .{}); + }; + lua.pushAny(result) catch |err| { + lua.raiseErrorStr(@errorName(err), .{}); + }; + } else { + const result = @call(.auto, function, parameters); + lua.pushAny(result) catch |err| { + lua.raiseErrorStr(@errorName(err), .{}); + }; + } + + return 1; + } + }; + } + + ///generates the interface for and pushes a function to the stack + pub fn autoPushFunction(lua: *Lua, function: anytype) void { + const Interface = GenerateInterface(function); + lua.pushFunction(wrap(Interface.interface)); + } + + ///get any lua global + pub fn get(lua: *Lua, comptime ReturnType: type, name: [:0]const u8) !ReturnType { + _ = try lua.getGlobal(name); + return try lua.toAny(ReturnType, -1); + } + + ///set any lua global + pub fn set(lua: *Lua, name: [:0]const u8, value: anytype) !void { + try lua.pushAny(value); + lua.setGlobal(name); + } }; /// A string buffer allowing for Zig code to build Lua strings piecemeal diff --git a/src/tests.zig b/src/tests.zig index 749c9ea..e11e09e 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2401,3 +2401,206 @@ test "namecall" { lua.pop(-1); try expectEqual(6, s); } + +test "toAny" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + //int + lua.pushInteger(100); + const my_int = try lua.toAny(i32, -1); + try testing.expect(my_int == 100); + + //bool + lua.pushBoolean(true); + const my_bool = try lua.toAny(bool, -1); + try testing.expect(my_bool); + + //float + lua.pushNumber(100.0); + const my_float = try lua.toAny(f32, -1); + try testing.expect(my_float == 100.0); + + //[]const u8 + _ = lua.pushString("hello world"); + const my_string_1 = try lua.toAny([]const u8, -1); + try testing.expect(std.mem.eql(u8, my_string_1, "hello world")); + + //[:0]const u8 + _ = lua.pushString("hello world"); + const my_string_2 = try lua.toAny([:0]const u8, -1); + try testing.expect(std.mem.eql(u8, my_string_2, "hello world")); + + //[*:0]const u8 + _ = lua.pushString("hello world"); + const my_string_3 = try lua.toAny([*:0]const u8, -1); + const end = std.mem.indexOfSentinel(u8, 0, my_string_3); + try testing.expect(std.mem.eql(u8, my_string_3[0..end], "hello world")); + + //ptr + var my_value: i32 = 100; + _ = lua.pushLightUserdata(&my_value); + const my_ptr = try lua.toAny(*i32, -1); + try testing.expect(my_ptr.* == my_value); + + //optional + lua.pushNil(); + const maybe = try lua.toAny(?i32, -1); + try testing.expect(maybe == null); +} + +test "toAny struct" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + const MyType = struct { + foo: i32, + bar: bool, + bizz: []const u8 = "hi", + }; + try lua.doString("value = {[\"foo\"] = 10, [\"bar\"] = false}"); + 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 }, + )); +} + +test "toAny struct recursive" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + const MyType = struct { + foo: i32 = 10, + bar: bool = false, + bizz: []const u8 = "hi", + meep: struct { a: ?i7 = null } = .{}, + }; + + try lua.doString( + \\value = { + \\ ["foo"] = 10, + \\ ["bar"] = true, + \\ ["bizz"] = "hi", + \\ ["meep"] = { + \\ ["a"] = nil + \\ } + \\} + ); + + _ = try lua.getGlobal("value"); + const my_struct = try lua.toAny(MyType, -1); + _ = my_struct; +} + +test "pushAny" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + //int + try lua.pushAny(1); + const my_int = try toInteger(&lua, -1); + try testing.expect(my_int == 1); + + //float + try lua.pushAny(1.0); + const my_float = try toNumber(&lua, -1); + try testing.expect(my_float == 1.0); + + //bool + try lua.pushAny(true); + const my_bool = lua.toBoolean(-1); + try testing.expect(my_bool); + + //string literal + try lua.pushAny("hello world"); + const value = try lua.toString(-1); + const end = std.mem.indexOfSentinel(u8, 0, value); + try testing.expect(std.mem.eql(u8, value[0..end], "hello world")); + + //null + try lua.pushAny(null); + try testing.expect(try lua.toAny(?f32, -1) == null); + + //optional + const my_optional: ?i32 = -1; + try lua.pushAny(my_optional); + try testing.expect(try lua.toAny(?i32, -1) == my_optional); +} + +test "pushAny struct" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + const MyType = struct { + foo: i32 = 1, + bar: bool = false, + bizz: []const u8 = "hi", + }; + try lua.pushAny(MyType{}); + const value = try lua.toAny(MyType, -1); + try testing.expect(std.mem.eql(u8, value.bizz, (MyType{}).bizz)); + try testing.expect(value.foo == (MyType{}).foo); + try testing.expect(value.bar == (MyType{}).bar); +} + +fn foo(a: i32, b: i32) i32 { + return a + b; +} + +fn bar(a: i32, b: i32) !i32 { + if (a > b) return error.wrong; + return a + b; +} + +test "autoPushFunction" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + lua.openLibs(); + + lua.autoPushFunction(foo); + lua.setGlobal("foo"); + + lua.autoPushFunction(bar); + lua.setGlobal("bar"); + + try lua.doString("result = foo(1, 2)"); + + const program = + \\local status, result = pcall(bar, 1, 2) + ; + lua.doString(program) catch |err| { + std.debug.print("{!}\n\n", .{err}); + }; +} + +test "autoCall" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + const program = + \\function add(a, b) + \\ return a + b + \\end + ; + + try lua.doString(program); + const sum = try lua.autoCall(usize, "add", .{ 1, 2 }); + try std.testing.expect(3 == sum); +} + +test "get set" { + var lua = try Lua.init(&testing.allocator); + defer lua.deinit(); + + try lua.set("hello", true); + try testing.expect(try lua.get(bool, "hello")); + + try lua.set("world", 1000); + try testing.expect(try lua.get(u64, "world") == 1000); + + try lua.set("foo", 'a'); + try testing.expect(try lua.get(u8, "foo") == 'a'); +}