From 188508cd3e2054f3ab7bdb88e8b90d2abe11760c Mon Sep 17 00:00:00 2001 From: D-Berg Date: Thu, 6 Aug 2026 09:13:49 +0200 Subject: [PATCH] improve lua error handling All native zig functions are wrapped by a lua helper. Avoids calling assert in manifests and avoids calling lua_Lerror which makes defers not work. --- src/actions/build_package.zig | 42 ++++++++++++++++------------- src/actions/info.zig | 2 +- src/actions/install.zig | 2 +- src/error_wrapper.lua | 13 +++++++++ src/lua_helpers.zig | 51 ++++++++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 src/error_wrapper.lua diff --git a/src/actions/build_package.zig b/src/actions/build_package.zig index 785de7b..e8ed161 100644 --- a/src/actions/build_package.zig +++ b/src/actions/build_package.zig @@ -39,7 +39,7 @@ pub fn build(io: Io, gpa: Allocator, arena: Allocator, env: *std.process.Environ try lua.new(0); defer lua.close(); - lua_helpers.setupState(&lua); + try lua_helpers.setupState(&lua); var state: Package.State = .empty; const pkg_id = try Package.collect(io, arena, &state, packa_dir, args.package_name, &lua, true); @@ -154,8 +154,11 @@ pub fn build(io: Io, gpa: Allocator, arena: Allocator, env: *std.process.Environ .verbose = args.verbose, }; { // b.run = luaRun - lua.pushLightUserdata(@ptrCast(@alignCast(@constCast(&run_ctx)))); - lua.pushCClosure(luaRun, 1); + try lua_helpers.pushChecked( + &lua, + luaRun, + @ptrCast(@alignCast(@constCast(&run_ctx))), + ); lua.setField(b, "run"); } @@ -166,30 +169,33 @@ pub fn build(io: Io, gpa: Allocator, arena: Allocator, env: *std.process.Environ .pkg_state = &state, }; { - lua.pushLightUserdata(@ptrCast(@alignCast(@constCast(&dep_ctx)))); - lua.pushCClosure(luaDep, 1); + try lua_helpers.pushChecked( + &lua, + luaDep, + @ptrCast(@alignCast(@constCast(&dep_ctx))), + ); lua.setField(b, "dep"); } const join_ctx: PathJoinContext = .{ .gpa = gpa }; - lua.pushLightUserdata(@ptrCast(@alignCast(@constCast(&join_ctx)))); - lua.pushCClosure(luaPathJoin, 1); + try lua_helpers.pushChecked( + &lua, + luaPathJoin, + @ptrCast(@alignCast(@constCast(&join_ctx))), + ); lua.setField(b, "pathJoin"); { // b.env = env; lua.createTable(0, 3); const env_table = lua.getTop(); - lua.pushLightUserdata(@ptrCast(@alignCast(&build_env))); - lua.pushCClosure(luaEnvSet, 1); + try lua_helpers.pushChecked(&lua, luaEnvSet, @ptrCast(@alignCast(&build_env))); lua.setField(env_table, "set"); - lua.pushLightUserdata(@ptrCast(@alignCast(&build_env))); - lua.pushCClosure(luaEnvGet, 1); + try lua_helpers.pushChecked(&lua, luaEnvGet, @ptrCast(@alignCast(&build_env))); lua.setField(env_table, "get"); - lua.pushLightUserdata(@ptrCast(@alignCast(&build_env))); - lua.pushCClosure(luaEnvAppend, 1); + try lua_helpers.pushChecked(&lua, luaEnvAppend, @ptrCast(@alignCast(&build_env))); lua.setField(env_table, "append"); lua.setField(b, "env"); @@ -291,7 +297,7 @@ fn luaEnvSet(state: ?*zlua.LuaState) callconv(.c) c_int { } const ud = lua.toUserdata(lua.upvalueIndex(1)) orelse { - lua.pushBoolean(false); + lua.pushNil(); _ = lua.pushLString("null userdata"); return 2; }; @@ -321,13 +327,13 @@ fn luaEnvGet(state: ?*zlua.LuaState) callconv(.c) c_int { } const ud = lua.toUserdata(lua.upvalueIndex(1)) orelse { - lua.pushBoolean(false); + lua.pushNil(); _ = lua.pushLString("null userdata"); return 2; }; const env_map: *std.process.Environ.Map = @ptrCast(@alignCast(ud)); - const key = lua.toLString(1); + const key = lua.toString(1); if (env_map.get(key)) |val| { _ = lua.pushLString(val); return 1; @@ -346,7 +352,7 @@ fn luaEnvAppend(state: ?*zlua.LuaState) callconv(.c) c_int { } const ud = lua.toUserdata(lua.upvalueIndex(1)) orelse { - lua.pushBoolean(false); + lua.pushNil(); _ = lua.pushLString("null userdata"); return 2; }; @@ -381,7 +387,7 @@ fn luaRun(state: ?*zlua.LuaState) callconv(.c) c_int { const n_args: usize = @intCast(lua.getTop()); if (n_args < 1) { lua.pushNil(); - _ = lua.pushLString("run requires atleast 1 arg"); + _ = lua.pushLString("run requires atleast 1 argument"); return 2; } diff --git a/src/actions/info.zig b/src/actions/info.zig index 648027d..6aeec29 100644 --- a/src/actions/info.zig +++ b/src/actions/info.zig @@ -50,7 +50,7 @@ pub fn info(io: Io, gpa: Allocator, package_name: []const u8) !void { try lua.new(0); defer lua.close(); - lua_helpers.setupState(&lua); + try lua_helpers.setupState(&lua); var state: Package.State = .empty; defer state.deinit(gpa); diff --git a/src/actions/install.zig b/src/actions/install.zig index 81a32c8..2f2baae 100644 --- a/src/actions/install.zig +++ b/src/actions/install.zig @@ -68,7 +68,7 @@ pub fn install( var lua: zlua.State = .{ .gpa = gpa }; try lua.new(0); defer lua.close(); - lua_helpers.setupState(&lua); + try lua_helpers.setupState(&lua); var state: Package.State = .empty; defer state.deinit(gpa); diff --git a/src/error_wrapper.lua b/src/error_wrapper.lua new file mode 100644 index 0000000..8f95768 --- /dev/null +++ b/src/error_wrapper.lua @@ -0,0 +1,13 @@ +---Wraps `native` functions so they can safely return +--- errors and defer with longjump after cleanup +---@param native fun(...: any): (any, string?) +---@return fun(...: any): any +return function(native) + return function(...) + local value, failure = native(...) + if failure ~= nil then + error(failure, 2) + end + return value + end +end diff --git a/src/lua_helpers.zig b/src/lua_helpers.zig index 8e2363d..bede34c 100644 --- a/src/lua_helpers.zig +++ b/src/lua_helpers.zig @@ -8,9 +8,13 @@ pub fn lua_pkg(state: ?*zlua.LuaState) callconv(.c) c_int { return 1; } -pub fn setupState(lua: *const zlua.State) void { +pub fn setupState(lua: *const zlua.State) !void { lua.requiref("_G", zlua.Open.base, true); + try lua.loadBuffer(@embedFile("error_wrapper.lua"), "@packa_lua_error_wrapper"); + try lua.pcall(0, 1, 0); + lua.setField(zlua.REGISTRYINDEX, "packa.checked"); + lua.setGlobal("load"); lua.pushNil(); lua.setGlobal("loadfile"); @@ -25,3 +29,48 @@ pub fn setupState(lua: *const zlua.State) void { })); lua.setGlobal("platform"); } + +/// Wraps a native function with error_wrapper.lua +pub fn pushChecked( + lua: *const zlua.State, + native: zlua.CFunction, + context: ?*anyopaque, +) !void { + std.debug.assert(lua.getField(zlua.REGISTRYINDEX, "packa.checked") == .function); + + var upvalues: usize = 0; + if (context) |ctx| { + lua.pushLightUserdata(ctx); + upvalues += 1; + } + lua.pushCClosure(native, upvalues); + try lua.pcall(1, 1, 0); +} + +const TestContext = struct { + cleaned: bool = false, +}; + +fn testFailure(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: zlua.State = .{ .inner = state.? }; + const context: *TestContext = @ptrCast(@alignCast(lua.toUserdata(lua.upvalueIndex(1)))); + defer context.cleaned = true; + + lua.pushNil(); + _ = lua.pushLString("native failure"); + return 2; +} + +test pushChecked { + var lua: zlua.State = .{ .gpa = std.testing.allocator }; + try lua.new(0); + defer lua.close(); + + try setupState(&lua); + + var context: TestContext = .{}; + try pushChecked(&lua, testFailure, &context); + try std.testing.expectError(zlua.Error.Run, lua.pcall(0, 0, 0)); + try std.testing.expect(context.cleaned); + try std.testing.expectEqualStrings("native failure", lua.toLString(-1)); +}