From 8bfce722f4469f29e49b8cdfd9350fb59199ef14 Mon Sep 17 00:00:00 2001 From: Nathan Craddock Date: Sat, 9 May 2026 22:02:56 -0600 Subject: [PATCH 1/2] Enable stack traces in api check For Lua 5.1, 5.2, 5.3, 5.4, and 5.5, this configures the Lua "api check" to use Zig's std.debug.assert() instead of C's assert() from . This includes stack traces which makes the assert a million times more helpful. LuaJIT does not use assert() so this does not work for LuaJIT, but this commit does enable basic api check support for LuaJIT. Enables these stack trace assertions by default. Users can opt-out by setting apicheck_type to .default when adding ziglua as a dependency. --- build.zig | 14 ++++++++++++-- build/include/assert.h | 6 ++++++ build/lua.zig | 14 +++++++++++--- build/luajit.zig | 18 +++++++++++++++++- src/lib.zig | 6 ++++++ 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 build/include/assert.h diff --git a/build.zig b/build.zig index e4eabc9..cb635cb 100644 --- a/build.zig +++ b/build.zig @@ -28,6 +28,13 @@ pub const ApiCheck = enum { off, }; +pub const ApiCheckType = enum { + /// Apicheck with Zig stack traces for supported Lua verions. + stack_trace, + /// Default Lua api checks. + basic, +}; + pub fn build(b: *Build) void { // Remove the default install and uninstall steps b.top_level_steps = .{}; @@ -42,7 +49,9 @@ pub fn build(b: *Build) void { const luau_use_4_vector = b.option(bool, "luau_use_4_vector", "Build Luau to use 4-vectors instead of the default 3-vector.") orelse false; const lua_user_h = b.option(Build.LazyPath, "lua_user_h", "Lazy path to user supplied c header file") orelse null; const additional_system_headers = b.option([]Build.LazyPath, "additional_system_headers", "Slice of Lazy paths to additional system headers to include when building Lua") orelse null; - const api_check = b.option(ApiCheck, "apicheck", "Enable parameter checks in the Lua API") orelse .debug; + const api_check = b.option(ApiCheck, "apicheck", "Enable parameter check assertions in the Lua API") orelse .debug; + // TODO: if the stack traces go well and don't error, maybe remove this. Why would someone not want the stack trace? + const api_check_type = b.option(ApiCheckType, "apicheck_type", "Whether apicheck should show stack traces or only abort") orelse .stack_trace; if (lang == .luau and shared) { std.debug.panic("Luau does not support compiling or loading shared modules", .{}); @@ -108,7 +117,7 @@ pub fn build(b: *Build) void { t.mod.linkSystemLibrary(system_library_name, .{ .preferred_link_mode = link_mode }); } else if (b.lazyDependency(@tagName(lang), .{})) |upstream| { const lib = switch (lang) { - .luajit => luajit_setup.configure(b, target, optimize, upstream, shared), + .luajit => luajit_setup.configure(b, target, optimize, upstream, shared, api_check), .luau => luau_setup.configure(b, target, optimize, upstream, luau_use_4_vector), else => lua_setup.configure(b, target, optimize, upstream, .{ .lang = lang, @@ -116,6 +125,7 @@ pub fn build(b: *Build) void { .library_name = library_name, .lua_user_h = lua_user_h, .api_check = api_check, + .api_check_type = api_check_type, }), }; diff --git a/build/include/assert.h b/build/include/assert.h new file mode 100644 index 0000000..3c7a192 --- /dev/null +++ b/build/include/assert.h @@ -0,0 +1,6 @@ +// This will be included when is included when LUA_USE_APICHECK is +// defined. Undefines any assert that may have already been defined, and then +// defines assert to be the zlua_assert function that shows a stack trace. +#undef assert +extern void zlua_assert(int e); +#define assert(e) zlua_assert((e) ? 1 : 0) diff --git a/build/lua.zig b/build/lua.zig index 9b5f0c3..92bbfb2 100644 --- a/build/lua.zig +++ b/build/lua.zig @@ -3,11 +3,13 @@ const std = @import("std"); const Build = std.Build; const Step = std.Build.Step; -const applyPatchToFile = @import("utils.zig").applyPatchToFile; +const utils = @import("utils.zig"); +const applyPatchToFile = utils.applyPatchToFile; const build = @import("../build.zig"); const ApiCheck = build.ApiCheck; +const ApiCheckType = build.ApiCheckType; const Language = build.Language; pub const Options = struct { @@ -16,6 +18,7 @@ pub const Options = struct { library_name: []const u8, lua_user_h: ?Build.LazyPath, api_check: ApiCheck, + api_check_type: ApiCheckType, }; pub fn configure( @@ -56,6 +59,8 @@ pub fn configure( const user_header = "user.h"; + const apicheck_enabled = opts.api_check == .on or (opts.api_check == .debug and optimize == .debug); + const flags = [_][]const u8{ // Standard version used in Lua Makefile "-std=gnu99", @@ -69,12 +74,12 @@ pub fn configure( }, // Enable api check - if (opts.api_check == .on or (opts.api_check == .debug and optimize == .debug)) "-DLUA_USE_APICHECK" else "", + if (apicheck_enabled) "-DLUA_USE_APICHECK" else "", // Build as DLL for windows if shared if (target.result.os.tag == .windows and shared) "-DLUA_BUILD_AS_DLL" else "", - if (lua_user_h) |_| b.fmt("-DLUA_USER_H=\"{s}\"", .{user_header}) else "", + if (lua_user_h != null) b.fmt("-DLUA_USER_H=\"{s}\"", .{user_header}) else "", if (target.result.cpu.arch == .wasm32) "-flto" else "", }; @@ -110,6 +115,9 @@ pub fn configure( library.installHeader(upstream.path("src/lauxlib.h"), "lauxlib.h"); library.installHeader(upstream.path("src/luaconf.h"), "luaconf.h"); + // This adds build/include/assert.h to the include path so Lua finds it before the system . + if (apicheck_enabled and opts.api_check_type == .stack_trace) library.root_module.addIncludePath(b.path("build/include")); + if (lua_user_h) |user_h| { library.root_module.addIncludePath(user_h.dirname()); library.installHeader(user_h, user_header); diff --git a/build/luajit.zig b/build/luajit.zig index cd0ee1e..9a97b49 100644 --- a/build/luajit.zig +++ b/build/luajit.zig @@ -3,9 +3,18 @@ const std = @import("std"); const Build = std.Build; const Step = std.Build.Step; +const ApiCheck = @import("../build.zig").ApiCheck; + const applyPatchToFile = @import("utils.zig").applyPatchToFile; -pub fn configure(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, shared: bool) *Step.Compile { +pub fn configure( + b: *Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + upstream: *Build.Dependency, + shared: bool, + api_check: ApiCheck, +) *Step.Compile { // TODO: extract this to the main build function because it is shared between all specialized build functions const lib = b.createModule(.{ @@ -227,12 +236,19 @@ pub fn configure(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin. library.root_module.addIncludePath(recdef_header.dirname()); library.root_module.addIncludePath(folddef_header.dirname()); + const flags = [_][]const u8{ + // Enable api check + // TODO: can we support zig stack traces for LuaJIT too? + if (api_check == .on or (api_check == .debug and optimize == .debug)) "-DLUA_USE_APICHECK" else "", + }; + lib.addCSourceFiles(.{ .root = .{ .dependency = .{ .dependency = upstream, .sub_path = "", } }, .files = &luajit_vm, + .flags = &flags, }); lib.sanitize_c = .off; diff --git a/src/lib.zig b/src/lib.zig index 1b45fc9..526c994 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -111,6 +111,12 @@ extern "c" fn zig_registerAssertionHandler() void; /// This function is defined in luau.cpp and ensures Zig uses the correct free when compiling luau code extern "c" fn zig_luau_free(ptr: *anyopaque) void; +/// Called from C for Lua function parameter check assertions when LUA_USE_APICHECK is defined. +/// Essentially, this is used in place of C's assert so stack traces are shown. +export fn zlua_assert(ok: bool) void { + std.debug.assert(ok); +} + const Allocator = std.mem.Allocator; // Types From 9c4c564085197c16abf375d958fa4516d2a79fd1 Mon Sep 17 00:00:00 2001 From: Nathan Craddock Date: Wed, 3 Jun 2026 11:24:33 -0600 Subject: [PATCH 2/2] Cleanup 0.14.0 support code --- src/lib.zig | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 526c994..f1162d8 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -737,12 +737,8 @@ pub const Lua = opaque { } else if (nsize == 0) { return null; } else { - const builtin = @import("builtin"); // FIXME: remove when zig-0.15 is released and 0.14 can be dropped // ptr is null, allocate a new block of memory - const new_ptr = (if (builtin.zig_version.major == 0 and builtin.zig_version.minor < 15) - allocator_ptr.alignedAlloc(u8, alignment, nsize) - else - allocator_ptr.alignedAlloc(u8, .fromByteUnits(alignment), nsize)) catch return null; + const new_ptr = allocator_ptr.alignedAlloc(u8, .fromByteUnits(alignment), nsize) catch return null; return new_ptr.ptr; } }