zla/src/root.zig

425 lines
13 KiB
Zig
Raw Normal View History

2026-07-27 10:07:04 +02:00
//! ----------------------------------------------------
//! ----------------------------------------------------
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.?});
// }