//! Build-time codegen: scan Lua's `lua.h` and `lauxlib.h` for every //! `LUA_API` / `LUALIB_API` function declaration, and emit a C source //! file that takes the address of each into a single `__attribute__ //! ((used))` array. //! //! Result: the linker can't drop those functions even under LTO + //! gc-sections, because they're reachable from the surviving array. //! Combined with `rdynamic = true` on the executable, every Lua API //! function ends up in the dynamic symbol table for C rocks (luv.so //! etc.) to resolve at `dlopen` time. //! //! Invoked from `build.zig`: //! //! gen-lua-anchor const std = @import("std"); pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); const io = init.io; var args = init.minimal.args.iterate(); defer args.deinit(); _ = args.next(); const src_dir = args.next() orelse return error.MissingSrcDir; const out_path = args.next() orelse return error.MissingOutPath; var names: std.array_list.Managed([]const u8) = .init(arena); try collectFrom(arena, io, src_dir, "lua.h", &names); try collectFrom(arena, io, src_dir, "lauxlib.h", &names); // Deduplicate via a string set. var seen: std.StringHashMapUnmanaged(void) = .empty; try seen.ensureTotalCapacity(arena, @intCast(names.items.len * 2)); var unique: std.array_list.Managed([]const u8) = .init(arena); for (names.items) |n| { const gop = seen.getOrPutAssumeCapacity(n); if (!gop.found_existing) try unique.append(n); } std.mem.sort([]const u8, unique.items, {}, lessThanString); var out_file = try std.Io.Dir.cwd().createFile(io, out_path, .{}); defer out_file.close(io); var buf: [4096]u8 = undefined; var writer = out_file.writer(io, &buf); const w = &writer.interface; try w.writeAll( \\/* Auto-generated by build/gen_lua_anchor.zig. Do not edit. \\ * \\ * Holds a reference to every Lua public API function so the \\ * linker can't drop them under LTO + gc-sections. C rocks \\ * loaded at runtime (luv.so etc.) resolve these symbols \\ * against the host process's symbol table; combined with \\ * rdynamic on the executable, this keeps the whole API \\ * surface exported. \\ */ \\ \\#include "lua.h" \\#include "lauxlib.h" \\ \\__attribute__((used, visibility("default"))) \\const void *const _panto_lua_api_anchor[] = { \\ ); for (unique.items) |n| { try w.print(" (const void *)&{s},\n", .{n}); } try w.writeAll("};\n"); try w.flush(); } fn lessThanString(_: void, a: []const u8, b: []const u8) bool { return std.mem.lessThan(u8, a, b); } fn collectFrom( arena: std.mem.Allocator, io: std.Io, src_dir: []const u8, filename: []const u8, out: *std.array_list.Managed([]const u8), ) !void { const path = try std.fs.path.join(arena, &.{ src_dir, filename }); const contents = try std.Io.Dir.cwd().readFileAlloc(io, path, arena, .unlimited); // Walk line by line. When we hit a line starting with `LUA_API`, // accumulate continuation lines until we see `);`. From the joined // string, extract the function name \u2014 either parenthesized // (`(lua_foo)`) or bare (`lua_foo(...)`). var line_it = std.mem.splitScalar(u8, contents, '\n'); while (line_it.next()) |first_line| { const trimmed = std.mem.trimStart(u8, first_line, " \t"); if (!std.mem.startsWith(u8, trimmed, "LUA_API") and !std.mem.startsWith(u8, trimmed, "LUALIB_API")) continue; var joined: std.array_list.Managed(u8) = .init(arena); try joined.appendSlice(first_line); while (std.mem.indexOf(u8, joined.items, ");") == null) { const next_line = line_it.next() orelse break; try joined.append(' '); try joined.appendSlice(next_line); } if (extractName(joined.items)) |name| { try out.append(try arena.dupe(u8, name)); } } } /// Return the function-name portion of a Lua API declaration. Handles /// both `LUA_API (lua_foo) (...)` and `LUA_API lua_foo(...)`. fn extractName(decl: []const u8) ?[]const u8 { // Find a `(name)` token first. var i: usize = 0; while (i + 1 < decl.len) : (i += 1) { if (decl[i] != '(') continue; const end = std.mem.indexOfScalar(u8, decl[i + 1 ..], ')') orelse return null; const candidate = decl[i + 1 .. i + 1 + end]; if (isApiName(candidate)) return candidate; // Skip this candidate; try further on. } // Fall back: bare-name pattern (no parens around the name). var j: usize = 0; while (j < decl.len) : (j += 1) { if (j + 4 > decl.len) break; if (!std.mem.eql(u8, decl[j .. j + 4], "lua_") and !std.mem.eql(u8, decl[j .. j + 5], "luaL_")) continue; var end = j; while (end < decl.len and (std.ascii.isAlphanumeric(decl[end]) or decl[end] == '_')) end += 1; return decl[j..end]; } return null; } fn isApiName(s: []const u8) bool { if (s.len < 4) return false; const ok_prefix = std.mem.startsWith(u8, s, "lua_") or std.mem.startsWith(u8, s, "luaL_"); if (!ok_prefix) return false; for (s) |ch| { if (!std.ascii.isAlphanumeric(ch) and ch != '_') return false; } return true; }