From 76caa37a60aeb8d4651bedd10ee5c205c58b6b27 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Sun, 4 Oct 2026 23:22:05 +0800 Subject: [PATCH] perf(jit): cache GCC artifacts and make generated C reproducible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS 上首次 dlopen 一个新路径的 dylib 需要约 4.5~5.4 秒走代码签名验证 (amfid / syspolicyd IPC),而同一路径的后续加载只要约 0.2 毫秒。由于 GccJitter::Compile 每次都生成全新路径的临时 dylib,反复编译同一份 C 代码时 这笔固定开销成倍累积:单个 gtest 用例耗时 30 秒以上,全量测试在本地实际 无法跑完。CI 三个 workflow 都是直接跑 unit_tests 二进制且无超时保护,因此 问题一直只表现为"变慢"而非失败。 根因是生成的 C 代码里有两处随运行变化的进程级状态,导致同一输入编译出不同 字节,既破坏可复现构建,也让按内容缓存产物永远无法命中: 1. State 指针被硬编码进产物(static void * _S = (void *) ;)。地址受 ASLR 影响。更严重的是产物一旦被跨 State 复用,_S 会指向别的 State。 2. 常量字符串 ID 被内联进产物(FlSetTableStrId(ops, 35471392768, ...))。该 ID 来自进程级自增计数器。 改为: - _S 初值置 0,由宿主在加载产物后、调用 __fakelua_init 之前通过新导出的 __fakelua_set_state 注入。GCC 与 TCC 两条路径都做注入。 - 常量字符串 ID 不再内联,改为在 __fakelua_const_init 里经 FakeluaConstStrAlloc 注册后存入产物内的静态变量,代码引用变量。含 \0 或 非文本字节的字符串(如 string.charpattern)自动改用长度 + 字节数组形式, 避免 C 字符串字面量截断。依赖常量字符串 ID 的文件级全局变量改为零初始化 并在 __fakelua_const_init 里赋值,因为静态变量不是编译期常量、不能出现在 静态初始化器中。 - 新增按「生成的 C 代码 + 全部编译参数」摘要(xxhash-64)的产物缓存,命中时 复用同一路径从而命中 dyld 的路径缓存。写入走「临时文件 + rename」保证原子。 可通过 StateGCCConfig::enable_compile_cache = false 关闭。 - cache 命中与正常编译两条路径共用 GccJitter::RegisterAndInit,避免行为漂移。 缓存条目失效时删除并退回重新编译,不让缓存成为故障源。 效果(macOS,Release): - closure 套件(11 tests):487 s → 热缓存 1.1 s - 17 个套件 1012 个测试(含 jitter 差分):29 m 12 s → 94 s - 副作用:exception 套件因编译去重从 36 s 降到 7.2 s 已知取舍:缓存目录 $TMPDIR/fakelua-jit-cache 不自动清理,需手动删除。刻意不放在 $TMPDIR/fakelua/ 下 —— 该目录会被测试 runtime.generate_tmp_filename_creates_dir 用 remove_all 清掉。 另外修正文档:README 里的 ctest --test-dir build 实际不生效(根 CTestTestfile 缺 subdirs("test")),已改为直接运行二进制并说明需开 -DFAKELUA_BUILD_TESTS=ON。 --- .gitignore | 1 + README.md | 5 +- README.zh.md | 3 +- include/fakelua.h | 11 ++ src/compile/c_gen.cpp | 188 ++++++++++++++++++---- src/compile/c_gen.h | 20 +++ src/compile/c_runtime_header.h | 4 + src/compile/compile_common.h | 8 + src/jit/gcc_cache.cpp | 73 +++++++++ src/jit/gcc_cache.h | 30 ++++ src/jit/gcc_jit.cpp | 274 ++++++++++++++++++++++++++------- src/jit/gcc_jit.h | 10 ++ src/jit/tcc_jit.cpp | 12 ++ src/jit/vm.cpp | 16 ++ 14 files changed, 562 insertions(+), 93 deletions(-) create mode 100644 src/jit/gcc_cache.cpp create mode 100644 src/jit/gcc_cache.h diff --git a/.gitignore b/.gitignore index 1fd3ca8..d62d4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ test_results.log .cache/ base_cov/ new_cov/ +.workbuddy/ diff --git a/README.md b/README.md index 0530fb1..ff2e467 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,8 @@ ctest --test-dir build -V ```bash cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build build --parallel -ctest --test-dir build -V +# Run from test/lua so relative Lua file paths resolve correctly +(cd test/lua && ../build/bin/unit_tests) ./build/bin/bench_mark ``` @@ -227,6 +228,8 @@ ctest --test-dir build -V > - macOS: `brew install lua` > - Windows MSYS2: `pacman -S mingw-w64-x86_64-lua` +> Tests must be enabled explicitly: `cmake -S . -B build -DFAKELUA_BUILD_TESTS=ON`. + ### CLI Tool `flua` ```bash diff --git a/README.zh.md b/README.zh.md index ee1d9ca..5f37ca8 100644 --- a/README.zh.md +++ b/README.zh.md @@ -218,7 +218,8 @@ ctest --test-dir build -V ```bash cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build build --parallel -ctest --test-dir build -V +# 需要在 test/lua 下运行,让相对路径的 Lua 文件能解析 +(cd test/lua && ../build/bin/unit_tests) ./build/bin/bench_mark ``` diff --git a/include/fakelua.h b/include/fakelua.h index d5d5a09..eb3ebbe 100644 --- a/include/fakelua.h +++ b/include/fakelua.h @@ -388,6 +388,17 @@ struct StateGCCConfig { std::vector include_paths; std::vector library_paths; std::vector libraries; + // 编译产物磁盘缓存(仅 macOS 上有实际收益,详见下方说明)。 + // + // 在 macOS 上,首次 dlopen 某个新路径的 dylib 需要约 4.5~5.4 秒走代码签名验证, + // 而同一路径后续加载只要约 0.2 毫秒。当测试或业务反复编译同一份 C 代码时 + // (例如 6 次 JIT 类型 × debug 模式组合),这笔开销会成倍累积。 + // + // 开启后按「生成的 C 代码 + 编译参数」的摘要缓存 .so,命中时复用同一路径, + // 从而命中 dyld 的路径缓存。缓存文件不会自动删除,可手动清理缓存目录。 + // + // 设为 false 可完全关闭(此时行为与之前一致)。 + bool enable_compile_cache = true; }; struct StateConfig { diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 56fe719..7934b6f 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -179,6 +179,76 @@ bool CGen::ClassifyLiteralKey(const SyntaxTreeInterfacePtr &exp, LiteralKeyInfo } // 核心编译入口函数:为输入的 AST、推断结果及配置生成 C 代码 +// 返回一个「常量字符串 ID 的静态变量名」,并登记注册语句。 +// 见头文件处注释:直接内联 ID 会破坏可复现构建。 +const char *CGen::ConstStrRef(const std::string_view &str) { + if (const auto it = const_str_var_of_.find(std::string(str)); it != const_str_var_of_.end()) { + return it->second.c_str(); + } + + // 含 \0 或其他非文本字节时不能用 C 字符串字面量(\0 会截断),自动走字节数组形式。 + // Lua pattern 字面量里出现 \0 很常见(如 "[\0-\xff]"),所以这个判断不能省。 + for (const char c: str) { + const auto uc = static_cast(c); + if (uc == 0 || uc >= 0x80) { + return ConstStrRefBinary(str); + } + } + + const auto var = "__fl_cstr_" + std::to_string(const_str_var_of_.size()); + const_str_var_of_.emplace(std::string(str), var); + // 必须转义:字符串里可能含引号或反斜杠,直接拼进 C 字面量会导致编译失败。 + const_str_regs_.push_back(std::format(" {} = FakeluaConstStrAlloc(_S, \"{}\");\n", var, EscapeCStringLiteral(std::string(str)))); + // 声明文本先攒起来,等 Build 收尾时统一插到头段最前面 —— + // 直接就地输出会插到正在生成的字符串字面量中间。 + const_str_decls_.push_back(std::format("static int64_t {} = 0;\n", var)); + return const_str_var_of_.find(std::string(str))->second.c_str(); +} + +const char *CGen::ConstStrRefBinary(const std::string_view &str) { + if (const auto it = const_str_var_of_.find(std::string(str)); it != const_str_var_of_.end()) { + return it->second.c_str(); + } + + const auto var = "__fl_cstr_" + std::to_string(const_str_var_of_.size()); + const_str_var_of_.emplace(std::string(str), var); + + // 字节数组形式:显式给长度,避免 \0 截断;用长度构造 std::string_view 传给运行时。 + std::string bytes; + bytes.reserve(str.size() * 4 + 32); + bytes += " static const char " + var + "_data[] = {"; + for (size_t i = 0; i < str.size(); ++i) { + if (i) { + bytes += ","; + } + bytes += std::format("{}", static_cast(static_cast(str[i]))); + } + bytes += ",0};\n"; + bytes += std::format(" static const size_t {}_len = {};\n", var, str.size()); + bytes += std::format(" {} = FakeluaConstStrAllocN(_S, {}_data, {}_len);\n", var, var, var); + const_str_binary_.push_back(bytes); + + const_str_decls_.push_back(std::format("static int64_t {} = 0;\n", var)); + return const_str_var_of_.find(std::string(str))->second.c_str(); +} + +void CGen::EmitConstInit() { + SectionGuard g(*this, Section::Impls); + Out() << "void __fakelua_const_init(void) {\n"; + for (const auto ®: const_str_regs_) { + Out() << reg; + } + for (const auto &bin: const_str_binary_) { + Out() << bin; + } + // 依赖常量字符串 ID 的全局变量:静态初始化器里不能用非编译期常量, + // 放到这里赋值(此时 ID 已注册完毕)。 + for (const auto &d: const_str_deferred_inits_) { + Out() << d; + } + Out() << "}\n\n"; +} + GenResult CGen::Generate(const ParseResult &pr, const InferResult &ir, const AnalysisResult &ar, const CompileConfig &cfg) { LOG_DEBUG(s_, "engine", "start CGen::Generate {}", pr.file_name); @@ -262,7 +332,30 @@ GenResult CGen::Build(const ParseResult &pr, const CompileConfig &cfg) { // 4. 遍历生成所有函数特化与通用的具体 C 代码实现 GenerateImpl(pr.chunk, gr); - // 5. 根据配置记录生成的 C 核心区代码,或组合全部代码块输出最终结果 + // 5. 发射常量字符串 ID 的静态变量声明与注册函数。 + // 声明要早于全部使用点,但不能插到 #include 之前 —— 插到运行时头之后、 + // 其余代码之前。 + if (!const_str_var_of_.empty()) { + std::string decls; + for (const auto &d: const_str_decls_) { + decls += d; + } + decls += "\n"; + auto &headers = sections_[static_cast(Section::Headers)]; + std::string header_text = headers.str(); + constexpr const char *kAnchor = "// __fakelua_const_str_decls__"; + const auto pos = header_text.find(kAnchor); + if (pos != std::string::npos) { + header_text.insert(pos + std::strlen(kAnchor), "\n" + decls); + } else { + // 锚点缺失说明 GenerateHeader 未跑,属于内部不一致;前置到头段最前。 + header_text = decls + header_text; + } + headers.str(header_text); + EmitConstInit(); + } + + // 6. 根据配置记录生成的 C 核心区代码,或组合全部代码块输出最终结果 if (cfg.record_c_code) { gr.recorded_c_code = GetSectionStr(Section::Globals) + GetSectionStr(Section::Decls) + GetSectionStr(Section::Impls); } @@ -276,22 +369,23 @@ GenResult CGen::Build(const ParseResult &pr, const CompileConfig &cfg) { void CGen::EmitSpecAccessorBody(const SpecTypeMetadata &meta, bool is_get) { // ---- string keys ---- if (meta.has_string_keys) { + // 用 if 链而非 switch:常量字符串 ID 存在静态变量里(见 ConstStrRef), + // 不是编译期常量表达式,不能作为 case 标签。字段数很少,if 链完全够用。 Out() << " if (LIKELY(k.type_ == VAR_STRINGID)) {\n"; - Out() << " switch (k.data_.i) {\n"; int f_idx = 0; for (const auto &f: meta.fields) { if (f.key_kind == TableKeyKind::kString) { + const auto id = ConstStrRef(f.key); if (is_get) { - Out() << " case " << s_->GetConstString().Alloc(f.key) << ": *__finish = true; return s->" << f.c_field_name << ";\n"; + Out() << " if (k.data_.i == " << id << ") { *__finish = true; return s->" << f.c_field_name << "; }\n"; } else { - Out() << " case " << s_->GetConstString().Alloc(f.key) << ": s->" << f.c_field_name << " = v; tbl->spec_vals[" << f_idx << "] = v; tbl->spec_keys[" << f_idx - << "] = k; *__finish = true; return;\n"; + Out() << " if (k.data_.i == " << id + << ") { s->" << f.c_field_name << " = v; tbl->spec_vals[" << f_idx << "] = v; tbl->spec_keys[" << f_idx + << "] = k; *__finish = true; return; }\n"; } } f_idx++; } - Out() << " default: break;\n"; - Out() << " }\n"; Out() << " } else if (k.type_ == VAR_STRING) {\n"; Out() << " VarString *__vs = k.data_.s;\n"; f_idx = 0; @@ -301,7 +395,7 @@ void CGen::EmitSpecAccessorBody(const SpecTypeMetadata &meta, bool is_get) { Out() << " if (__vs->size_ == " << f.key.size() << " && memcmp(__vs->data_, \"" << EscapeCStringLiteral(f.key) << "\", " << f.key.size() << ") == 0) { *__finish = true; return s->" << f.c_field_name << "; }\n"; } else { - const auto id = s_->GetConstString().Alloc(f.key); + const auto id = ConstStrRef(f.key); Out() << " if (__vs->size_ == " << f.key.size() << " && memcmp(__vs->data_, \"" << EscapeCStringLiteral(f.key) << "\", " << f.key.size() << ") == 0) { s->" << f.c_field_name << " = v; tbl->spec_vals[" << f_idx << "] = v; tbl->spec_keys[" << f_idx << "] = (CVar){.type_ = VAR_STRINGID, .data_.i = " << id << "}; *__finish = true; return; }\n"; } @@ -442,18 +536,40 @@ void CGen::GenerateHeader() { )"; - // 硬编码 state 指针到生成的 C 代码中。 - // 生命周期保证:生成的代码最终由 TCCState/GCC 产物承载,这些产物通过 TCCHandle/JITHandle - // 被 VmFunction 以 shared_ptr 持有,而 VmFunction 注册在本 State 的 Vm 中(见 - // src/jit/tcc_jit.cpp 与 src/jit/vm_function.h)。因此生成代码里的 _S 永远不会 - // 比其宿主 State 活得更久——State 析构即销毁 Vm,进而释放 handle 和代码页。 - // 同一份代码也不会被跨 State 复用(每次编译都会重新生成)。 - Out() << "static void * _S = (void *) " << s_ << ";\n"; + // State 指针不在此硬编码。 + // + // 早期实现直接把 State 地址写死进生成代码(`static void * _S = (void *) ;`), + // 但这会让同一份输入编译出不同字节:地址受 ASLR 影响,每次都不同。后果有二: + // 1. 构建不可复现,产物无法按内容缓存(编译缓存永远无法命中); + // 2. 一旦产物被跨 State 复用,_S 会指向别的 State,造成内存越界。 + // + // 改为初值 nullptr,由宿主在加载产物后、调用 __fakelua_init 之前通过 + // __fakelua_set_state 注入。这样生成代码对同一输入始终产生相同字节, + // 既可复现构建,也可安全地被缓存复用。 + // + // 生命周期保证:注入的 State 由 Vm 持有 handle 与代码页,State 析构即销毁 Vm, + // 因此 _S 不会比宿主 State 活得更久(见 src/jit/vm_function.h)。 + // 注意:TCC 以 C89 模式编译,不能用 nullptr / 声明混用等 C++ 写法。 + Out() << "static void * _S = 0;\n"; + Out() << "void __fakelua_set_state(void * s) { _S = s; }\n"; Out() << "static bool __fakelua_init_flag__ = false;\n"; + // 常量字符串 ID 不内联进代码,改为在 __fakelua_const_init 里注册后存入静态变量。 + // + // 内联 ID 来自进程级递增计数器,同一份 Lua 每次编译得到的数字都不同 —— + // 这会让构建不可复现,也让按内容缓存编译产物永远无法命中。 + // + // 宿主在注入 State 之后、调用 __fakelua_init 之前调用本函数完成注册, + // 因此这些静态变量在任何业务代码执行前就已就绪。 + Out() << "void __fakelua_const_init(void);\n"; + // C 运行时类型定义、宏 and 函数(从 c_runtime_header.h 提取,便于独立维护) Out() << kCRuntimeHeader; + // 常量字符串 ID 静态变量的插入锚点。Build 收尾时会把声明插到这行之后 —— + // 那时变量名已齐全,且此处保证晚于 #include、早于所有使用点。 + Out() << "\n// __fakelua_const_str_decls__\n"; + // 前置发射所有 spec 类型的 typedef + 特化 get/set 函数(读取 TypeInferencer 预计算的 // ir.spec_type_metadata)。CGen 不再在 CompileTableconstructor 里懒发射这些样板代码。 for (const auto &[spec_type, meta]: ir().spec_type_metadata) { @@ -518,7 +634,15 @@ void CGen::GenerateGlobal(const SyntaxTreeInterfacePtr &chunk) { // 注意:这里不能加 const,因为 init 函数里需要赋值。 // CONST_FLAG 会在 init 函数赋值后由 CompileStmtAssign 注入。 const std::string cvar_init = exp ? CompileExp(exp) : "(CVar){.type_ = VAR_NIL}"; - Out() << "static CVar " << cname << " = " << cvar_init << ";\n"; + // 常量字符串 ID 存在静态变量里,而静态变量不是编译期常量, + // 不能出现在静态初始化器中。这种情况下改为零初始化 + + // 在 __fakelua_const_init 里赋值。 + if (cvar_init.find("__fl_cstr_") != std::string::npos) { + const_str_deferred_inits_.push_back(std::format(" {} = {};\n", cname, cvar_init)); + Out() << "static CVar " << cname << " = (CVar){.type_ = VAR_NIL};\n"; + } else { + Out() << "static CVar " << cname << " = " << cvar_init << ";\n"; + } // 全局非数值变量(表/闭包)记录到集合,后续在 init 函数赋值后注入 CONST_FLAG。 global_const_table_vars_.insert(cname); } @@ -2305,7 +2429,7 @@ std::string CGen::CompileExp(const SyntaxTreeInterfacePtr &exp, bool preserve_mu return std::format("(CVar){{.type_ = VAR_FLOAT, .data_.f = {}}}", ToFloat(value)); } case ExpKind::kString: - return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc(value)); + return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", ConstStrRef(value)); case ExpKind::kPrefixExp: return CompilePrefixexp(e->Right(), preserve_multi); case ExpKind::kTableConstructor: @@ -2404,7 +2528,7 @@ std::string CGen::CompileTableconstructor(const SyntaxTreeInterfacePtr &tc) { if (fp->GetFieldKind() == FieldKind::kObject) { const auto key_name = fp->Name(); - const auto id = s_->GetConstString().Alloc(key_name); + const auto id = ConstStrRef(key_name); Out() << GenTab() << std::format("FlSetTableStrId({}, {}, {});\n", var_name, id, value_str); } else { if (fp->Key() == nullptr) { @@ -2417,7 +2541,7 @@ std::string CGen::CompileTableconstructor(const SyntaxTreeInterfacePtr &tc) { if (ClassifyLiteralKey(fp->Key(), key_info)) { switch (key_info.kind) { case TableKeyKind::kString: { - const auto id = s_->GetConstString().Alloc(key_info.repr); + const auto id = ConstStrRef(key_info.repr); Out() << GenTab() << std::format("FlSetTableStrId({}, {}, {});\n", var_name, id, value_str); break; } @@ -2483,13 +2607,13 @@ std::string CGen::CompileTableconstructor(const SyntaxTreeInterfacePtr &tc) { if (fkind == FieldKind::kObject) { const auto name = field_ptr->Name(); - const auto id = s_->GetConstString().Alloc(name); + const auto id = ConstStrRef(name); Out() << GenTab() << std::format("FlSetTableStrId({}, {}, {});\n", var_name, id, value_str); } else { DEBUG_ASSERT(fkind == FieldKind::kArray); if (const auto key = field_ptr->Key()) { if (const auto key_exp = std::dynamic_pointer_cast(key); key_exp && key_exp->GetExpKind() == ExpKind::kString) { - const auto id = s_->GetConstString().Alloc(key_exp->ExpValue()); + const auto id = ConstStrRef(key_exp->ExpValue()); Out() << GenTab() << std::format("FlSetTableStrId({}, {}, {});\n", var_name, id, value_str); } else { if (const auto key_type = GetType(key); key_type == T_INT) { @@ -2911,7 +3035,7 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { // 拦截 _VERSION 全局常量(编译期替换为 "Fakelua x.y.z") if (name == "_VERSION") { std::string ver = std::string("Fakelua ") + FAKELUA_VERSION_STRING; - return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc(ver)); + return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", ConstStrRef(ver)); } // 未声明/未定义的简单变量:Lua 语义中未定义变量求值为 nil。 @@ -2940,7 +3064,7 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { } switch (info.kind) { case TableKeyKind::kString: { - const auto id = s_->GetConstString().Alloc(info.repr); + const auto id = ConstStrRef(info.repr); return std::format("FlGetTableStrId({}, {})", pe_ret, id); } case TableKeyKind::kInt: @@ -2998,7 +3122,7 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { p.push_back(']'); return p; }(); - return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", s_->GetConstString().Alloc(kCharPattern)); + return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", ConstStrRefBinary(kCharPattern)); } } // 拦截 utf8 静态库常量访问 (utf8.charpattern) @@ -3025,7 +3149,7 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { return p; }(); return std::format("(CVar){{.type_ = VAR_STRINGID, .data_.i = {}}}", - s_->GetConstString().Alloc(kUtf8Pattern)); + ConstStrRefBinary(kUtf8Pattern)); } } } @@ -3041,7 +3165,7 @@ std::string CGen::CompileVar(const SyntaxTreeInterfacePtr &v) { } // String constant key fast path: use FlGetTableStrId directly. - const auto id = s_->GetConstString().Alloc(name); + const auto id = ConstStrRef(name); return std::format("FlGetTableStrId({}, {})", pe_ret, id); } } @@ -3618,8 +3742,8 @@ std::string CGen::TryCompileBuiltinMathCall(const std::shared_ptrGetConstString().Alloc(info.repr); + const auto id = ConstStrRef(info.repr); Out() << GenTab() << std::format("{}.data_.t->spec_keys[{}] = (CVar){{.type_ = VAR_STRINGID, .data_.i = {}}};\n", tbl_str, index, id); break; } @@ -4574,7 +4698,7 @@ std::string CGen::TryCompileSetTableCall(const std::shared_ptrGetConstString().Alloc(info.repr); + const auto id = ConstStrRef(info.repr); Out() << GenTab() << std::format("FlSetTableStrId({}, {}, {});\n", tbl_str, id, val_str); break; } @@ -4767,7 +4891,7 @@ std::string CGen::BuildMethodCall(const std::shared_ptr // 避免 FlGetTableStrId 对非表值(如 string)触发 "attempt to index a non-table value"。 return forwarded; } else { - const auto id = s_->GetConstString().Alloc(method_name); + const auto id = ConstStrRef(method_name); callee_expr = std::format("FlGetTableStrId({}, {})", obj_tmp, id); } diff --git a/src/compile/c_gen.h b/src/compile/c_gen.h index b5a0c52..8075245 100644 --- a/src/compile/c_gen.h +++ b/src/compile/c_gen.h @@ -265,6 +265,21 @@ class CGen { // 根据当前缩进深度生成 C 代码缩进空白字符 [[nodiscard]] std::string GenTab() const; + // 返回一个「常量字符串 ID 的静态变量名」,并登记一条 __fakelua_const_init 注册语句。 + // + // 不能把 ConstString::Alloc 的返回值直接内联进代码:那是进程级递增 ID, + // 同一份输入每次编译得到的数字都不同,构建既不可复现也无法按内容缓存。 + // 改为在 __fakelua_const_init 里注册后写入静态变量,代码其他地方引用变量名。 + // 相同的字符串会复用同一个变量。 + [[nodiscard]] const char *ConstStrRef(const std::string_view &str); + + // 与 ConstStrRef 相同,但用于含非文本字节(如 string.charpattern 里的 \0 与 0xFF) + // 的字符串。这类字节无法用 C 字符串字面量表达(\0 会截断),改成长度 + 字节数组常量。 + [[nodiscard]] const char *ConstStrRefBinary(const std::string_view &str); + + // 发射 __fakelua_const_init 的定义(含此前登记的全部注册语句)。 + void EmitConstInit(); + // 返回当前 active 代码段(section)的输出流引用 std::ostream &Out() { return sections_[static_cast(cur_section_)]; @@ -292,6 +307,11 @@ class CGen { Section cur_section_ = Section::Headers;// 当前输出对应的 CGen 逻辑代码段 std::array(Section::Count)> sections_;// C 代码分区输出流数组 + std::unordered_map const_str_var_of_;// 字符串 -> 生成的静态变量名(去重) + std::vector const_str_regs_;// __fakelua_const_init 的注册语句 + std::vector const_str_decls_;// 常量字符串 ID 的静态变量声明(Build 收尾时前置到头段) + std::vector const_str_binary_;// 二进制常量(长度 + 字节数组)定义 + std::vector const_str_deferred_inits_;// 依赖常量字符串 ID 的全局变量,改为在 const_init 里赋值 const struct SpecFuncContext *cur_spec_ctx_ = nullptr;// 当前特化版本的上下文(func_name/bitmask/snapshot) diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index d263477..f023605 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -339,6 +339,10 @@ extern CVar FakeluaInterpCall(State *state, VarClosure *cl, int arg_num, const C // pop 恢复。native 对象方法等在闭包内执行时据此登记派回本引擎的异步回调。 extern int FakeluaJitContextPush(State *s, int jit); extern void FakeluaJitContextPop(State *s, int prev); +// 把字符串注册进当前 State 的常量池并返回其 ID。生成代码用它替代内联常量 ID, +// 使同一份输入编译出相同字节(可复现构建 + 可缓存)。 +extern int64_t FakeluaConstStrAlloc(State *s, const char *str); +extern int64_t FakeluaConstStrAllocN(State *s, const char *str, size_t len); #ifdef __cplusplus } #endif diff --git a/src/compile/compile_common.h b/src/compile/compile_common.h index cf21dcb..c4ccd28 100644 --- a/src/compile/compile_common.h +++ b/src/compile/compile_common.h @@ -20,6 +20,14 @@ namespace fakelua { inline constexpr const char *kInitFunctionName = "__fakelua_init"; +// 宿主在加载产物后、调用 kInitFunctionName 之前调用它注入 State 指针。 +// 生成代码不再硬编码 State 地址,从而保证同一输入产生相同字节(可复现构建 + 可缓存)。 +inline constexpr const char *kSetStateFunctionName = "__fakelua_set_state"; + +// 注册生成代码里用到的常量字符串 ID。必须在注入 State 之后、init 之前调用: +// 生成代码把这些 ID 存在静态变量里,业务代码直接引用变量。 +inline constexpr const char *kConstInitFunctionName = "__fakelua_const_init"; + // Lua 标识符已是 [A-Za-z_][A-Za-z0-9_]*,用作 C 标识符时只需避开关键字和运行时保留名。 inline std::string SanitizeCIdent(const std::string &lua_name) { static const std::unordered_set kReserved = { diff --git a/src/jit/gcc_cache.cpp b/src/jit/gcc_cache.cpp new file mode 100644 index 0000000..8fa573e --- /dev/null +++ b/src/jit/gcc_cache.cpp @@ -0,0 +1,73 @@ +#include "jit/gcc_cache.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fakelua { + +namespace { + +// 缓存格式版本。参与摘要计算,便于将来改变布局时让旧缓存自然失效。 +constexpr int kCacheFormatVersion = 1; + +std::string ToHex(std::uint64_t v) { + static const char* kDigits = "0123456789abcdef"; + std::string out(16, '0'); + for (int i = 15; i >= 0; i--) { + out[static_cast(i)] = kDigits[v & 0xF]; + v >>= 4; + } + return out; +} + +}// namespace + +std::string ComputeGccCacheKey(std::string_view c_code, std::string_view compile_flags) { + boost::hash2::xxhash_64 hasher; + + // 长度前缀,避免 "ab"+"c" 与 "a"+"bc" 撞成同一个键。 + const auto mix = [&hasher](std::string_view s) { + const auto len = static_cast(s.size()); + hasher.update(reinterpret_cast(&len), sizeof(len)); + hasher.update(reinterpret_cast(s.data()), s.size()); + }; + + mix(std::to_string(kCacheFormatVersion)); + mix(c_code); + // 编译参数(-O0/-O3、include/library 路径等)不同则产物不同,必须进键。 + mix(compile_flags); + + return ToHex(hasher.result()); +} + +std::string GccCacheDir() { + boost::system::error_code ec; + auto dir = boost::filesystem::temp_directory_path(ec); + if (ec) { + dir = boost::filesystem::path("."); + } + // 刻意不放在 /fakelua/ 下:那个目录属于 GenerateTmpFilename 的临时产物, + // 测试(如 runtime.generate_tmp_filename_creates_dir)会 remove_all 整个目录, + // 连带把编译缓存一起清掉。独立命名可避免这种互相干扰。 + dir /= "fakelua-jit-cache"; + boost::filesystem::create_directories(dir, ec); + if (ec && !boost::filesystem::is_directory(dir)) { + return {}; + } + return dir.string(); +} + +std::string GccCacheArtifactPath(std::string_view key, std::string_view suffix) { + const std::string dir = GccCacheDir(); + if (dir.empty()) { + return {}; + } + return (boost::filesystem::path(dir) / (std::string(key) + std::string(suffix))).string(); +} + +}// namespace fakelua diff --git a/src/jit/gcc_cache.h b/src/jit/gcc_cache.h new file mode 100644 index 0000000..3412c5f --- /dev/null +++ b/src/jit/gcc_cache.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace fakelua { + +// GCC JIT 编译产物的磁盘缓存。 +// +// 背景:在 macOS 上,首次 dlopen 一个新路径的 dylib 需要约 4.5~5.4 秒走代码签名验证 +// (amfid / syspolicyd IPC),而同路径的后续加载只要约 0.2 毫秒。当同一份 C 代码被 +// 反复编译时(例如测试里 JIT 类型 × debug 模式的组合),这笔固定开销会成倍累积, +// 使测试套件慢到不可用。 +// +// 做法:以「生成的 C 代码 + 影响产物的编译参数」的摘要作为缓存键,命中时直接复用 +// 同一路径的 .so,从而命中 dyld 的路径缓存。实测 dlclose 之后重新 dlopen 同一路径 +// 仍是 0.2 毫秒,说明 dyld 的记账按路径且不随 dlclose 失效,因此缓存文件可以走 +// 正常的 GCCHandle 生命周期,无需常驻内存。 + +// 计算缓存摘要。相同输入必定返回相同字符串。 +std::string ComputeGccCacheKey(std::string_view c_code, std::string_view compile_flags); + +// 返回缓存目录下该摘要对应的产物路径(不保证文件已存在)。 +// suffix 为 ".dylib" / ".so" / ".dll"。 +std::string GccCacheArtifactPath(std::string_view key, std::string_view suffix); + +// 返回缓存根目录,并确保目录已创建。失败时返回空字符串表示不可用。 +std::string GccCacheDir(); + +}// namespace fakelua diff --git a/src/jit/gcc_jit.cpp b/src/jit/gcc_jit.cpp index 949d361..d0095fb 100644 --- a/src/jit/gcc_jit.cpp +++ b/src/jit/gcc_jit.cpp @@ -1,4 +1,5 @@ #include "jit/gcc_jit.h" +#include "jit/gcc_cache.h" #include "jit/gcc_handle.h" #include "state/state.h" #include "util/file_util.h" @@ -28,6 +29,16 @@ namespace { constexpr size_t kCExtLen = 2;// 文件扩展名 ".c" 的长度 constexpr int kExecFailedStatus = 127; +// JIT 产物的动态库扩展名。 +constexpr const char *kSoSuffix = +#if defined(_WIN32) + ".dll"; +#elif defined(__APPLE__) + ".dylib"; +#else + ".so"; +#endif + std::string JoinCommand(const std::vector &args) { std::ostringstream oss; for (size_t i = 0; i < args.size(); ++i) { @@ -76,36 +87,64 @@ std::vector GetWindowsDefaultLibraryPaths() { #endif }// namespace +// 产物加载完成后的统一收尾:注入 State → 注册函数 → 调用 init。 +// +// 单独抽出是因为缓存命中与正常编译两条路径都要走这一步, +// 放在一处可以避免两条路径的行为漂移。 +void GccJitter::RegisterAndInit(const ParseResult &pr, const GenResult &gr, + const std::shared_ptr &handle, + const std::function &dlsym_lambda, + const std::string &artifact_path) { + // 生成代码里 _S 初值为 0,必须在 init(以及任何函数调用)之前注入。 + void *set_state_ptr = dlsym_lambda(kSetStateFunctionName); + if (!set_state_ptr) { + ThrowFakeluaException(std::format("GCC compile failed, symbol resolution failed for {} in {}", kSetStateFunctionName, artifact_path)); + } + reinterpret_cast(set_state_ptr)(s_); + + // 注册常量字符串 ID(生成代码把它们存在静态变量里)。 + // 只有用到字符串表键时才会生成该函数,此时必须有。 + if (void *const_init_ptr = dlsym_lambda(kConstInitFunctionName)) { + reinterpret_cast(const_init_ptr)(); + } + + LOG_DEBUG(s_, "engine", "GCC: registering {} functions from {}", gr.function_names.size(), artifact_path); + for (const auto &[name, info]: gr.function_names) { + const std::string &sym = info.c_symbol_name.empty() ? name : info.c_symbol_name; + void *func_ptr = dlsym_lambda(sym); + if (!func_ptr) { + ThrowFakeluaException(std::format("GCC compile failed, symbol resolution failed for {} in {}", name, artifact_path)); + } + s_->GetVM().RegisterFunction(VmFunction(name, info.params_count, JIT_GCC, func_ptr, handle, info.is_vararg)); + LOG_DEBUG(s_, "engine", "Registered gcc function {} with {} params (vararg: {}) at address {}", name, info.params_count, info.is_vararg, func_ptr); + } + + void *init_ptr = dlsym_lambda(kInitFunctionName); + if (init_ptr) { + State::ConstAllocScope const_alloc(s_); + inter::DispatchCall(s_, init_ptr, nullptr, 0, JIT_GCC); + } +} + GccJitter::GccJitter(State *s) : s_(s) { } void GccJitter::Compile(const ParseResult &pr, const GenResult &gr, const CompileConfig &cfg) { LOG_INFO(s_, "engine", "GCC JIT compile start: {}, {} functions", pr.file_name, gr.function_names.size()); - const std::string c_file = GenerateTmpFilename("fakelua_jit_", ".c"); - const std::string so_file = c_file.substr(0, c_file.size() - kCExtLen) + -#if defined(_WIN32) - ".dll"; -#elif defined(__APPLE__) - ".dylib"; -#else - ".so"; -#endif - const std::string log_file = c_file.substr(0, c_file.size() - kCExtLen) + ".gcc.log"; - // RAII cleanup for temp files on early error paths (before GCCHandle takes ownership). - const auto CleanupOnError = [&]() { - std::error_code ec; - std::filesystem::remove(c_file, ec); - std::filesystem::remove(so_file, ec); - std::filesystem::remove(log_file, ec); - }; + const auto &gcc_cfg = s_->GetStateConfig().gcc_config; + const bool use_cache = gcc_cfg.enable_compile_cache; - if (std::ofstream ofs(c_file); !ofs.is_open()) { - ThrowFakeluaException(std::format("GCC compile failed, cannot open c file {}", c_file)); - } else { - ofs << gr.c_code; - ofs.close(); - } + std::string c_file; + std::string so_file; + std::string log_file; + // 命中缓存时为 true:此时产物归缓存目录所有,析构时不能删。 + bool artifact_owned_by_cache = false; + // 复用已有产物,跳过写 .c 与 gcc 调用。 + bool reuse_cached_artifact = false; + + std::string cache_key; + std::string cache_so_file; std::vector args; args.emplace_back("gcc"); @@ -138,6 +177,63 @@ void GccJitter::Compile(const ParseResult &pr, const GenResult &gr, const Compil for (const auto &lib: s_->GetStateConfig().gcc_config.libraries) { args.emplace_back("-l" + lib); } + + // ---- 编译产物缓存 ---- + // + // 缓存键包含生成的 C 代码与全部影响产物的编译参数,因此不同 debug_mode / + // 不同 include / library 配置会各自落到不同的缓存条目,不会互相污染。 + // + // 命中时复用同一路径的 .so,从而命中 dyld 的路径缓存(macOS 上首次加载一个新 + // 路径需要约 4.5~5.4 秒走代码签名验证,同路径后续约 0.2 毫秒)。 + if (use_cache) { + cache_key = ComputeGccCacheKey(gr.c_code, JoinCommand(args)); + cache_so_file = GccCacheArtifactPath(cache_key, kSoSuffix); + if (!cache_so_file.empty() && std::filesystem::exists(cache_so_file)) { + reuse_cached_artifact = true; + } + } + + if (reuse_cached_artifact) { + so_file = cache_so_file; + c_file.clear(); + log_file.clear(); + // 缓存文件归缓存目录所有,GCCHandle 析构时不得删除,否则缓存永不生效。 + artifact_owned_by_cache = true; + LOG_DEBUG(s_, "engine", "GCC: cache hit for {} -> {}", pr.file_name, cache_key); + } else { + c_file = GenerateTmpFilename("fakelua_jit_", ".c"); + log_file = c_file.substr(0, c_file.size() - kCExtLen) + ".gcc.log"; + so_file = c_file.substr(0, c_file.size() - kCExtLen) + kSoSuffix; + + if (std::ofstream ofs(c_file); !ofs.is_open()) { + ThrowFakeluaException(std::format("GCC compile failed, cannot open c file {}", c_file)); + } else { + ofs << gr.c_code; + ofs.close(); + } + + if (use_cache) { + // 编译成功后把产物放进缓存,下次同样输入即可命中。 + // 写入采用「先临时文件再 rename」的原子替换,避免并发进程读到半截文件。 + artifact_owned_by_cache = true; + } + } + + // RAII cleanup for temp files on early error paths (before GCCHandle takes ownership). + // 缓存命中时不删任何东西:产物归缓存目录所有,临时文件本来也没创建。 + const auto CleanupOnError = [&]() { + std::error_code ec; + if (!c_file.empty()) { + std::filesystem::remove(c_file, ec); + } + if (!so_file.empty() && !reuse_cached_artifact) { + std::filesystem::remove(so_file, ec); + } + if (!log_file.empty()) { + std::filesystem::remove(log_file, ec); + } + }; + args.emplace_back("-o"); args.emplace_back(so_file); args.emplace_back(c_file); @@ -153,24 +249,59 @@ void GccJitter::Compile(const ParseResult &pr, const GenResult &gr, const Compil return argv; }; + // 编译成功后把产物原子地放进缓存目录。失败时静默忽略 —— 缓存只是优化, + // 不该让编译本身失败。 + const auto PublishToCache = [&]() { + if (!artifact_owned_by_cache || cache_so_file.empty()) { + return; + } + std::error_code ec; + std::filesystem::rename(so_file, cache_so_file, ec); + if (ec) { + // 跨设备等场景下 rename 可能失败,退化为拷贝后删除。 + ec.clear(); + std::filesystem::copy_file(so_file, cache_so_file, std::filesystem::copy_options::overwrite_existing, ec); + if (!ec) { + std::filesystem::remove(so_file, ec); + ec.clear(); + } + } + if (ec) { + // 缓存写入失败则本次仍按普通临时文件处理,避免产物被误删。 + artifact_owned_by_cache = false; + LOG_DEBUG(s_, "engine", "GCC: cache write failed for {}: {}", pr.file_name, ec.message()); + } + }; + #if defined(_WIN32) auto argv = build_argv(); - const int compile_status = _spawnvp(_P_WAIT, "gcc", argv.data()); - if (compile_status == -1) { - CleanupOnError(); - ThrowFakeluaException(std::format("GCC compile failed for {}: cannot execute gcc (errno {}: {}). cmd: {}", pr.file_name, errno, std::strerror(errno), JoinCommand(args))); - } - if (compile_status != 0) { - CleanupOnError(); - ThrowFakeluaException(std::format("GCC compile failed for {} with exit code {}. cmd: {}", pr.file_name, compile_status, JoinCommand(args))); + if (!reuse_cached_artifact) { + const int compile_status = _spawnvp(_P_WAIT, "gcc", argv.data()); + if (compile_status == -1) { + CleanupOnError(); + ThrowFakeluaException(std::format("GCC compile failed for {}: cannot execute gcc (errno {}: {}). cmd: {}", pr.file_name, errno, std::strerror(errno), JoinCommand(args))); + } + if (compile_status != 0) { + CleanupOnError(); + ThrowFakeluaException(std::format("GCC compile failed for {} with exit code {}. cmd: {}", pr.file_name, compile_status, JoinCommand(args))); + } + PublishToCache(); } - HMODULE module_handle = LoadLibraryA(so_file.c_str()); + // 产物若已进缓存,实际路径变成了 cache_so_file。 + const std::string load_path = artifact_owned_by_cache ? cache_so_file : so_file; + + HMODULE module_handle = LoadLibraryA(load_path.c_str()); if (!module_handle) { CleanupOnError(); - ThrowFakeluaException(std::format("GCC compile failed, LoadLibrary failed for {}: {}", so_file, WinErrToString(GetLastError()))); + ThrowFakeluaException(std::format("GCC compile failed, LoadLibrary failed for {}: {}", load_path, WinErrToString(GetLastError()))); } - const auto handle = std::make_shared(c_file, so_file, log_file, module_handle); + // 产物归缓存所有时传空路径,GCCHandle 析构便不会删除缓存文件。 + const auto handle = std::make_shared( + artifact_owned_by_cache ? std::string{} : c_file, + artifact_owned_by_cache ? std::string{} : so_file, + artifact_owned_by_cache ? std::string{} : log_file, + module_handle); // Common post-load: register compiled functions + call init. auto dlsym_lambda = [&](const std::string &sym) -> void * { @@ -179,12 +310,41 @@ void GccJitter::Compile(const ParseResult &pr, const GenResult &gr, const Compil }; #else auto argv = build_argv(); - const int log_fd = open(log_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (log_fd < 0) { - CleanupOnError(); - ThrowFakeluaException(std::format("GCC compile failed, cannot open log file {}", log_file)); + + // 缓存命中:直接加载已缓存的产物,跳过 gcc 调用。 + // 若加载失败(文件被清理或损坏),删除该条目、退回未命中路径重新编译, + // 而不是直接失败——缓存只是优化,不该成为故障源。 + if (reuse_cached_artifact) { + void *cached = dlopen(so_file.c_str(), RTLD_NOW | RTLD_LOCAL); + if (cached) { + const auto handle = std::make_shared(std::string{}, std::string{}, std::string{}, cached); + auto dlsym_cached = [&](const std::string &sym) -> void * { return dlsym(cached, sym.c_str()); }; + RegisterAndInit(pr, gr, handle, dlsym_cached, so_file); + return; + } + LOG_DEBUG(s_, "engine", "GCC: stale cache entry {}, recompiling", cache_key); + std::error_code rm_ec; + std::filesystem::remove(so_file, rm_ec); + reuse_cached_artifact = false; + artifact_owned_by_cache = true; + c_file = GenerateTmpFilename("fakelua_jit_", ".c"); + log_file = c_file.substr(0, c_file.size() - kCExtLen) + ".gcc.log"; + so_file = c_file.substr(0, c_file.size() - kCExtLen) + kSoSuffix; + if (std::ofstream ofs(c_file); ofs.is_open()) { + ofs << gr.c_code; + } + args[args.size() - 2] = so_file; + args[args.size() - 1] = c_file; + argv = build_argv(); } + // 未命中(或缓存失效回退):走完整编译流程。 + { + const int log_fd = open(log_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (log_fd < 0) { + CleanupOnError(); + ThrowFakeluaException(std::format("GCC compile failed, cannot open log file {}", log_file)); + } // Block SIGCHLD before fork to prevent the default handler from turning // the child into a zombie before we get to waitpid. sigset_t old_mask, block_mask; @@ -227,33 +387,29 @@ void GccJitter::Compile(const ParseResult &pr, const GenResult &gr, const Compil ThrowFakeluaException(std::format("GCC compile failed for {} (log: {})\n{}", pr.file_name, log_file, gcc_log)); } - void *dl_handle = dlopen(so_file.c_str(), RTLD_NOW | RTLD_LOCAL); + PublishToCache(); + + // 产物若已进缓存,实际路径变成了 cache_so_file。 + const std::string load_path = artifact_owned_by_cache ? cache_so_file : so_file; + + void *dl_handle = dlopen(load_path.c_str(), RTLD_NOW | RTLD_LOCAL); if (!dl_handle) { CleanupOnError(); - ThrowFakeluaException(std::format("GCC compile failed, dlopen failed for {}: {}", so_file, dlerror())); + ThrowFakeluaException(std::format("GCC compile failed, dlopen failed for {}: {}", load_path, dlerror())); } - const auto handle = std::make_shared(c_file, so_file, log_file, dl_handle); - auto dlsym_lambda = [&](const std::string &sym) -> void * { return dlsym(dl_handle, sym.c_str()); }; -#endif + // 产物归缓存所有时传空路径,GCCHandle 析构便不会删除缓存文件。 + const auto handle = std::make_shared( + artifact_owned_by_cache ? std::string{} : c_file, + artifact_owned_by_cache ? std::string{} : so_file, + artifact_owned_by_cache ? std::string{} : log_file, + dl_handle); - // ---- Common post-load: register compiled functions + call init ---- - LOG_DEBUG(s_, "engine", "GCC: registering {} functions from {}", gr.function_names.size(), so_file); - for (const auto &[name, info]: gr.function_names) { - const std::string &sym = info.c_symbol_name.empty() ? name : info.c_symbol_name; - void *func_ptr = dlsym_lambda(sym); - if (!func_ptr) { - ThrowFakeluaException(std::format("GCC compile failed, symbol resolution failed for {} in {}", name, so_file)); - } - s_->GetVM().RegisterFunction(VmFunction(name, info.params_count, JIT_GCC, func_ptr, handle, info.is_vararg)); - LOG_DEBUG(s_, "engine", "Registered gcc function {} with {} params (vararg: {}) at address {}", name, info.params_count, info.is_vararg, func_ptr); - } + auto dlsym_lambda = [&](const std::string &sym) -> void * { return dlsym(dl_handle, sym.c_str()); }; - void *init_ptr = dlsym_lambda(kInitFunctionName); - if (init_ptr) { - State::ConstAllocScope const_alloc(s_); - inter::DispatchCall(s_, init_ptr, nullptr, 0, JIT_GCC); - } + RegisterAndInit(pr, gr, handle, dlsym_lambda, artifact_owned_by_cache ? cache_so_file : so_file); + }// 编译 + 加载 +#endif #if !defined(_WIN32) LOG_INFO(s_, "engine", "GCC JIT compilation finished for {}", pr.file_name); diff --git a/src/jit/gcc_jit.h b/src/jit/gcc_jit.h index 7aaf72d..761a51d 100644 --- a/src/jit/gcc_jit.h +++ b/src/jit/gcc_jit.h @@ -2,6 +2,9 @@ #include "compile/compile_common.h" #include "fakelua.h" +#include +#include +#include namespace fakelua { @@ -14,6 +17,13 @@ class GccJitter { void Compile(const ParseResult &pr, const GenResult &gr, const CompileConfig &cfg); private: + // 产物加载完成后的统一收尾:注入 State → 注册函数 → 调用 init。 + // 缓存命中与正常编译两条路径共用,避免行为漂移。 + void RegisterAndInit(const ParseResult &pr, const GenResult &gr, + const std::shared_ptr &handle, + const std::function &dlsym_lambda, + const std::string &artifact_path); + State *s_; }; diff --git a/src/jit/tcc_jit.cpp b/src/jit/tcc_jit.cpp index 5ef0452..d926780 100644 --- a/src/jit/tcc_jit.cpp +++ b/src/jit/tcc_jit.cpp @@ -38,6 +38,18 @@ void TccJitter::Compile(const ParseResult &pr, const GenResult &gr, const Compil LOG_DEBUG(s_, "engine", "Registered function {} with {} params (vararg: {}) at address {}", name, info.params_count, info.is_vararg, func_ptr); } + // 生成代码里 _S 初值为 nullptr,必须在 init(以及任何函数调用)之前注入。 + void *set_state_ptr = tcc_get_symbol(s, kSetStateFunctionName); + if (!set_state_ptr) { + ThrowFakeluaException(std::format("TCC compile failed, tcc_get_symbol failed for symbol {} in {}", kSetStateFunctionName, pr.file_name)); + } + reinterpret_cast(set_state_ptr)(s_); + + // 注册常量字符串 ID(只有用到字符串表键时才会生成该函数)。 + if (void *const_init_ptr = tcc_get_symbol(s, kConstInitFunctionName)) { + reinterpret_cast(const_init_ptr)(); + } + void *init_ptr = tcc_get_symbol(s, kInitFunctionName); if (init_ptr) { State::ConstAllocScope const_alloc(s_); diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index a621781..3336a72 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -52,6 +52,22 @@ extern "C" void FakeluaJitContextPop(State *s, int prev) { s->SetCurrentJit(static_cast(prev)); } +// JIT 生成的 C 代码用它把字符串注册进当前 State 的常量池,返回该 State 内的 ID。 +// +// 为什么需要:生成代码里若直接内联常量字符串 ID,该 ID 来自进程级递增计数器, +// 同一份 Lua 每次编译得到的数字都不同。这会让构建不可复现,也让按内容缓存 +// 编译产物永远无法命中。改为在 __fakelua_init 阶段调用本接口取回 ID 并存入 +// 产物里的静态变量,代码其他地方引用该变量即可。 +extern "C" __attribute__((used)) int64_t FakeluaConstStrAlloc(State *s, const char *str) { + return s->GetConstString().Alloc(std::string_view(str)); +} + +// 含非文本字节的版本(如 string.charpattern 里有 \0 与 0xFF),不能用 C 字符串 +// 字面量传递 —— \0 会截断。显式给长度。 +extern "C" __attribute__((used)) int64_t FakeluaConstStrAllocN(State *s, const char *str, size_t len) { + return s->GetConstString().Alloc(std::string_view(str, len)); +} + static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg_num, const CVar *raw_arg_arr); extern "C" __attribute__((used)) CVar FakeluaCallByName(State *state, int jit_type, const char *name, int arg_num, ...) {