diff --git a/.gitignore b/.gitignore index f451200..e34424a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.ninja_deps /tools/*/lua.def /compile/lua/*.c +/.claude/ diff --git a/README.md b/README.md index 6b359c8..71667f4 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,7 @@ Run lua file. > luamake test Equivalent to `luamake lua test.lua` + +## Documentation + +See [`skills/SKILL.md`](skills/SKILL.md) for the full guide on writing `make.lua`. diff --git a/scripts/lua_embed.lua b/scripts/lua_embed.lua index a1fefd9..42510cf 100644 --- a/scripts/lua_embed.lua +++ b/scripts/lua_embed.lua @@ -2,25 +2,23 @@ -- Helper module for the lm:lua_embed target (registered in writer.lua). -- Provides: config file generation and input file collection. -local fs = require "bee.filesystem" -local fsutil = require "fsutil" +local fs = require "bee.filesystem" +local fsutil = require "fsutil" local pathutil = require "pathutil" --- Directory containing lua_embed_gen.lua, lua_embed.h, etc. local EMBED_DIR = fsutil.join(package.procdir, "scripts", "lua_embed") local GEN_SCRIPT = fsutil.join(EMBED_DIR, "lua_embed_gen.lua") -local HEADER = fsutil.join(EMBED_DIR, "lua_embed.h") - -local BEE_GLUE = fsutil.join(EMBED_DIR, "bee_glue.c") +local BEE_GLUE = fsutil.join(EMBED_DIR, "bee_glue.c") local m = {} m.GEN_SCRIPT = GEN_SCRIPT -m.HEADER = HEADER -m.EMBED_DIR = EMBED_DIR m.BEE_GLUE = BEE_GLUE --- Recursively collect files under dirpath. --- When lua_only=true, only *.lua files are collected. +local function resolve_path(rootdir, p) + return rootdir and pathutil.tostr(rootdir, p) or p +end + +-- Recursively collect files under dirpath (lua_only filters to *.lua). function m.scan_inputs(dirpath, lua_only) local result = {} local root = fs.path(dirpath) @@ -42,142 +40,157 @@ function m.scan_inputs(dirpath, lua_only) return result end --- 将 dir/file 路径相对于 rootdir 解析为绝对路径 -local function resolve_path(rootdir, p) - if rootdir then - return pathutil.tostr(rootdir, p) +-- Serialize one entry { dir=, prefix=, pattern= } or { file=, name= }. +local function serialize_entry(e, rootdir) + local parts = {} + if e.dir then + parts[#parts+1] = "dir = " .. string.format("%q", resolve_path(rootdir, e.dir)) + if e.prefix and e.prefix ~= "" then + parts[#parts+1] = "prefix = " .. string.format("%q", e.prefix) + end + if e.pattern then + parts[#parts+1] = "pattern = " .. string.format("%q", e.pattern) + end + elseif e.file then + parts[#parts+1] = "file = " .. string.format("%q", resolve_path(rootdir, e.file)) + parts[#parts+1] = "name = " .. string.format("%q", + assert(e.name, "entry with 'file' requires 'name'")) end - return p + return " { " .. table.concat(parts, ", ") .. " }," end --- Write a config.lua file for lua_embed_gen.lua to dofile(). --- attribute: the lm:lua_embed attribute table. --- rootdir: (optional) root directory for resolving relative paths in attribute. --- Returns the path of the written config file. --- --- 只在内容实际变化时才写入文件,避免时间戳更新导致 Ninja 不必要的重建。 +-- A group uses lua_mode (module-name scanning) when: +-- 1. any dir-entry in the group has 'pattern', or +-- 2. bee_glue is enabled and this is the 'preload' group +-- (the _PRELOAD hard-coded contract requires lua module names, +-- not raw filenames, as keys). +local function group_lua_mode(grp, grp_name, bee_glue) + if bee_glue and grp_name == "preload" then return true end + for _, e in ipairs(grp) do + if type(e) == "table" and e.pattern then return true end + end + return false +end + +-- Write config.lua for lua_embed_gen.lua. +-- Groups are derived from attribute.data string keys, sorted alphabetically. +-- Writes only when content changes (avoids unnecessary Ninja rebuilds). function m.write_config(outdir, attribute, rootdir) fs.create_directories(outdir) local config_path = outdir .. "/config.lua" - local lines = {} - lines[#lines+1] = "-- auto-generated lua_embed config" - lines[#lines+1] = "return {" - - if attribute.bee_glue then - -- bee_glue 文件路径写入配置,生成器会将其嵌入 lua_embed.c 中 - -- (通过 lua_embed_get_main() 访问,不暴露到 data_table) - local main_path = resolve_path(rootdir, attribute.bee_glue) - lines[#lines+1] = string.format(" main = %q,", main_path) + local lines = { + "-- auto-generated lua_embed config", + "return {", + " groups = {", + } + + local data = attribute.data or {} + + -- Normalize bee_glue: accept only boolean / nil. Anything else (string, + -- number, table, ...) is almost certainly a user mistake, so fail fast + -- with a clear message instead of silently (mis)interpreting it. + local bee_glue = attribute.bee_glue + assert(bee_glue == nil or type(bee_glue) == "boolean", + string.format("lua_embed: bee_glue must be a boolean, got %s", type(bee_glue))) + + -- When bee_glue is enabled, bee_glue.c references lua_embed.{main,preload,data} + -- by name. These groups must be declared (even if empty) so the generated + -- lua_embed_bundle struct contains the corresponding fields; otherwise + -- bee_glue.c will fail to compile with "struct has no member" errors. + if bee_glue == true then + for _, required in ipairs({ "main", "preload", "data" }) do + assert(type(data[required]) == "table", + string.format( + "lua_embed: bee_glue requires group %q to be defined (use an empty table {} if unused)", + required)) + end end - if attribute.bytecode then - lines[#lines+1] = " bytecode = true," + local order = {} + for k in pairs(data) do + if type(k) == "string" then order[#order+1] = k end end - - -- preload - lines[#lines+1] = " preload = {" - for _, e in ipairs(attribute.preload or {}) do - if e.dir then - local dir = resolve_path(rootdir, e.dir) - local s = " { dir = " .. string.format("%q", dir) - if e.prefix and e.prefix ~= "" then - s = s .. ", prefix = " .. string.format("%q", e.prefix) - end - if e.pattern then - s = s .. ", pattern = " .. string.format("%q", e.pattern) - end - s = s .. " }," - lines[#lines+1] = s - elseif e.file then - local file = resolve_path(rootdir, e.file) - lines[#lines+1] = " { file = " .. string.format("%q", file) - .. ", name = " .. string.format("%q", - assert(e.name, "preload file entry requires 'name'")) - .. " }," + table.sort(order) + + for _, grp_name in ipairs(order) do + local grp = data[grp_name] + if type(grp) ~= "table" then goto continue end + assert(grp_name:match("^[%a_][%w_]*$"), + string.format("lua_embed group name %q is not a valid C identifier", grp_name)) + + lines[#lines+1] = " {" + lines[#lines+1] = string.format(" name = %q,", grp_name) + if grp.bytecode then + lines[#lines+1] = " bytecode = true," end - end - lines[#lines+1] = " }," - - -- data - lines[#lines+1] = " data = {" - for _, e in ipairs(attribute.data or {}) do - if e.dir then - local dir = resolve_path(rootdir, e.dir) - local s = " { dir = " .. string.format("%q", dir) - if e.prefix then - s = s .. ", prefix = " .. string.format("%q", e.prefix) + if group_lua_mode(grp, grp_name, bee_glue == true) then + lines[#lines+1] = " lua_mode = true," + end + lines[#lines+1] = " entries = {" + for _, e in ipairs(grp) do + if type(e) == "string" then + local abspath = resolve_path(rootdir, e) + local fname = abspath:match("[^/\\]+$") or e + lines[#lines+1] = " { file = " + .. string.format("%q", abspath) + .. ", name = " .. string.format("%q", fname) .. " }," + elseif type(e) == "table" then + lines[#lines+1] = serialize_entry(e, rootdir) end - s = s .. " }," - lines[#lines+1] = s - elseif e.file then - local file = resolve_path(rootdir, e.file) - lines[#lines+1] = " { file = " .. string.format("%q", file) - .. ", name = " .. string.format("%q", - assert(e.name, "data file entry requires 'name'")) - .. " }," end + lines[#lines+1] = " }," + lines[#lines+1] = " }," + ::continue:: end - lines[#lines+1] = " }," + lines[#lines+1] = " }," lines[#lines+1] = "}" - lines[#lines+1] = "" -- 末尾换行 + lines[#lines+1] = "" local new_content = table.concat(lines, "\n") - - -- 只在内容变化时写入,避免时间戳更新导致不必要的重建 local old_f = io.open(config_path, "rb") if old_f then - local old_content = old_f:read "*a" + local old = old_f:read "*a" old_f:close() - if old_content == new_content then - return config_path - end + if old == new_content then return config_path end end - local f = assert(io.open(config_path, "wb"), - "cannot write config: " .. config_path) + local f = assert(io.open(config_path, "wb"), "cannot write config: " .. config_path) f:write(new_content) f:close() return config_path end --- Collect all input files (for ninja dependency tracking). --- 除了 Lua 源文件和生成器脚本外,config.lua 也作为输入被追踪, --- 这样当用户修改 lua_embed 选项(bytecode/pattern/prefix/bee_glue 等)时, --- Ninja 能检测到配置变化并重新生成 lua_embed.c。 --- write_config 已做内容比对,内容不变时不会更新文件时间戳, --- 因此不会导致不必要的重建。 --- rootdir: (optional) root directory for resolving relative paths in attribute. +-- Collect all input files for Ninja dependency tracking. function m.collect_inputs(attribute, rootdir, config_path) local inputs = { GEN_SCRIPT } if config_path then inputs[#inputs+1] = config_path end - for _, e in ipairs(attribute.preload or {}) do - if e.dir then - local dir = resolve_path(rootdir, e.dir) - for _, p in ipairs(m.scan_inputs(dir, true)) do - inputs[#inputs+1] = p - end - elseif e.file then - inputs[#inputs+1] = resolve_path(rootdir, e.file) - end - end - for _, e in ipairs(attribute.data or {}) do - if e.dir then - local dir = resolve_path(rootdir, e.dir) - for _, p in ipairs(m.scan_inputs(dir, false)) do - inputs[#inputs+1] = p + + local data = attribute.data or {} + local bee_glue = attribute.bee_glue == true + for grp_name, grp in pairs(data) do + if type(grp_name) ~= "string" or type(grp) ~= "table" then goto continue end + local lua_mode = group_lua_mode(grp, grp_name, bee_glue) + for _, e in ipairs(grp) do + if type(e) == "string" then + inputs[#inputs+1] = resolve_path(rootdir, e) + elseif type(e) == "table" then + if e.dir then + local lua_only = lua_mode or (e.pattern ~= nil) + for _, p in ipairs(m.scan_inputs(resolve_path(rootdir, e.dir), lua_only)) do + inputs[#inputs+1] = p + end + elseif e.file then + inputs[#inputs+1] = resolve_path(rootdir, e.file) + end end - elseif e.file then - inputs[#inputs+1] = resolve_path(rootdir, e.file) end + ::continue:: end - -- bee_glue 文件也需要追踪(会嵌入到 lua_embed.c 中) - if attribute.bee_glue then - inputs[#inputs+1] = resolve_path(rootdir, attribute.bee_glue) - end + table.sort(inputs) return inputs end diff --git a/scripts/lua_embed/bee_glue.c b/scripts/lua_embed/bee_glue.c index aef35ae..d90ae83 100644 --- a/scripts/lua_embed/bee_glue.c +++ b/scripts/lua_embed/bee_glue.c @@ -1,12 +1,16 @@ /* bee_glue.c -- static glue layer for bee.lua runtime - * This file is NOT auto-generated; it is a static part of the lua_embed module. - * It provides _bee_preload_module() and _bee_main() which rely on the - * generated lua_embed.c for actual embedded data. + * Not auto-generated; compiled together with the generated lua_embed.c. + * + * Hard-coded group conventions: + * lua_embed.preload -- registered into package.preload + * lua_embed.main -- first entry is the main script + * lua_embed.data -- exposed via require("bee.embed") */ -#include "lua_embed.h" +#include "lua_embed_data.h" #include #include +#include #if defined(_WIN32) # define LUA_EMBED_EXPORT __declspec(dllexport) @@ -14,91 +18,112 @@ # define LUA_EMBED_EXPORT __attribute__((visibility("default"))) #endif -/* C helper: load a lua_embed_preload entry from a lightuserdata pointer. - * Signature: load_bytecode(lightuserdata entry) -> function - */ -static int load_bytecode(lua_State* L) { - const lua_embed_preload* e = (const lua_embed_preload*)lua_touserdata(L, 1); - if (luaL_loadbuffer(L, e->data, e->size, e->name) != LUA_OK) return lua_error(L); +/* ── preload ──────────────────────────────────────────────────────────────── */ + +/* Load a lua_embed_entry (passed as lightuserdata) and return a function. */ +static int load_entry(lua_State* L) { + const lua_embed_entry* e = (const lua_embed_entry*)lua_touserdata(L, 1); + if (luaL_loadbuffer(L, e->data, e->size, e->name) != LUA_OK) + return lua_error(L); return 1; } -/* preload_loader: a fixed Lua factory function (always embedded as source). - * Receives (load_bytecode, entry) and returns a closure suitable - * for _PRELOAD[modname]. - */ -static const char preload_loader_src[] = - "local load_bytecode, entry = ...\n" +/* Lua factory: returns a package.preload-compatible loader closure. */ +static const char preload_factory_src[] = + "local load_entry, entry = ...\n" "return function(modname)\n" - " return load_bytecode(entry)(modname)\n" + " return load_entry(entry)(modname)\n" "end\n"; -/* bee.embed module: exposes embedded data files to Lua. - * require("bee.embed") returns a table. Indexing it by name returns the - * embedded string (or nil). The result is cached in the table itself via - * rawset so the __index metamethod fires at most once per key. - */ +/* ── bee.embed ────────────────────────────────────────────────────────────── */ + +/* __index(t, name): look up name in lua_embed.data, cache result in t. + * + * Design note: the lookup is a deliberate O(N) linear scan (strcmp per entry). + * Each successful key is cached into the module table via rawset below, so + * subsequent accesses hit Lua's native hash table in O(1). Total cost over a + * process lifetime is bounded by O(N * distinct_keys_accessed), which in + * practice is negligible for the small-to-medium resource sets this API is + * intended for. Switching to a generated perfect hash or a sorted+bsearch + * table is possible but would force the generator to emit additional data + * structures and complicate the contract between lua_embed.c and this glue; + * do that only when profiling shows this scan is a real bottleneck (e.g. + * thousands of entries *and* many cold accesses). For very large asset sets, + * splitting the data into multiple groups or moving them to a filesystem / + * archive-backed loader is usually the better answer. */ static int l_embed_index(lua_State* L) { - /* L: table, name */ const char* name = luaL_checkstring(L, 2); - const lua_embed_data* d = lua_embed_get_data(name); - if (!d) { - lua_pushnil(L); - return 1; - } + const lua_embed_entry* e; + for (e = lua_embed.data; e->name != NULL; e++) { + if (strcmp(e->name, name) == 0) { #if LUA_VERSION_NUM >= 505 - /* Zero-copy: data arrays end with a sentinel '\0' (see lua_embed_gen.lua). - * falloc=NULL tells Lua the buffer is static and must not be freed. */ - lua_pushexternalstring(L, d->data, d->size, NULL, NULL); + lua_pushexternalstring(L, e->data, e->size, NULL, NULL); #else - lua_pushlstring(L, d->data, d->size); + lua_pushlstring(L, e->data, e->size); #endif - /* cache: rawset(table, name, string) so __index won't fire again */ - lua_pushvalue(L, 2); /* key */ - lua_pushvalue(L, -2); /* value */ - lua_rawset(L, 1); + /* cache: rawset(t, name, value) */ + lua_pushvalue(L, 2); + lua_pushvalue(L, -2); + lua_rawset(L, 1); + return 1; + } + } + lua_pushnil(L); return 1; } static int luaopen_bee_embed(lua_State* L) { - lua_newtable(L); /* module table (acts as its own cache) */ - lua_newtable(L); /* metatable */ + lua_newtable(L); /* module table */ + lua_newtable(L); /* metatable */ lua_pushcfunction(L, l_embed_index); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2); return 1; } +/* ── exported entry points ────────────────────────────────────────────────── */ + +/* Host-driven contract (two independent atomic ops; call order is up to host): + * + * _bee_preload_module(L) install package.preload entries + bee.embed + * _bee_main(L) load + pcall the main[0] script + * + * The typical sequence is preload → main, so that require() inside the main + * script can see the embedded modules. These are intentionally kept separate + * so the host can: + * - preload once and run multiple chunks of its own, + * - override / inject entries into package.preload between the two calls, + * - run preload on worker lua_States that never execute main, + * - or run a self-contained main that does not need preload at all. + * Do NOT fold preload into _bee_main; callers rely on this separation. + */ + LUA_EMBED_EXPORT int _bee_preload_module(lua_State* L) { - const lua_embed_preload* e; + const lua_embed_entry* e; luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); - /* Register bee.embed */ lua_pushcfunction(L, luaopen_bee_embed); lua_setfield(L, -2, "bee.embed"); - /* Load the preload_loader factory once, outside the loop */ - if (luaL_loadbuffer(L, preload_loader_src, sizeof(preload_loader_src) - 1, "=preload_loader") != LUA_OK) + if (luaL_loadbuffer(L, preload_factory_src, sizeof(preload_factory_src) - 1, + "=preload_factory") != LUA_OK) return lua_error(L); - for (e = lua_embed_get_preload(); e->name != NULL; e++) { - /* Reuse the factory function at the top of the stack */ - lua_pushvalue(L, -1); - lua_pushcfunction(L, load_bytecode); + for (e = lua_embed.preload; e->name != NULL; e++) { + lua_pushvalue(L, -1); /* dup factory */ + lua_pushcfunction(L, load_entry); lua_pushlightuserdata(L, (void*)e); - lua_call(L, 2, 1); /* -> lua closure */ + lua_call(L, 2, 1); /* -> loader closure */ lua_setfield(L, -3, e->name); } - lua_pop(L, 2); /* pop factory + _PRELOAD table */ + lua_pop(L, 2); /* factory, _PRELOAD */ return 0; } LUA_EMBED_EXPORT int _bee_main(lua_State* L) { - const lua_embed_data* f; - _bee_preload_module(L); - f = lua_embed_get_main(); - if (!f) { + const lua_embed_entry* m = lua_embed.main; + if (m == NULL || m->name == NULL) { lua_pushstring(L, "lua_embed: no main entry configured"); return lua_error(L); } - if (luaL_loadbuffer(L, f->data, f->size, f->name) != LUA_OK) + if (luaL_loadbuffer(L, m->data, m->size, m->name) != LUA_OK) return lua_error(L); if (lua_pcall(L, 0, 0, 0) != LUA_OK) return lua_error(L); diff --git a/scripts/lua_embed/lua_embed.h b/scripts/lua_embed/lua_embed.h deleted file mode 100644 index 5400d62..0000000 --- a/scripts/lua_embed/lua_embed.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -/* lua_embed.h -- runtime interface for embedded Lua files - * Generated data is linked into the executable. - * Include this header in any C/C++ file that needs to access embedded modules. - */ - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* A Lua module stored for injection into _PRELOAD. - * 'name' is the require() key, e.g. "http.httpc". - * 'data' points to Lua source code or bytecode; 'size' is its length in bytes. - * By default source code is embedded for cross-Lua-version compatibility; - * set bytecode=true in lm:lua_embed to embed bytecode instead (smaller, but - * requires the host luamake Lua version to match the target luaversion). - */ -typedef struct lua_embed_preload { - const char* name; - const char* data; - size_t size; -} lua_embed_preload; - -/* An arbitrary file stored as raw bytes (source or binary). - * 'name' is the lookup key chosen by the user. - * 'data' points to the raw content; 'size' is its length in bytes. - */ -typedef struct lua_embed_data { - const char* name; - const char* data; - size_t size; -} lua_embed_data; - -/* Returns a NULL-terminated array of all preload entries. */ -const lua_embed_preload* lua_embed_get_preload(void); - -/* Finds a data entry by name; returns NULL if not found. */ -const lua_embed_data* lua_embed_get_data(const char* name); - -/* Returns the embedded main entry point, or NULL if none was configured. - * The main entry is stored separately from the data table and cannot be - * accessed via lua_embed_get_data(). - */ -const lua_embed_data* lua_embed_get_main(void); - -#ifdef __cplusplus -} -#endif diff --git a/scripts/lua_embed/lua_embed_gen.lua b/scripts/lua_embed/lua_embed_gen.lua index 8e741be..8d21b70 100644 --- a/scripts/lua_embed/lua_embed_gen.lua +++ b/scripts/lua_embed/lua_embed_gen.lua @@ -1,23 +1,28 @@ -- lua_embed_gen.lua --- Called by luamake runlua to generate lua_embed.c +-- Called by luamake runlua to generate lua_embed.c and lua_embed_data.h -- Args: --- config_file : Lua file (dofile), describes preload/data/main options +-- config_file : Lua file (dofile), describes data groups -- output_c : path to write the generated lua_embed.c +-- (lua_embed_data.h is written alongside, in the same dir) + +local fs = require "bee.filesystem" local config_file = assert(arg[1], "arg[1]: config file required") local output_c = assert(arg[2], "arg[2]: output .c path required") +-- lua_embed_data.h is written to the same directory as output_c. +-- Use bee.filesystem to split the path so both '/' and '\\' work, and the +-- "no directory component" case (e.g. just "foo.c") naturally yields ".". +local output_c_path = fs.path(output_c) +local output_dir = output_c_path:parent_path() +if output_dir:string() == "" then + output_dir = fs.path(".") +end +local output_h = (output_dir / "lua_embed_data.h"):string() local cfg = assert(dofile(config_file)) --- bytecode=true 时使用 string.dump 生成字节码(体积更小、可隐藏源码), --- 但要求 luamake 宿主 Lua 版本与目标 luaversion 一致。 --- 默认 false,嵌入源码以保证跨 Lua 版本兼容。 -local use_bytecode = cfg.bytecode or false - -- ── helpers ────────────────────────────────────────────────────────────────── -local fs = require "bee.filesystem" - local function readfile(path) local f, err = io.open(path, "rb") if not f then error("cannot open: " .. path .. " (" .. tostring(err) .. ")") end @@ -42,7 +47,6 @@ local function emit_c_bytes(data) local len = #data if len == 0 then -- 空数据时输出一个哨兵字节,避免生成空初始化列表(标准 C 不允许) - -- 调用方记录的 size 仍为 0,运行时按长度读取不受影响 emit("0x00\n ") return end @@ -60,12 +64,60 @@ local function emit_c_bytes(data) end end -local function to_c_ident(s) - local ident = s:gsub("[^%w]", "_") - if ident:match("^[0-9]") then - ident = "_" .. ident +-- Map an arbitrary string to a valid C identifier. +-- Non [A-Za-z0-9_] characters are collapsed to '_', so distinct inputs can +-- collide (e.g. "foo.bar" and "foo_bar" both -> "foo_bar"; this is common +-- for Lua module names where '.' is a separator). To keep the mapping a +-- pure function *and* injective across a single run, we append a short +-- deterministic hash suffix when a collision is detected. +-- +-- Cache keeps the mapping stable for the same input inside one generation. +local to_c_ident +do + local str_to_id = {} -- original string -> produced C identifier + local id_to_str = {} -- produced C identifier -> original string (reverse) + + -- djb2-style hash; output is a short lowercase hex string. + local function short_hash(s) + local h = 5381 + for i = 1, #s do + h = (h * 33 + s:byte(i)) % 0x100000000 + end + return string.format("%08x", h) end - return ident + + local function sanitize(s) + local ident = s:gsub("[^%w]", "_") + if ident:match("^[0-9]") then + ident = "_" .. ident + end + return ident + end + + function to_c_ident(s) + local cached = str_to_id[s] + if cached then return cached end + local base = sanitize(s) + local id = base + local owner = id_to_str[id] + if owner ~= nil and owner ~= s then + -- collision: fall back to base + short hash of the original string. + id = base .. "_" .. short_hash(s) + -- If even that collides (astronomically unlikely), keep extending. + while id_to_str[id] ~= nil and id_to_str[id] ~= s do + id = id .. "_" .. short_hash(id) + end + end + str_to_id[s] = id + id_to_str[id] = s + return id + end +end + +-- validate that a group name is a valid C identifier +local function check_c_ident(name) + assert(name:match("^[%a_][%w_]*$"), + string.format("group name %q is not a valid C identifier", name)) end -- ── scan a directory and collect preload entries ────────────────────────────── @@ -73,14 +125,11 @@ end -- returns list of { modname, abspath } local function match_pattern(rel, pat) - -- pat uses '?' as placeholder for module path (slashes), suffix is literal local prefix, suffix = pat:match("^(.-)%?(.*)$") if not prefix or not suffix then return nil end - -- rel must start with prefix and end with suffix if rel:sub(1, #prefix) ~= prefix then return nil end if rel:sub(-#suffix) ~= suffix and #suffix > 0 then return nil end local mid = rel:sub(#prefix + 1, #suffix > 0 and -#suffix - 1 or #rel) - -- mid is the '?' part, e.g. "http/httpc" or "tui" return mid:gsub("/", ".") end @@ -95,9 +144,16 @@ local function sorted_entries(dir) return entries end -local function scan_preload_dir(dirpath, patterns, mod_prefix, result, seen) +local function scan_lua_dir(dirpath, patterns, mod_prefix, result, seen) local root = fs.path(dirpath) if not fs.exists(root) then return end + -- A unique token for this scan_lua_dir invocation. Two entries matched in + -- the *same* directory scan can be compared by pat_idx (mirroring Lua's + -- package.path priority). Across different scan_lua_dir calls we fall + -- back to "first one wins" (i.e. earlier entries in the config have + -- priority over later ones), because pat_idx values from different + -- pattern lists are not comparable. + local scan_token = {} local function recurse(base, rel_base) for _, entry in ipairs(sorted_entries(base)) do local name = entry:filename():string() @@ -105,23 +161,51 @@ local function scan_preload_dir(dirpath, patterns, mod_prefix, result, seen) if fs.is_directory(entry) then recurse(entry, rel) elseif name:match("%.lua$") then - local modname - for _, pat in ipairs(patterns) do + local modname, pat_idx + for i, pat in ipairs(patterns) do local m = match_pattern(rel, pat) if m then modname = (mod_prefix ~= "" and (mod_prefix .. "." .. m) or m) + pat_idx = i break end end if modname then local abspath = entry:string():gsub("\\", "/") - if seen[modname] then - io.write(string.format( - "[lua_embed] warning: preload %q overridden by %s\n", - modname, abspath)) + local prev = seen[modname] + if prev then + -- Duplicate module name. Deterministic tie-breaker: + -- * same scan + lower pat_idx -> new wins (mirrors + -- Lua's package.path left-to-right priority, + -- e.g. "?.lua" beats "?/init.lua") + -- * otherwise -> first one wins + -- Either way we *replace in place* rather than + -- appending, so the generated C never contains two + -- entries with the same name (which would collide in + -- to_c_ident()). + local new_wins = + prev.scan == scan_token and pat_idx < prev.pat_idx + if new_wins then + io.write(string.format( + "[lua_embed] warning: module %q: %s overrides %s\n", + modname, abspath, prev.path)) + result[prev.index] = { name = modname, path = abspath } + seen[modname] = { + index = prev.index, path = abspath, + pat_idx = pat_idx, scan = scan_token, + } + else + io.write(string.format( + "[lua_embed] warning: module %q: %s kept, %s skipped\n", + modname, prev.path, abspath)) + end + else + result[#result+1] = { name = modname, path = abspath } + seen[modname] = { + index = #result, path = abspath, + pat_idx = pat_idx, scan = scan_token, + } end - seen[modname] = true - result[#result+1] = { modname = modname, path = abspath } else io.write(string.format( "[lua_embed] warning: %s/%s does not match any pattern, skipped\n", @@ -133,62 +217,6 @@ local function scan_preload_dir(dirpath, patterns, mod_prefix, result, seen) recurse(root, "") end --- ── collect preload entries ─────────────────────────────────────────────────── - -local preload_map = {} -- modname → path (for override detection) -local preload_list = {} -- ordered { modname, path } - -for _, entry in ipairs(cfg.preload or {}) do - local patterns_str = entry.pattern or "?.lua;?/init.lua" - local patterns = {} - for p in patterns_str:gmatch("[^;]+") do - patterns[#patterns+1] = p - end - local mod_prefix = entry.prefix or "" - - if entry.dir then - local tmp_seen = {} - local tmp = {} - scan_preload_dir(entry.dir, patterns, mod_prefix, tmp, tmp_seen) - for _, e in ipairs(tmp) do - if preload_map[e.modname] then - io.write(string.format( - "[lua_embed] warning: preload %q overridden by %s\n", - e.modname, e.path)) - -- 更新已有条目的路径 - for _, existing in ipairs(preload_list) do - if existing.modname == e.modname then - existing.path = e.path - break - end - end - else - preload_list[#preload_list+1] = e - end - preload_map[e.modname] = e.path - end - elseif entry.file then - local modname = assert(entry.name, - "preload entry with 'file' requires 'name': " .. tostring(entry.file)) - local abspath = entry.file:gsub("\\", "/") - if preload_map[modname] then - io.write(string.format( - "[lua_embed] warning: preload %q overridden by %s\n", - modname, abspath)) - for _, e in ipairs(preload_list) do - if e.modname == modname then e.path = abspath; break end - end - else - preload_list[#preload_list+1] = { modname = modname, path = abspath } - end - preload_map[modname] = abspath - end -end - --- ── collect data entries ────────────────────────────────────────────────────── - -local data_list = {} -- { name, path } - local function scan_data_dir(dirpath, prefix, result) local root = fs.path(dirpath) if not fs.exists(root) then return end @@ -209,138 +237,165 @@ local function scan_data_dir(dirpath, prefix, result) recurse(root, "") end -for _, entry in ipairs(cfg.data or {}) do - if entry.dir then - scan_data_dir(entry.dir, entry.prefix or "", data_list) - elseif entry.file then - data_list[#data_list+1] = { - name = assert(entry.name, - "data entry with 'file' requires 'name': " .. tostring(entry.file)), - path = entry.file:gsub("\\", "/"), - } +-- ── process groups ──────────────────────────────────────────────────────────── +-- cfg.groups is an ordered list of { name, bytecode, entries[] } +-- each entry is { dir=, prefix=, pattern= } or { file=, name= } + +local function collect_group_files(group_cfg) + -- group_cfg.entries is the array part of the group table from config + -- group_cfg.lua_only: true means scan only .lua files (preload-style naming) + -- For simplicity: if entry has pattern field or dir with lua files → use lua scan + -- In the new unified model every entry is raw bytes; naming is caller's concern. + -- We use two sub-helpers based on whether 'pattern' key is present. + local result = {} + local seen = {} + for _, e in ipairs(group_cfg.entries) do + if e.dir then + if e.pattern ~= nil or group_cfg.lua_mode then + -- lua module scan + local patterns_str = e.pattern or "?.lua;?/init.lua" + local patterns = {} + for p in patterns_str:gmatch("[^;]+") do + patterns[#patterns+1] = p + end + scan_lua_dir(e.dir, patterns, e.prefix or "", result, seen) + else + scan_data_dir(e.dir, e.prefix or "", result) + end + elseif e.file then + result[#result+1] = { + name = assert(e.name, "entry with 'file' requires 'name'"), + path = e.file, + } + end end + return result end -- ── ensure output directory exists ─────────────────────────────────────────── -local outdir = output_c:match("^(.*)/[^/]+$") -if outdir then fs.create_directories(outdir) end +if output_dir:string() ~= "." then + fs.create_directories(output_dir) +end -- ── generate .c file ───────────────────────────────────────────────────────── emit('/* Auto-generated by lua_embed_gen.lua -- DO NOT EDIT */\n') -emit('#include "lua_embed.h"\n\n') - --- preload entries: embed source code or bytecode depending on cfg.bytecode -local preload_idents = {} -local seen_idents = {} -- 检测标识符冲突 -for _, e in ipairs(preload_list) do - local src = readfile(e.path) - local func, err = load(src, "@" .. e.path) - if not func then error("syntax error in " .. e.path .. ": " .. tostring(err)) end - local payload - if use_bytecode then - payload = string.dump(func) - else - payload = src - end - local id = "lep_" .. to_c_ident(e.modname) - if seen_idents[id] then - error(string.format( - "C identifier collision: %q and %q both map to %s", - seen_idents[id], e.modname, id)) +emit('#include "lua_embed_data.h"\n\n') + +-- group_idents: ordered list of { name, entries_id } +local group_idents = {} +local seen_c_idents = {} + +for _, grp in ipairs(cfg.groups) do + local grp_name = grp.name + check_c_ident(grp_name) + local use_bytecode = grp.bytecode or false + local lua_mode = grp.lua_mode or false + + local files = collect_group_files({ entries = grp.entries, lua_mode = lua_mode }) + + -- per-entry byte arrays + local entry_idents = {} + for _, f in ipairs(files) do + local src = readfile(f.path) + local payload + if use_bytecode then + local func, err = load(src, "@" .. f.path) + if not func then error("syntax error in " .. f.path .. ": " .. tostring(err)) end + payload = string.dump(func) + else + payload = src + end + -- Feed the *full* logical name to to_c_ident in one shot, instead of + -- stitching per-segment results with '_'. Because '_' is also a legal + -- identifier char, piecewise sanitisation can collide across segment + -- boundaries: group "a_b" + file "c" and group "a" + file "b_c" would + -- both produce "le_a_b_c". Using '.' as the segment separator makes + -- the two raw strings distinct ("le_a_b.c" vs "le_a.b_c"), so + -- to_c_ident's internal collision path (hash suffix) can actually + -- fire on the unlikely case that sanitising them yields the same + -- base. to_c_ident guarantees uniqueness across the whole run, but + -- we still assert here so any future refactor that breaks that + -- invariant is caught immediately instead of producing silently + -- mis-linked C. + local id = to_c_ident("le_" .. grp_name .. "." .. f.name) + assert(not seen_c_idents[id], + string.format("internal error: duplicate C identifier %s", id)) + seen_c_idents[id] = f.name + entry_idents[#entry_idents+1] = { name = f.name, id = id, size = #payload } + emit(string.format( + "/* %s: %s */\nstatic const unsigned char %s[] = {\n ", + grp_name, f.name, id)) + emit_c_bytes(payload) + -- sentinel '\0' for lua_pushexternalstring (lua 5.5+) + emit("0x00\n};\n\n") end - seen_idents[id] = e.modname - preload_idents[#preload_idents+1] = { modname = e.modname, id = id, size = #payload } - emit(string.format( - "/* preload: %s */\nstatic const unsigned char %s[] = {\n ", - e.modname, id)) - emit_c_bytes(payload) - emit("\n};\n\n") -end --- data entries: raw bytes -local data_idents = {} -for _, e in ipairs(data_list) do - local src = readfile(e.path) - local id = "led_" .. to_c_ident(e.name) - if seen_idents[id] then - error(string.format( - "C identifier collision: %q and %q both map to %s", - seen_idents[id], e.name, id)) + -- entries array for this group. + -- grp_name is already validated as a legal C identifier by check_c_ident + -- above, so we can concatenate it directly without going through + -- to_c_ident. The "lg_entries_" prefix also guarantees there is no + -- collision with the "le_..." per-entry symbols. + local entries_id = "lg_entries_" .. grp_name + emit(string.format("static const lua_embed_entry %s[] = {\n", entries_id)) + for _, e in ipairs(entry_idents) do + emit(string.format(' { %q, (const char*)%s, %d },\n', e.name, e.id, e.size)) end - seen_idents[id] = e.name - data_idents[#data_idents+1] = { name = e.name, id = id, size = #src } - emit(string.format( - "/* data: %s */\nstatic const unsigned char %s[] = {\n ", - e.name, id)) - emit_c_bytes(src) - emit("0x00\n};\n\n") -- sentinel '\0' required by lua_pushexternalstring (lua 5.5+) -end + emit(' { NULL, NULL, 0 }\n};\n\n') --- preload table -emit("static const lua_embed_preload lua_embed_preload_table[] = {\n") -for _, e in ipairs(preload_idents) do - emit(string.format( - ' { %q, (const char*)%s, %d },\n', e.modname, e.id, e.size)) + group_idents[#group_idents+1] = { name = grp_name, entries_id = entries_id } end -emit(' { NULL, NULL, 0 }\n};\n\n') --- data table -emit("static const lua_embed_data lua_embed_data_table[] = {\n") -for _, e in ipairs(data_idents) do - emit(string.format(' { %q, (const char*)%s, %d },\n', e.name, e.id, e.size)) -end -emit(' { NULL, NULL, 0 }\n};\n\n') - --- main entry: embedded separately from data_table, accessible only via lua_embed_get_main() -local main_path = cfg.main -if main_path then - local src = readfile(main_path) - local func, err = load(src, "@" .. main_path) - if not func then error("syntax error in " .. main_path .. ": " .. tostring(err)) end - local payload - if use_bytecode then - payload = string.dump(func) - else - payload = src +-- bundle struct instance. +-- When there are no groups the initializer must still contain something, +-- because the struct in the .h carries a placeholder `_reserved` member +-- (an empty struct is non-standard C / rejected by MSVC). See the .h +-- generation below for the matching placeholder declaration. +emit("const lua_embed_bundle lua_embed = {\n") +if #group_idents == 0 then + emit(" 0 /* _reserved */\n") +else + for _, g in ipairs(group_idents) do + emit(string.format(" /* .%s */ %s,\n", g.name, g.entries_id)) end - emit(string.format( - "/* main entry (private, not in data_table) */\nstatic const unsigned char lua_embed_main_data[] = {\n ")) - emit_c_bytes(payload) - emit("\n};\n\n") - emit(string.format( - 'static const lua_embed_data lua_embed_main_entry = { "=main", (const char*)lua_embed_main_data, %d };\n\n', - #payload)) end +emit("};\n") + +emit_flush(output_c) --- lookup functions -emit([[ -const lua_embed_preload* lua_embed_get_preload(void) { - return lua_embed_preload_table; -} - -const lua_embed_data* lua_embed_get_data(const char* name) { - const lua_embed_data* e; - for (e = lua_embed_data_table; e->name != NULL; e++) { - if (strcmp(e->name, name) == 0) return e; - } - return NULL; -} -]]) - -if main_path then - emit([[ -const lua_embed_data* lua_embed_get_main(void) { - return &lua_embed_main_entry; -} -]]) +-- ── generate lua_embed_data.h ───────────────────────────────────────────────── + +local hbuf = {} +local function hemit(s) hbuf[#hbuf+1] = s end +hemit('/* Auto-generated by lua_embed_gen.lua -- DO NOT EDIT */\n') +hemit('#pragma once\n') +hemit('#include \n\n') +hemit('#ifdef __cplusplus\nextern "C" {\n#endif\n\n') + +hemit('typedef struct lua_embed_entry {\n') +hemit(' const char* name;\n') +hemit(' const char* data;\n') +hemit(' size_t size;\n') +hemit('} lua_embed_entry;\n\n') + +hemit('typedef struct lua_embed_bundle {\n') +if #group_idents == 0 then + -- An empty struct is non-standard C (MSVC: error C2016). Emit a + -- placeholder member so the generated header always compiles, even + -- when the target declares no data groups (e.g. `data = {}` without + -- bee_glue). The .c initializer has a matching `0 /* _reserved */`. + hemit(' char _reserved; /* placeholder: no groups declared */\n') else - emit([[ -const lua_embed_data* lua_embed_get_main(void) { - return NULL; -} -]]) + for _, g in ipairs(group_idents) do + hemit(string.format(' const lua_embed_entry* %s; /* NULL-terminated */\n', g.name)) + end end +hemit('} lua_embed_bundle;\n\n') -emit_flush(output_c) +hemit('extern const lua_embed_bundle lua_embed;\n') +hemit('\n#ifdef __cplusplus\n}\n#endif\n') + +local hout = assert(io.open(output_h, "wb"), "cannot write: " .. output_h) +hout:write(table.concat(hbuf)) +hout:close() diff --git a/scripts/writer.lua b/scripts/writer.lua index c832f7d..2bef8a1 100644 --- a/scripts/writer.lua +++ b/scripts/writer.lua @@ -903,15 +903,19 @@ function api.lua_embed(global_attribute, name) return function (local_attribute) local lua_embed = require "lua_embed" - -- 解析 rootdir,使 preload/data 中的相对路径能正确定位到子工程目录 + -- 解析 rootdir,使 data 中的相对路径能正确定位到子工程目录 local rootdir = normalize_rootdir(global_attribute.workdir, local_attribute.rootdir or global_attribute.rootdir) local outdir = globals.builddir .. "/lua_embed/" .. name local out_c = outdir .. "/lua_embed.c" local out_c_ninja = "$builddir/lua_embed/" .. name .. "/lua_embed.c" + local out_h_ninja = "$builddir/lua_embed/" .. name .. "/lua_embed_data.h" local gen_name = "__lua_embed_gen_" .. name .. "__" - local use_bee = local_attribute.bee_glue ~= nil + -- bee_glue accepts boolean only; nil = disabled. Type validation and + -- normalization is done inside lua_embed.write_config below, but we + -- need the boolean here to decide whether to compile bee_glue.c. + local use_bee = local_attribute.bee_glue == true -- write config.lua for the generator local config_path = lua_embed.write_config(outdir, local_attribute, rootdir) @@ -919,13 +923,12 @@ function api.lua_embed(global_attribute, name) -- collect inputs for ninja tracking local inputs = lua_embed.collect_inputs(local_attribute, rootdir, config_path) - -- emit shared rule + build edge; config/output passed via Ninja variables - -- so all lua_embed targets reuse a single rule definition. + -- emit shared rule + build edge ninja:rule("lua_embed", "$luamake lua " .. fsutil.quotearg(lua_embed.GEN_SCRIPT) .. " $config $out_c", { description = "lua_embed $config", restat = 1, }) - local outputs = { out_c_ninja } + local outputs = { out_c_ninja, out_h_ninja } ninja:build(outputs, inputs, { variables = { config = fsutil.quotearg(config_path), @@ -933,31 +936,12 @@ function api.lua_embed(global_attribute, name) }, }) - -- copy lua_embed.h via ninja build edge so it is tracked in the DAG; - -- if builddir is cleaned or the header is updated, ninja will re-copy. - local header_src = lua_embed.HEADER - local header_dst_ninja = "$builddir/lua_embed/" .. name .. "/lua_embed.h" - local copy_name = "__lua_embed_hdr_" .. name .. "__" - log.assert(loaded_target[copy_name] == nil, "`%s`: redefinition.", copy_name) - generate_copy({}, { header_src }, { header_dst_ninja }) - ninja:phony(copy_name, header_dst_ninja) - loaded_target[copy_name] = { implicit_inputs = copy_name } - - -- gen_name aggregates both the generated .c and the copied header - local gen_all = {} - for _, o in ipairs(outputs) do - gen_all[#gen_all+1] = o - end - gen_all[#gen_all+1] = header_dst_ninja + -- gen_name aggregates both generated outputs (.c and .h) log.assert(loaded_target[gen_name] == nil, "`%s`: redefinition.", gen_name) - ninja:phony(gen_name, gen_all) + ninja:phony(gen_name, outputs) loaded_target[gen_name] = { implicit_inputs = gen_name } -- build the source_set that callers dep on - -- 先用 reslove_attributes_nolink 归一化用户在 lua_embed 块中写的编译属性 - -- (defines/flags/includes/objdeps/confs 等),然后再追加 lua_embed 自己构造的 - -- sources/includes/objdeps(已经是 WORKDIR 相对路径),直接调用 - -- generate_compile 跳过 glob 展开,避免路径被 rootdir 错误归一化。 local src_attr = {} for k, v in pairs(local_attribute) do src_attr[k] = v @@ -966,22 +950,20 @@ function api.lua_embed(global_attribute, name) src_attr.luaversion = "lua55" end -- lua_embed 专用字段不传入编译属性解析 - src_attr.preload = nil - src_attr.data = nil + src_attr.data = nil src_attr.bee_glue = nil - src_attr.bytecode = nil - -- sources 由 lua_embed 自己构造,不参与属性归一化 - src_attr.sources = nil + src_attr.sources = nil local attribute = reslove_attributes_nolink(global_attribute, src_attr) -- 追加 lua_embed 自己构造的路径(已经是 WORKDIR 相对路径) local sources = { out_c } - if use_bee then sources[#sources+1] = lua_embed.BEE_GLUE end + if use_bee then + sources[#sources+1] = lua_embed.BEE_GLUE + end attribute.includes = attribute.includes or {} table.insert(attribute.includes, 1, outdir) - -- 保留用户指定的 objdeps,追加 lua_embed 生成目标的依赖 attribute.objdeps = attribute.objdeps or {} attribute.objdeps[#attribute.objdeps+1] = gen_name @@ -991,7 +973,7 @@ function api.lua_embed(global_attribute, name) local t = loaded_target[name] if t then t.export_includes = { outdir } - t.export_objdeps = { copy_name } + t.export_objdeps = { gen_name } end end end diff --git a/skills/SKILL.md b/skills/SKILL.md index 3a564f2..31d4302 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -37,7 +37,7 @@ luamake rebuild # 重建 | `lm:lua_src` | Lua C 模块(静态嵌入到 `lm:lua_exe`) | 无 | | `lm:lua_dll` | Lua C 模块(动态加载) | `module.dll` / `module.so` | | `lm:lua_exe` | 内嵌 Lua 的可执行文件 | `app.exe` / `app` | -| `lm:lua_embed` | Lua 嵌入资源(将 Lua 文件嵌入为 C 源码集,默认嵌入源码,可选字节码)。自动向依赖方导出 `includes` 和 `objdeps`,无需手动声明 | 无(source_set) | +| `lm:lua_embed` | Lua 嵌入资源(按命名组嵌入 Lua 文件,生成 source_set;自动导出 `includes`/`objdeps`) | 无(source_set) | | `lm:phony` | 伪目标(聚合依赖) | 无 | --- @@ -115,37 +115,27 @@ lm:lua_exe "app" { } ``` -**`lm:lua_embed`** — 将 Lua 文件嵌入到 C 源码中,生成一个 source_set 供其他目标依赖。默认嵌入 Lua 源码以保证跨 Lua 版本兼容;设置 `bytecode = true` 可嵌入字节码(体积更小、可隐藏源码,但要求 luamake 宿主 Lua 版本与目标 `luaversion` 一致)。 - -`lua_embed` 会自动向依赖方导出 `includes`(生成的头文件目录)和 `objdeps`(头文件生成目标),因此通过 `deps` 依赖 `lua_embed` 的目标**无需手动声明** `includes` 路径和 `objdeps`: +**`lm:lua_embed`** — 将 Lua 文件按命名组嵌入到 C 源码中,生成一个 source_set 供其他目标依赖。`data` 下每个 key 是一个组,组名直接成为生成的 `lua_embed_bundle` 结构体的字段名。每组可独立设置 `bytecode = true`。`bee_glue = true` 启用 bee 胶水层,约定 `data.preload` 注入 `_PRELOAD`、`data.main[1]` 作为入口。自动向依赖方导出 `includes` 和 `objdeps`,通过 `deps` 依赖时无需手动声明。**完整规则(分组语义、`pattern` 语法、字节码取舍、bee_glue 契约、无胶水时的接入姿势)见 `references/advanced/lua_embed.md`**: ```lua -lm:lua_embed "embedded_scripts" { - -- 可选:嵌入字节码而非源码(默认 false) - -- bytecode = true, - -- 将目录下的 Lua 文件嵌入,注入 _PRELOAD - preload = { - { dir = "scripts/modules", prefix = "app" }, - { file = "scripts/init.lua", name = "app.init" }, - }, - -- 将文件作为原始数据嵌入 +lm:lua_embed "myembed" { + bee_glue = true, data = { - { dir = "assets", prefix = "assets/" }, - { file = "main.lua", name = "main.lua" }, + main = { bytecode = true, "src/main.lua" }, + preload = { bytecode = true, { dir = "lualib" } }, + data = { { name = "config.json", file = "assets/config.json" } }, }, - -- 可选:启用 bee 胶水层并指定入口文件 - bee_glue = "main.lua", } --- deps 依赖 lua_embed 时,includes 和 objdeps 自动传递 lm:lua_src "glue" { - deps = "embedded_scripts", -- 自动获取生成头文件的 includes 和 objdeps - includes = "3rd/bee.lua", -- 只需声明自己额外的 includes - sources = "src/glue.cpp", + deps = "myembed", -- 自动获取 includes 和 objdeps + includes = "3rd/bee.lua", + sources = "src/glue.cpp", } +``` lm:lua_exe "app" { - deps = { "glue", "embedded_scripts" }, + deps = { "glue", "myembed" }, sources = "src/main.cpp", } ``` @@ -299,6 +289,7 @@ luamake rebuild | Bee 系统 API | `references/bee/bee_system.md` | | Bee 线程 API | `references/bee/bee_thread.md` | | Bee Channel API | `references/bee/bee_channel.md` | +| `lm:lua_embed` 权威参考(分组 / pattern / bytecode / bee_glue / 自定义接入) | `references/advanced/lua_embed.md` | ### 适用边界 diff --git a/skills/references/advanced/lua_embed.md b/skills/references/advanced/lua_embed.md new file mode 100644 index 0000000..8ea06cd --- /dev/null +++ b/skills/references/advanced/lua_embed.md @@ -0,0 +1,370 @@ +# lm:lua_embed — Lua / 资源嵌入 + +`lm:lua_embed` 是一个高阶目标,用来把 Lua 脚本或任意二进制资源以 C 数组的方式嵌入到可执行文件。它在背后自动完成 **代码生成 → source_set → 依赖导出** 的完整管道,相比手写 `lm:runlua` + `objdeps` 更简洁、可靠。 + +本文档是 `lm:lua_embed` 的权威参考,集中阐述其所有规则与行为。`SKILL.md` / `bp_codegen.md` / `bp_dependency.md` / `advanced_api.md` 中的相关片段为摘要/示例,细节以本文档为准。 + +--- + +## 1. 目标产物与导出属性 + +一个 `lm:lua_embed "xxx" { ... }` 在内部登记为一个 `source_set`,会固定生成两个文件: + +| 产物 | 路径 | 作用 | +|---|---|---| +| 生成的 C 源 | `$builddir/lua_embed/xxx/lua_embed.c` | 定义全局变量 `const lua_embed_bundle lua_embed` | +| 生成的 C 头 | `$builddir/lua_embed/xxx/lua_embed_data.h` | 声明 `lua_embed_entry` / `lua_embed_bundle`,`extern` 全局变量 | + +并自动导出两个属性: + +- `export_includes` → 上述目录,使 `#include "lua_embed_data.h"` 能找到; +- `export_objdeps` → 聚合 `.c` 和 `.h` 两个产物的 phony,确保编译前源与头都已生成。 + +**依赖方只需 `deps = "xxx"`**,无需手动写 `includes` / `objdeps`。内部 phony 名(例如 `__lua_embed_gen_xxx__`)与内部路径为实现细节,**不要在用户脚本中硬编码**。 + +> 启用 `bee_glue = true` 时,会额外把 [scripts/lua_embed/bee_glue.c](../../../scripts/lua_embed/bee_glue.c) 也加入该 source_set 一起编译。 + +--- + +## 2. 配置骨架 + +```lua +lm:lua_embed "myembed" { + bee_glue = true, -- 可选:启用 bee 胶水层 + data = { + = { -- 组名必须是合法 C 标识符 + bytecode = true, -- 可选:该组嵌入字节码而非源码 + -- 组的数组部分 = 条目列表,三种写法见 §3 + "src/foo.lua", + { dir = "lualib" }, + { file = "assets/a.json", name = "a.json" }, + }, + = { ... }, + }, +} +``` + +核心概念: + +- 所有文件通过顶层 `data` 的 **组(group)** 组织,组名直接成为生成的 `lua_embed_bundle` 结构体的字段名; +- 组名必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$`,否则代码生成阶段会报错; +- 组之间在结构体中按 **字母序** 排列(稳定,可用于代码静态分析); +- `bytecode` 是 **每个组独立** 的开关,不是全局开关。 + +--- + +## 3. 组内条目:三种写法 + +组的数组部分逐项处理,允许混合三种写法: + +### 3.1 裸字符串 —— 单文件,name 取文件名 + +```lua +"src/main.lua" -- name = "main.lua" +``` + +### 3.2 `{ dir = ..., prefix = ..., pattern = ... }` —— 扫描目录 + +| 字段 | 必选 | 说明 | +|---|---|---| +| `dir` | ✓ | 要递归扫描的目录(相对 `rootdir`) | +| `prefix` | ✗ | 生成的 `name`(或模块名)前缀 | +| `pattern` | ✗ | 指定后启用 **Lua 模块名扫描**,见 §4 | + +**扫描模式**由 §4 决定(原始文件名 / Lua 模块名)。 + +### 3.3 `{ file = ..., name = ... }` —— 单文件 + 显式命名 + +```lua +{ file = "scripts/config.lua", name = "config" } +``` + +`name` 必填;缺失会在 `write_config` 阶段 `assert` 失败。 + +--- + +## 4. `lua_mode`(Lua 模块名扫描)启用规则 + +每个组有两种扫描模式: + +- **普通模式**:`dir` 条目按 **原始相对路径** 作为 `name`(例如 `sub/bar.lua`); +- **lua_mode**:`dir` 条目按 **Lua 模块名** 作为 `name`(例如 `sub.bar`)。 + +触发 `lua_mode` 的条件(见 [lua_embed.lua](../../../scripts/lua_embed.lua) `group_lua_mode`)**任一满足即可**: + +1. 组内任何一个 `dir` 条目带了 `pattern` 字段 → **整组** 切到 lua_mode; +2. `bee_glue = true` 且组名 = `preload` → 自动切到 lua_mode(因为 `_PRELOAD` 以模块名作键); + +其余情况走普通模式。 + +### 4.1 `pattern` 语法 + +格式同 Lua 的 `package.path`:用 `;` 分隔多个模板,每个模板用 `?` 作占位符。默认值为: + +``` +?.lua;?/init.lua +``` + +匹配规则(见 `match_pattern`): +- `foo.lua` 命中 `?.lua`,模块名 `foo`; +- `foo/init.lua` 命中 `?/init.lua`,模块名 `foo`; +- `a/b.lua` 命中 `?.lua`,`a/b` → 内部 `/` 自动替换为 `.`,最终 `a.b`; +- 若设置了 `prefix = "pkg"`,则最终模块名为 `pkg.a.b`; +- 不匹配任何模板的 `.lua` 文件会被跳过并打印 `[lua_embed] warning: ... does not match any pattern, skipped`; +- 非 `.lua` 文件在 lua_mode 下直接忽略。 + +### 4.2 同模块名冲突处理 + +扫描过程中若同一模块名重复命中,采用 **确定性消歧策略**(见 `scan_lua_dir`): + +- **同一次 `scan_lua_dir` 调用内**(即同一 `{ dir, pattern }`):按 `pattern` 的左到右优先级决定胜者,对齐 Lua `package.path` 的 `?.lua` 优先于 `?/init.lua` 的语义;新胜者 **原地替换** 旧条目,`result` 中始终只有一条,避免后续 C 标识符碰撞; +- **跨 `scan_lua_dir` 调用**(不同条目、不同 dir):先到先得; +- 两种情况都会打印警告,包含保留与跳过的路径。 + +普通模式下 `scan_data_dir` 不做去重(因为普通模式下 `name` 天然唯一),依赖调用者自行约束。 + +--- + +## 5. `bytecode` 规则 + +- 默认 `false`:嵌入 **源码文本**,运行时用 `luaL_loadbuffer` 以文本模式加载; +- 设为 `true`:生成时用 `load(src)` + `string.dump(func)` 输出 **字节码**。 + +取舍: + +| 选项 | 体积 | 隐藏源码 | Lua 版本耦合 | +|---|---|---|---| +| `bytecode = false` | 较大 | 否 | **无**,任何版本 luamake 都能构建,任何版本 Lua 都能加载 | +| `bytecode = true` | 较小 | 是 | **宿主 Lua 版本 必须 == 目标 `luaversion`**,否则运行时加载失败 | + +**默认源码嵌入** 是刻意的:luamake 宿主 Lua 版本可能与被嵌入目标的 `luaversion` 不一致,保留文本能避免字节码不兼容。 + +语法错误在代码生成阶段即刻失败(`syntax error in `),不会等到运行期才暴露。 + +--- + +## 6. `bee_glue = true`:bee.lua 胶水层 + +启用后 [bee_glue.c](../../../scripts/lua_embed/bee_glue.c) 会被一并编译进来,并强制要求以下三个组存在(即使为空): + +```lua +data = { + main = { ... }, -- 必须存在 + preload = { ... }, -- 必须存在,自动切到 lua_mode + data = { ... }, -- 必须存在 +} +``` + +缺少任一组会在 `write_config` 阶段 `assert` 失败,提示: + +``` +lua_embed: bee_glue requires group "main" to be defined (use an empty table {} if unused) +``` + +> 为什么强制存在?`bee_glue.c` 通过 `lua_embed.main` / `lua_embed.preload` / `lua_embed.data` 直接引用结构体字段;字段由组名派生,**任一组缺失都会造成 C 编译错误 `struct has no member`**。 + +启用后的运行时契约: + +| 组 | 行为 | +|---|---| +| `main` | 导出 `_bee_main(L)`:以 `main` 组的第一个条目(Lua 配置侧 `main[1]` / C 侧 `main[0]`)作为入口脚本,`luaL_loadbuffer` + `lua_pcall(0, 0, 0)` 执行 | +| `preload` | 导出 `_bee_preload_module(L)`:遍历全部条目,以 `name` 为键注入 `_PRELOAD`,每个 loader 是一个轻量闭包,`require` 触发时才 `luaL_loadbuffer` | +| `data` | 注册 `require "bee.embed"`:返回一个带 `__index` 的 table,按字符串键线性查找 `lua_embed.data`,命中后缓存到 table 本体(后续 O(1))。Lua 5.5 下用 `lua_pushexternalstring` 零拷贝,旧版本回退为 `lua_pushlstring` | + +示例: + +```lua +lm:lua_embed "myembed" { + bee_glue = true, + data = { + main = { + bytecode = true, + "src/main.lua", -- 入口脚本 + }, + preload = { + bytecode = true, + { dir = "lualib" }, -- 自动 lua_mode,require "foo" 即可 + { file = "scripts/init.lua", name = "init" }, + }, + data = { + { file = "assets/config.json", name = "config.json" }, + { dir = "assets/res", prefix = "res/" }, -- 原始文件名扫描 + }, + }, +} +``` + +在 Lua 侧: + +```lua +local embed = require "bee.embed" +local cfg = embed["config.json"] -- string 或 nil;命中后缓存 +``` + +--- + +## 7. 不使用 `bee_glue` 时如何接入 + +不启用 `bee_glue` 时: + +- 组名 **完全自由**(合法 C 标识符即可),没有 `main` / `preload` / `data` 强制约束; +- 不注入任何 Lua 运行时钩子,拿到的就是一个纯数据 bundle,**由调用者决定用法**; +- 依然导出 `export_includes` / `export_objdeps`,C 侧照常 `#include "lua_embed_data.h"`。 + +三种常见接入姿势: + +### 7.1 自定义 `package.searchers` + +在 `searchers[2]` 插入一个查 `lua_embed.` 的 C 函数: + +```c +static int embed_searcher(lua_State* L) { + const char* modname = luaL_checkstring(L, 1); + for (const lua_embed_entry* e = lua_embed.preload; e->name; ++e) { + if (strcmp(e->name, modname) == 0) { + if (luaL_loadbuffer(L, e->data, e->size, modname) != LUA_OK) + return lua_error(L); + lua_pushstring(L, modname); + return 2; + } + } + lua_pushfstring(L, "\n\tno embedded module '%s'", modname); + return 1; +} +``` + +> 注意:该组要用模块名作键,必须显式 `pattern = "?.lua;?/init.lua"`,否则键会是 `"foo.lua"` 而非 `"foo"`。 + +### 7.2 批量注入 `package.preload` + +开局把整组塞进 `package.preload`,比自定义 searcher 更简单,等价于 `bee_glue` 对 `preload` 组的处理。 + +### 7.3 纯二进制资源 + +如果嵌入的不是 Lua 模块而是证书 / 配置 / schema,`lm:lua_embed` 也可以用——它退化为一个"内嵌资源表",甚至可以给 **非 `lm:lua_exe`** 的普通 `lm:exe` 使用: + +```lua +lm:lua_embed "resources" { + data = { + assets = { + { file = "cert.pem", name = "cert.pem" }, + { file = "schema.json", name = "schema.json" }, + }, + }, +} + +lm:exe "myapp" { + deps = "resources", + sources = "src/main.c", -- 按 lua_embed.assets 线性查找即可 +} +``` + +--- + +## 8. C API(`lua_embed_data.h`) + +```c +#include "lua_embed_data.h" + +typedef struct lua_embed_entry { + const char* name; /* 条目名(文件名 / 模块名,取决于 §4) */ + const char* data; /* 内容指针;末尾有 '\0' 哨兵(便于 lua_pushexternalstring) */ + size_t size; /* 字节数,不含哨兵 */ +} lua_embed_entry; + +/* 字段由组名派生,按字母序排列。以下仅示例: */ +typedef struct lua_embed_bundle { + const lua_embed_entry* data; /* NULL-terminated */ + const lua_embed_entry* main; /* NULL-terminated */ + const lua_embed_entry* preload; /* NULL-terminated */ +} lua_embed_bundle; + +extern const lua_embed_bundle lua_embed; +``` + +遍历 / 查找惯用法: + +```c +for (const lua_embed_entry* e = lua_embed.preload; e->name != NULL; ++e) { ... } + +const lua_embed_entry* m = lua_embed.main; /* 入口是第一个元素 */ + +for (const lua_embed_entry* e = lua_embed.data; e->name != NULL; ++e) + if (strcmp(e->name, "config.json") == 0) { /* ... */ } +``` + +**查找复杂度**:线性扫描 O(N)。[bee_glue.c](../../../scripts/lua_embed/bee_glue.c) 中的 `bee.embed` 通过 **命中后缓存到 Lua table** 摊销为 O(1);自定义实现若需要高频查找,建议采用相同模式,或在资产量非常大时拆分多个组 / 改用文件系统后端。 + +--- + +## 9. C 标识符碰撞 + +代码生成器对每个条目产生一个形如 `le__` 的 C 标识符(见 `to_c_ident`)。由于 `name` 中非 `[A-Za-z0-9_]` 字符会被折叠成 `_`,不同 `name` 可能映射到同一基名(例如 `foo.bar` 与 `foo_bar`)。策略: + +1. 同一次运行内,冲突时在基名后追加 **短 djb2 哈希**; +2. 映射通过双向表缓存,**在单次运行中保持纯函数性**; +3. 另外在循环里 `assert` 去重,任何破坏上述不变式的改动都会立刻暴露。 + +用户侧一般不会感知到这一点;当出现 `internal error: duplicate C identifier` 时,通常意味着组内存在 `name` 碰撞,应该改名或拆分。 + +--- + +## 10. 依赖方示例 + +```lua +lm:lua_embed "my_embed" { + bee_glue = true, + data = { + main = { bytecode = true, "src/main.lua" }, + preload = { bytecode = true, { dir = "lualib" } }, + data = { { file = "assets/config.json", name = "config.json" } }, + }, +} + +-- ✅ 推荐:只写自己额外的 includes,headers & objdeps 由 lm:lua_embed 自动导出 +lm:lua_src "my_glue" { + deps = "my_embed", + includes = "3rd/bee.lua", + sources = "src/glue.cpp", +} + +lm:lua_exe "my_app" { + deps = { "my_glue", "my_embed" }, + sources = "src/main.cpp", +} +``` + +❌ **不推荐**:硬编码内部路径或 phony 名: + +```lua +lm:lua_src "my_glue" { + includes = { "3rd/bee.lua", "_build/lua_embed/my_embed" }, -- 内部路径 + sources = "src/glue.cpp", + objdeps = "__lua_embed_gen_my_embed__", -- 内部 phony 名 +} +``` + +这两项都由 `lm:lua_embed` 通过 `deps` 自动传递。 + +--- + +## 11. 最小验证清单 + +写完 `lm:lua_embed` 后对照检查: + +1. 组名是不是合法 C 标识符? +2. 启用 `bee_glue = true`?→ 必须定义 `main` / `preload` / `data` 三组(可以是空表 `{}`)。 +3. 没启用 `bee_glue`?→ 如果该组要按模块名作键,**记得写 `pattern = "?.lua;?/init.lua"`**。 +4. 依赖方只写 `deps = "xxx"`,不要重复指定 `includes` / `objdeps`。 +5. 跨 Lua 版本分发?→ 不要开 `bytecode = true`。 +6. 有跨组同名条目?→ 没关系,条目名只在组内唯一即可。 +7. `[lua_embed] warning: ...` 出现时要读一下——通常是模板没匹配上或模块名碰撞。 + +--- + +## 参考实现 + +- [scripts/lua_embed.lua](../../../scripts/lua_embed.lua) — 配置写出、输入收集、`lua_mode` 判定 +- [scripts/lua_embed/lua_embed_gen.lua](../../../scripts/lua_embed/lua_embed_gen.lua) — 实际扫描、字节码转换、C 代码生成 +- [scripts/lua_embed/bee_glue.c](../../../scripts/lua_embed/bee_glue.c) — `bee_glue = true` 时的运行时胶水 +- [scripts/writer.lua](../../../scripts/writer.lua) `api.lua_embed` — 目标注册与 `export_includes` / `export_objdeps` 导出 diff --git a/skills/references/best_practices/bp_codegen.md b/skills/references/best_practices/bp_codegen.md index 3056175..f2e6e78 100644 --- a/skills/references/best_practices/bp_codegen.md +++ b/skills/references/best_practices/bp_codegen.md @@ -73,82 +73,52 @@ lm:exe "server" { ## 3. 使用 lm:lua_embed 嵌入 Lua 资源 -当需要将 Lua 脚本或数据文件嵌入到 C/C++ 可执行文件中时,推荐使用 `lm:lua_embed`。它封装了完整的代码生成管道:自动扫描文件 → 生成 C 源码 → 构建 source_set,无需手动编写 `lm:runlua` + `objdeps` 组合。 +当需要将 Lua 脚本或数据文件嵌入到 C/C++ 可执行文件中时,推荐使用 `lm:lua_embed`。它封装了完整的代码生成管道:自动扫描文件 → 生成 C 源码(`lua_embed.c`)与头文件(`lua_embed_data.h`)→ 构建 source_set → 自动导出 `includes` / `objdeps`。相比手动 `lm:runlua` + `objdeps` 更简洁、可靠。 -默认嵌入 Lua **源码**,保证跨 Lua 版本兼容(luamake 宿主 Lua 版本可以与目标 `luaversion` 不同)。如果需要更小的体积或隐藏源码,可设置 `bytecode = true` 嵌入字节码,但此时要求 luamake 宿主 Lua 版本与目标 `luaversion` 一致。 +默认嵌入 Lua **源码**(跨 Lua 版本兼容);需要体积更小或隐藏源码时,在组内设置 `bytecode = true` 嵌入字节码(此时要求 luamake 宿主 Lua 版本与目标 `luaversion` 一致)。 -### 典型场景:嵌入 Lua 模块到可执行文件 +### 最常见形态 ```lua +-- 场景 A:不用 bee_glue,自己决定怎么用数据 lm:lua_embed "embedded_lua" { - preload = { - { dir = "scripts/modules" }, -- 自动扫描 - { file = "scripts/config.lua", name = "config" }, -- 单文件 - }, - data = { - { file = "main.lua", name = "main.lua" }, - }, -} - -lm:lua_exe "myapp" { - deps = "embedded_lua", - sources = "src/main.cpp", -} -``` - -### 典型场景:使用字节码嵌入(体积更小、隐藏源码) - -```lua -lm:lua_embed "embedded_lua" { - bytecode = true, -- 嵌入字节码而非源码(需 Lua 版本一致) - preload = { - { dir = "scripts/modules" }, - }, data = { - { file = "main.lua", name = "main.lua" }, + preload = { + -- 无 bee_glue 时,preload 组要显式写 pattern 才能得到模块名作键 + { dir = "scripts/modules", pattern = "?.lua;?/init.lua" }, + { file = "scripts/config.lua", name = "config" }, + }, + main = { + "scripts/main.lua", + }, }, } -``` - -### 典型场景:与 bee.lua 集成 -```lua +-- 场景 B:bee.lua 集成(自动 _PRELOAD / main 入口 / require "bee.embed") lm:lua_embed "bee_app" { - bee_glue = "main.lua", - preload = { - { dir = "scripts", prefix = "app" }, - }, + bee_glue = true, data = { - { file = "main.lua", name = "main.lua" }, + preload = { bytecode = true, { dir = "scripts" } }, -- bee_glue 下 preload 自动 lua_mode + main = { bytecode = true, "src/main.lua" }, + data = {}, }, } +-- 依赖方:只写 deps,不要重复指定 includes / objdeps lm:lua_exe "myapp" { - deps = "bee_app", - sources = "src/main.cpp", + deps = "bee_app", + sources = "src/main.cpp", -- 该文件可 #include "lua_embed_data.h" } ``` -### 典型场景:其他目标通过 deps 自动获取 lua_embed 的 includes 和 objdeps - -`lm:lua_embed` 会自动导出 `export_includes`(生成的头文件目录)和 `export_objdeps`(头文件生成目标),其他目标只需通过 `deps` 依赖 `lua_embed` 目标,即可自动获取正确的头文件搜索路径和编译顺序依赖,**无需手动指定 `includes` 和 `objdeps`**。 +### 关键要点 -```lua --- ✅ 推荐:通过 deps 自动获取 includes 和 objdeps -lm:lua_embed "my_embed" { - bee_glue = "src/main.lua", - preload = { - { dir = "lualib" }, - }, -} - -lm:lua_src "my_glue" { - deps = "my_embed", -- 自动获取 includes 和 objdeps - includes = "3rd/bee.lua", -- 只需写自己额外的 includes - sources = "src/glue.cpp", -- glue.cpp 中 #include -} -``` +- **组(group)** 组织:`data` 下每个 string key 是一个组,组名直接成为生成的 `lua_embed_bundle` 结构体字段; +- **`bytecode`** 是每个组独立的开关,默认 `false`; +- **`lua_mode` 启用**:组内任一 `dir` 带 `pattern` → 整组切到 Lua 模块名扫描;`bee_glue = true` 时 `preload` 组自动切到该模式; +- **`bee_glue = true`** 硬约定:必须定义 `main` / `preload` / `data` 三组(可空表 `{}`); +- 自动导出 `export_includes`(含 `lua_embed_data.h`)与 `export_objdeps`(聚合 `.c` 和 `.h` 两个产物的 phony),**依赖方只需 `deps = "xxx"`**。 -> **与手动 `lm:runlua` 的对比**:`lm:lua_embed` 自动处理了 ninja 依赖追踪、`objdeps` 声明、头文件复制等细节。对于简单的单文件代码生成仍可使用 `lm:runlua`,但批量嵌入 Lua 资源时 `lm:lua_embed` 更简洁可靠。 +> **完整规则(`pattern` 语法、字节码版本约束、模块名冲突处理、bee.embed 运行时契约、不启用 `bee_glue` 时的三种接入姿势、C API、C 标识符碰撞等)请参阅** [`references/advanced/lua_embed.md`](../advanced/lua_embed.md)。 -> **`bytecode` 选项**:默认 `false`(嵌入源码),设为 `true` 时使用 `string.dump` 生成字节码嵌入。字节码体积更小且可隐藏源码,但生成的字节码绑定到 luamake 宿主的 Lua 版本,若目标项目通过 `luaversion` 指定了不同版本则会加载失败。 +> 与手动 `lm:runlua` 的对比:`lm:lua_embed` 自动处理 Ninja 依赖追踪、`objdeps` 声明、`.c` 与 `lua_embed_data.h` 联合产出等细节;简单的单文件代码生成可继续用 `lm:runlua`,批量嵌入 Lua 资源时优先 `lm:lua_embed`。 diff --git a/skills/references/best_practices/bp_dependency.md b/skills/references/best_practices/bp_dependency.md index 8ee13ec..97ba024 100644 --- a/skills/references/best_practices/bp_dependency.md +++ b/skills/references/best_practices/bp_dependency.md @@ -112,20 +112,26 @@ lm:exe "main" { ### 典型场景:依赖 lua_embed 生成的头文件 `lm:lua_embed` 会自动导出两个属性: -- **`export_includes`**:生成的头文件所在目录,依赖方自动获得 `-I` 搜索路径 -- **`export_objdeps`**:头文件复制的 phony 目标,确保编译前头文件已就绪 +- **`export_includes`**:生成的头文件所在目录(含 `lua_embed_data.h`),依赖方自动获得 `-I` 搜索路径 +- **`export_objdeps`**:代码生成 phony 目标(聚合 `lua_embed.c` 和 `lua_embed_data.h` 两个输出),确保编译前生成的源码与头文件都已就绪 ```lua lm:lua_embed "my_embed" { - preload = { { dir = "scripts" } }, - data = { { file = "main.lua", name = "main.lua" } }, + data = { + preload = { + { dir = "scripts" }, + }, + main = { + { file = "main.lua", name = "main.lua" }, + }, + }, } -- ✅ 推荐:通过 deps 自动获取 includes 和 objdeps lm:lua_src "my_glue" { deps = "my_embed", includes = "3rd/bee.lua", -- 只需写自己额外的 includes - sources = "src/my_glue.cpp", -- 该文件 #include + sources = "src/my_glue.cpp", -- 该文件 #include } -- ❌ 不推荐:手动硬编码内部路径和目标名 @@ -135,8 +141,10 @@ lm:lua_src "my_glue" { "_build/lua_embed/my_embed", -- 内部路径,不应硬编码 }, sources = "src/my_glue.cpp", - objdeps = "__lua_embed_hdr_my_embed__", -- 内部目标名,不应硬编码 + objdeps = "__lua_embed_gen_my_embed__", -- 内部目标名,不应硬编码 } ``` > **原理**:`lm:lua_embed` 在 `loaded_target` 中设置了 `export_includes` 和 `export_objdeps` 字段。`generate_compile` 在处理 `deps` 时,会自动将这些导出属性合并到依赖方的 `attribute.includes` 和 `attribute.objdeps` 中。这是一个通用机制,未来其他代码生成目标也可以使用。 + +> `lm:lua_embed` 本身的完整规则(分组语义、`pattern` 语法、字节码约束、`bee_glue` 契约、C API 等)见 [`references/advanced/lua_embed.md`](../advanced/lua_embed.md)。 diff --git a/skills/references/core/advanced_api.md b/skills/references/core/advanced_api.md index aceefa9..638a82e 100644 --- a/skills/references/core/advanced_api.md +++ b/skills/references/core/advanced_api.md @@ -109,92 +109,29 @@ lm:runlua { ## lm:lua_embed — Lua 嵌入资源 -将 Lua 文件嵌入到 C 源码中,生成一个 `source_set` 供其他目标通过 `deps` 引用。默认嵌入 Lua 源码,设置 `bytecode = true` 可改为嵌入字节码。支持两种嵌入方式: - -- **preload**:生成可用于预加载的 Lua 模块条目,可通过 `require()` 加载。设置 `bee_glue` 时会自动生成 `_bee_preload_module()` 将模块注入 `_PRELOAD` 表;否则需用户自行遍历 `lua_embed_get_preload()` 完成注入 -- **data**:文件作为原始字节嵌入,运行时可通过 `lua_embed_get_data()` C API 查找;设置 `bee_glue` 时还可在 Lua 中通过 `require "bee.embed"` 访问 - -### 基本用法 +将 Lua 文件(或任意二进制资源)嵌入到 C 源码中,生成一个 `source_set` 供其他目标通过 `deps` 引用。自动导出 `export_includes` 与 `export_objdeps`,依赖方无需再写 `includes` / `objdeps`。 ```lua -lm:lua_embed "embedded" { - preload = { - -- 扫描目录,自动推导模块名 - { dir = "scripts/modules", prefix = "app" }, - -- 单文件指定模块名 - { file = "scripts/init.lua", name = "app.init" }, - }, +lm:lua_embed "myembed" { + bee_glue = true, data = { - -- 扫描目录,嵌入所有文件 - { dir = "assets", prefix = "assets/" }, - -- 单文件 - { file = "main.lua", name = "main.lua" }, + main = { bytecode = true, "src/main.lua" }, + preload = { bytecode = true, { dir = "lualib" } }, + data = { { file = "assets/config.json", name = "config.json" } }, }, } -lm:exe "myapp" { - deps = "embedded", - sources = "src/main.cpp", -} -``` - -### 与 bee.lua 集成 - -设置 `bee_glue` 时,链接静态的 `bee_glue.c`,提供 `_bee_preload_module()` 和 `_bee_main()` 函数。`bee_glue` 指定的入口文件嵌入到 `lua_embed.c` 中,通过 `lua_embed_get_main()` 访问,外部无法通过 `lua_embed_get_data()` 查找到它: - -```lua -lm:lua_embed "embedded" { - bee_glue = "main.lua", -- 启用 bee 胶水层并指定入口文件 - preload = { ... }, - data = { ... }, -} +lm:exe "myapp" { deps = "myembed", sources = "src/main.cpp" } ``` -#### bee.embed 模块 +关键概念速览: -启用 `bee_glue` 后,Lua 代码可通过 `require "bee.embed"` 访问 `data` 条目。返回的 table 以嵌入文件名为键,索引时按需构造字符串并缓存,重复访问同一键不会重复构造: +- 所有条目通过顶层 `data` 的 **组**(group,合法 C 标识符)组织,组名即生成的 `lua_embed_bundle` 结构体字段; +- 每组可独立 `bytecode = true` 嵌入字节码; +- `{ dir, pattern }` 条目指定 `pattern` 时启用 **Lua 模块名扫描**;`bee_glue = true` 时 `preload` 组自动启用; +- `bee_glue = true` 硬约定:必须定义 `main` / `preload` / `data` 三组(可空表),`preload` 注入 `_PRELOAD`、`main[1]` 为入口、`data` 通过 `require "bee.embed"` 暴露。 -```lua -local embed = require "bee.embed" - --- 首次访问:触发构造并缓存 -local data = embed["assets/config.json"] -- string 或 nil - --- 再次访问:直接命中缓存,返回同一对象 -assert(embed["assets/config.json"] == data) -``` - -Lua 5.5 下使用 `lua_pushexternalstring`,字符串直接引用嵌入数组,零拷贝;低版本回退到 `lua_pushlstring`。 - -### 属性说明 - -| 属性 | 类型 | 说明 | -|------|------|------| -| `preload` | table | preload 条目列表 | -| `preload[].dir` | string | 扫描目录路径 | -| `preload[].prefix` | string | 模块名前缀(如 `"app"` → `app.xxx`) | -| `preload[].pattern` | string | 匹配模式(默认 `"?.lua;?/init.lua"`) | -| `preload[].file` | string | 单文件路径(需配合 `name`) | -| `preload[].name` | string | 模块名(`file` 模式必需) | -| `data` | table | data 条目列表 | -| `data[].dir` | string | 扫描目录路径 | -| `data[].prefix` | string | 名称前缀 | -| `data[].file` | string | 单文件路径(需配合 `name`) | -| `data[].name` | string | 查找键名(`file` 模式必需) | -| `bee_glue` | string | 入口文件路径,设置后启用 bee 胶水层(链接 `bee_glue.c`),入口文件嵌入到 `lua_embed.c` 中 | - -### C API(lua_embed.h) - -```c -// 获取所有 preload 条目(NULL 终止数组) -const lua_embed_preload* lua_embed_get_preload(void); - -// 按名称查找 data 条目,未找到返回 NULL -const lua_embed_data* lua_embed_get_data(const char* name); - -// 获取嵌入的 main 入口(bee_glue 时配置),未配置返回 NULL -const lua_embed_data* lua_embed_get_main(void); -``` +> **完整规则、`pattern` 语法、字节码版本约束、bee.embed 运行时行为、不使用 `bee_glue` 时的三种自定义接入方式、C API 使用、碰撞处理等**,参见 [`references/advanced/lua_embed.md`](../advanced/lua_embed.md)。 ---