From 576891dc2ec4d917932a4c396471d4bbbad90c8e Mon Sep 17 00:00:00 2001 From: T Date: Wed, 27 May 2026 08:04:04 -0600 Subject: Finish the hard parts of the lua makeover - bundle luarocks source in the panto binary - bootstrap process (intended for first `panto` run): - make ~/.local/share/panto/... - write out luarocks sources into it - run luarocks to install luv - new `panto bootstrap` command just runs the bootstrap - `panto bootstrap --force` removes everything and re-bootstraps - new `panto lua` command just runs panto's embedded lua --- build/gen_lua_anchor.zig | 144 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 build/gen_lua_anchor.zig (limited to 'build/gen_lua_anchor.zig') diff --git a/build/gen_lua_anchor.zig b/build/gen_lua_anchor.zig new file mode 100644 index 0000000..1143c2c --- /dev/null +++ b/build/gen_lua_anchor.zig @@ -0,0 +1,144 @@ +//! 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; +} -- cgit v1.3