//! ---------------------------------------------------- //! ---------------------------------------------------- const std = @import("std"); const lua = @import("luajit"); const Self = @This(); // // FIELDS // state: *lua.lua_State, err: ?[*:0]const u8 = null, /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn init() !Self { const state = lua.luaL_newstate() orelse return error.LuaStateFailed; lua.luaL_openlibs(state); return .{ .state = state }; } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn deinit(self: *Self) void { lua.lua_close(self.state); } // // EXECUTION // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn exec(self: *Self, comptime ReturnType: type, code: []const u8) !ReturnType { if (lua.luaL_loadbuffer(self.state, code.ptr, code.len, "") != 0) { return self.luaError(); } return self.pcall(ReturnType, 0); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn execFromFile(self: *Self, comptime ReturnType: type, path: [*:0]const u8) !ReturnType { if (lua.luaL_dofile(self.state, path) != 0) { return self.luaError(); } if (ReturnType == void) return {}; return self.readReturn(ReturnType); } // // GLOBALS // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn getGlobal(self: *Self, name: [*:0]const u8) void { lua.lua_getglobal(self.state, name); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn setGlobal(self: *Self, name: [*:0]const u8) void { lua.lua_setglobal(self.state, name); } // // STACK // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn getTop(self: *Self) c_int { return lua.lua_gettop(self.state); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn pop(self: *Self, n: c_int) void { lua.lua_settop(self.state, -n - 1); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn pushNil(self: *Self) void { lua.lua_pushnil(self.state); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn push(self: *Self, value: anytype) void { const T = @TypeOf(value); switch (@typeInfo(T)) { .void, .null => lua.lua_pushnil(self.state), .optional => { if (value) |v| { self.push(v); } else { lua.lua_pushnil(self.state); } }, .bool => lua.lua_pushboolean(self.state, @intFromBool(value)), .int => lua.lua_pushinteger(self.state, @intCast(value)), .float => lua.lua_pushnumber(self.state, @floatCast(value)), .pointer => |p| { if (p.child == u8) { if (p.sentinel != null) { lua.lua_pushstring(self.state, @ptrCast(value.ptr)); } else { lua.lua_pushlstring(self.state, value.ptr, value.len); } } else { const child_info = @typeInfo(p.child); if (child_info == .array and child_info.array.child == u8 and child_info.array.sentinel_ptr != null) { lua.lua_pushstring(self.state, @ptrCast(value)); } } }, else => @compileError("unsupported push type: " ++ @typeName(T)), } } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn add(self: *Self, values: anytype) void { inline for (values) |v| { self.push(v); } } // // READING // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn to(self: *Self, comptime T: type, idx: c_int) T { return switch (T) { bool => lua.lua_toboolean(self.state, idx) != 0, i8, i16, i32, i64, u8, u16, u32, u64 => @intCast(lua.lua_tointeger(self.state, idx)), f32, f64 => @floatCast(lua.lua_tonumber(self.state, idx)), [:0]const u8 => ptr: { const ptr = lua.lua_tolstring(self.state, idx, null) orelse break :ptr ""; break :ptr std.mem.sliceTo(ptr, 0); }, []const u8 => ptr: { var len: usize = 0; const ptr = lua.lua_tolstring(self.state, idx, &len) orelse break :ptr ""; break :ptr ptr[0..len]; }, else => @compileError("unsupported to type: " ++ @typeName(T)), }; } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn toOwned(self: *Self, comptime T: type, alloc: std.mem.Allocator, idx: c_int) !T { return switch (T) { []const u8 => { var len: usize = 0; const ptr = lua.lua_tolstring(self.state, idx, &len) orelse return error.InvalidValue; return try alloc.dupe(u8, ptr[0..len]); }, [:0]const u8 => { const ptr = lua.lua_tolstring(self.state, idx, null) orelse return error.InvalidValue; return try alloc.dupeZ(u8, std.mem.sliceTo(ptr, 0)); }, else => self.to(T, idx), }; } // // CALLING // /// ---------------------------------------------------- /// ---------------------------------------------------- fn readReturn(self: *Self, comptime ReturnType: type) ReturnType { const info = @typeInfo(ReturnType); if (info == .@"struct" and info.@"struct".is_tuple) { const n: c_int = @intCast(info.@"struct".fields.len); var result: ReturnType = undefined; inline for (info.@"struct".fields, 0..) |field, i| { result[i] = self.to(field.type, -n + @as(c_int, @intCast(i))); } self.pop(n); return result; } const result = self.to(ReturnType, -1); self.pop(1); return result; } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn pcall(self: *Self, comptime ReturnType: type, nargs: u32) !ReturnType { const nresults: c_int = if (ReturnType == void) 0 else switch (@typeInfo(ReturnType)) { .@"struct" => |s| if (s.is_tuple) @intCast(s.fields.len) else @as(c_int, 1), else => 1, }; if (lua.lua_pcall(self.state, @intCast(nargs), nresults, 0) != 0) { return self.luaError(); } if (ReturnType == void) return {}; return self.readReturn(ReturnType); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn call( self: *Self, comptime ReturnType: type, name: [*:0]const u8, args: anytype, ) !ReturnType { self.getGlobal(name); inline for (args) |arg| { self.push(arg); } return self.pcall(ReturnType, args.len); } // // TABLES // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn tableGet(self: *Self, idx: c_int, key: anytype) void { self.push(key); lua.lua_gettable(self.state, if (idx < 0) idx - 1 else idx); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn tableSet(self: *Self, idx: c_int, key: anytype, value: anytype) void { self.push(key); self.push(value); lua.lua_settable(self.state, if (idx < 0) idx - 2 else idx); } // // REGISTRATION // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn register(self: *Self, name: [*:0]const u8, comptime func: anytype) void { const fn_info = @typeInfo(@TypeOf(func)).@"fn"; const Return = fn_info.return_type orelse void; const Wrapper = struct { fn luaFunc(state: ?*lua.lua_State) callconv(.c) c_int { var zl = Self{ .state = state.? }; const args: std.meta.ArgsTuple(@TypeOf(func)) = args: { var a: std.meta.ArgsTuple(@TypeOf(func)) = undefined; inline for (fn_info.params, 0..) |param, i| { a[i] = zl.to(param.type.?, @intCast(i + 1)); } break :args a; }; const ret = @call(.auto, func, args); if (Return == void) return 0; zl.push(ret); return 1; } }; lua.lua_pushcfunction(self.state, Wrapper.luaFunc); lua.lua_setglobal(self.state, name); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn registerWithCtx( self: *Self, name: [*:0]const u8, comptime func: anytype, ctx: anytype, ) void { const CtxType = @TypeOf(ctx); const fn_info = @typeInfo(@TypeOf(func)).@"fn"; const Return = fn_info.return_type orelse void; const Wrapper = struct { fn luaFunc(state: ?*lua.lua_State) callconv(.c) c_int { var zl = Self{ .state = state.? }; const ctx_ptr: CtxType = @ptrCast(@alignCast( lua.lua_touserdata(state, lua.lua_upvalueindex(1)), )); const args: std.meta.ArgsTuple(@TypeOf(func)) = args: { var a: std.meta.ArgsTuple(@TypeOf(func)) = undefined; inline for (fn_info.params, 0..) |param, i| { if (i == 0) { a[i] = ctx_ptr; } else { a[i] = zl.to(param.type.?, @intCast(i)); } } break :args a; }; const ret = @call(.auto, func, args); if (Return == void) return 0; zl.push(ret); return 1; } }; lua.lua_pushlightuserdata(self.state, @ptrCast(@constCast(ctx))); lua.lua_pushcclosure(self.state, Wrapper.luaFunc, 1); lua.lua_setglobal(self.state, name); } /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn registerMany(self: *Self, values: anytype) void { inline for (values) |value| { self.register(value.name, value.func); } } // // ERRORS // /// ---------------------------------------------------- /// ---------------------------------------------------- pub fn lastError(self: *Self) ?[:0]const u8 { if (self.err) |p| { return std.mem.sliceTo(p, 0); } return null; } /// ---------------------------------------------------- /// ---------------------------------------------------- fn luaError(self: *Self) error{LuaError} { self.err = lua.lua_tolstring(self.state, -1, null); if (self.err) |p| { std.debug.print("lua error: {s}\n", .{p[0..std.mem.len(p)]}); } lua.lua_pop(self.state, 1); return error.LuaError; } // // TESTS // fn spawnEntity(name: []const u8, x: f64, y: f64) void { std.debug.print("spawned '{s}' at ({d:.1}, {d:.1})\n", .{ name, x, y }); } const Ctx = struct { counter: i64 = 0, }; fn increment(ctx: *Ctx) i64 { ctx.counter += 1; return ctx.counter; } test "exec" { // --- ZLA --- var zl = try Self.init(); defer zl.deinit(); // --- REGISTER FUNCTIONS TO LUA --- zl.register("spawnEntity", spawnEntity); // --- EXEC FILE --- try zl.exec(void, @embedFile("scripts/basic.lua")); // try zl.execFromFile(void, "src/scripts/basic.lua"); // --- CALL --- try zl.call(void, "hello", .{"World"}); try zl.call(void, "hello", .{"World"}); // --- CALL RETURN --- const sum = try zl.call(i64, "add", .{ @as(i64, 3), @as(i64, 4) }); std.debug.print("Result: {}\n", .{sum}); // --- MULTI RETURN --- const coords = try zl.call(struct { f64, f64 }, "getCoords", .{}); std.debug.print("coords: ({d:.1}, {d:.1})\n", .{ coords[0], coords[1] }); // --- EXEC WITH RETURN --- const val = try zl.exec(i64, "return 6 * 7"); try std.testing.expectEqual(@as(i64, 42), val); } // TODO: Nicer tables / generic // test "tables" { // // --- ZLA --- // var zl = try Self.init(); // defer zl.deinit(); // // var ctx = Ctx{}; // zl.registerWithCtx("increment", increment, &ctx); // // // --- TABLE --- // zl.getGlobal("config"); // zl.tableGet(-1, "width"); // const width = zl.to(i64, -1); // std.debug.print("width: {}\n", .{width}); // zl.pop(2); // // // --- CTX --- // const n = try zl.call(i64, "increment", .{}); // std.debug.print("counter: {}\n", .{n}); // // // --- LAST ERROR --- // zl.exec(void, "nonexistent()") catch {}; // const err = zl.lastError(); // try std.testing.expect(err != null); // std.debug.print("caught: {s}\n", .{err.?}); // }