From 40471f5e10f9ce425970aff61b64571d59aed795 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Wed, 30 Sep 2026 08:03:41 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20pitfalls=20fixes=20=E2=80=94=20mysql?= =?UTF-8?q?=20callbacks,=20JIT=20const,=20json=20arrays,=20pool=20lease,?= =?UTF-8?q?=20net=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the 8 issues found by the FakeFish project (fakelua-pitfalls): - P0-1: mysql callbacks now accept inline closures (VarClosure*) in addition to global function names; invalid callback types throw a loud "bad argument" instead of being silently dropped by CVarToString(""). - P0-2: all early-return paths in Query/StmtPrepare/StmtExecute now fire the callback with an error. Contract: every conn:query() call results in exactly one callback. - P1-3: file-level numeric locals with reassignment points are no longer emitted as C const (which broke JIT compilation with "cannot assign to variable with const-qualified type"). Never- reassigned file constants keep the const optimization. - P1-4: in-flight query on the same connection is queued instead of erroring; new pool:with(fn) lease API auto-returns the connection even when fn throws. - P1-5: json.encode({}) now produces [] (pure-array heuristic); new json.encode_array() for strict array-shaped encoding. - P1-6: sends issued inside dispatch callbacks are queued and pumped by the tick after dispatch completes; send failures log WARN. - P2-7: result rows convert values by column type (int/float columns return numbers instead of strings); result table layout documented. - P2-8: tick pump order and callback-context rules documented in native READMEs (zh/en), incl. the allowed/forbidden operation matrix. exception.const_reassign test updated: it asserted the P1-3 bug (compile failure); now expects successful compilation per the new semantics. New regression tests for all the above. --- src/compile/c_gen.cpp | 12 +- src/compile/compile_common.h | 3 + src/compile/type_inferencer.cpp | 20 ++- src/compile/type_inferencer.h | 5 + src/native/README.md | 59 ++++++- src/native/README.zh.md | 63 +++++++- src/native/json/native_json.cpp | 36 ++++- src/native/mysql/mysql_connection.cpp | 147 ++++++++++++++---- src/native/mysql/mysql_connection.h | 34 +++- src/native/mysql/native_mysql.cpp | 38 +++-- src/native/mysql/native_mysql_pool.cpp | 53 ++++++- src/native/net/native_net.cpp | 87 ++++++++++- .../infer/test_global_const_reassigned.lua | 19 +++ test/lua/json/test_json_edge.lua | 33 +++- test/lua/mysql/test_mysql_contract.lua | 97 ++++++++++++ test/lua/net/test_net_server_client.lua | 45 ++++++ test/test_exception.cpp | 8 +- test/test_infer.cpp | 21 +++ test/test_json.cpp | 26 ++++ test/test_mysql.cpp | 38 ++++- test/test_net.cpp | 18 +++ 21 files changed, 776 insertions(+), 86 deletions(-) create mode 100644 test/lua/infer/test_global_const_reassigned.lua create mode 100644 test/lua/mysql/test_mysql_contract.lua diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 5d6dfed..cb585bd 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -488,17 +488,21 @@ void CGen::GenerateGlobal(const SyntaxTreeInterfacePtr &chunk) { InferredType global_type = ir().global_const_vars.at(name); const auto cname = CIdent(name); const auto exp_node = std::dynamic_pointer_cast(exp); + // 声明后仍有再赋值点的变量不能 const:const + 赋值 = C 编译错误。 + // (此前只看推断类型,多赋值点全为编译期常量时会被误标 const。) + const bool reassigned = ir().global_reassigned_vars.contains(name); + const std::string qualifier = reassigned ? "static " : "static const "; if (global_type == T_INT) { if (!exp_node || exp_node->GetExpKind() == ExpKind::kNil) { - Out() << "static const int64_t " << cname << " = 0;\n"; + Out() << qualifier << "int64_t " << cname << " = 0;\n"; } else { - Out() << "static const int64_t " << cname << " = " << CompileNumericExp(exp) << ";\n"; + Out() << qualifier << "int64_t " << cname << " = " << CompileNumericExp(exp) << ";\n"; } } else if (global_type == T_FLOAT) { if (!exp_node || exp_node->GetExpKind() == ExpKind::kNil) { - Out() << "static const double " << cname << " = 0.0;\n"; + Out() << qualifier << "double " << cname << " = 0.0;\n"; } else { - Out() << "static const double " << cname << " = " << CompileNumericExp(exp) << ";\n"; + Out() << qualifier << "double " << cname << " = " << CompileNumericExp(exp) << ";\n"; } } else { // 非数值字面量:保留 static CVar 形式。 diff --git a/src/compile/compile_common.h b/src/compile/compile_common.h index 44ff6a6..a61f111 100644 --- a/src/compile/compile_common.h +++ b/src/compile/compile_common.h @@ -584,6 +584,9 @@ struct InferResult { EvalTypeSnapshot main_eval_types; // 文件级/全局数值常量及其推断类型映射 std::unordered_map global_const_vars; + // 声明后仍被再赋值过的文件级 local 变量名。这些变量即使推断为数值类型也不能 + // 发射成 C 的 const 声明(const + 后续赋值 = C 编译错误),CGen 据此去掉 const。 + std::unordered_set global_reassigned_vars; // table 特化信息:table constructor 节点 → 特化信息 std::unordered_map table_spec_infos; // 流敏感 table 特化标注:Var 引用节点(kDot/kSquare 的 prefixexp 所指 Var 节点)→ 该程序点该变量的 spec 类型名。 diff --git a/src/compile/type_inferencer.cpp b/src/compile/type_inferencer.cpp index 84a2b65..1adb6e8 100644 --- a/src/compile/type_inferencer.cpp +++ b/src/compile/type_inferencer.cpp @@ -384,10 +384,11 @@ InferredType TypeInferencer::TypeEnvironment::MergeType(const InferredType old_t InferResult TypeInferencer::InferTypes(const ParseResult &pr, const CompileConfig &cfg) { LOG_DEBUG(s_, "engine", "InferTypes: start for {}", pr.file_name); file_level_types_.clear(); + file_level_init_exps_.clear(); InferResult ir; EvalTypeSnapshot current_map; TypeEnvironment env; - TraversalContext tctx{current_map, env, nullptr, ir.var_define_nodes, ir.shadowed_decls}; + TraversalContext tctx{current_map, env, nullptr, ir.var_define_nodes, ir.shadowed_decls, ir.global_reassigned_vars}; LOG_DEBUG(s_, "engine", "InferTypes: step 1 - InferNode"); InferNode(pr.chunk, tctx); @@ -610,6 +611,11 @@ InferredType TypeInferencer::InferLocalVar(const std::shared_ptr if (const auto *init = tctx.env.LookupInitNode(name)) { tctx.var_define_nodes[var.get()] = init; + // 赋值目标解析到文件级 local 的 initializer → 这是一个再赋值点。 + // 有再赋值点的数值变量不能被 CGen 发射成 const(否则 C 编译报错, + // 见 global_reassigned_vars 的说明)。 + if (file_level_init_exps_.contains(init)) { + tctx.global_reassigned_vars.insert(name); + } } current_map[var.get()] = current; @@ -1092,9 +1104,11 @@ EvalTypeSnapshot TypeInferencer::RunTrialInference(const SyntaxTreeInterfacePtr // 运行函数体类型推断(不新开作用域,参数已在当前作用域中定义)。 // Trial 推断不消费 shadow 信息(shadow 只与 AST 结构相关,主推断已覆盖), - // 但 TraversalContext 需要一个引用,因此用一个丢弃式的 set 占位。 + // 也不收集再赋值信息(主推断已覆盖),但 TraversalContext 需要引用, + // 因此用丢弃式的容器占位。 std::set> dummy_shadowed_decls; - TraversalContext tctx{current_map, env, &ctx, var_define_nodes, dummy_shadowed_decls}; + std::unordered_set dummy_reassigned_vars; + TraversalContext tctx{current_map, env, &ctx, var_define_nodes, dummy_shadowed_decls, dummy_reassigned_vars}; InferBlock(std::dynamic_pointer_cast(func_block), false, tctx); // 快照本轮推断结果:为未被推断触及的节点补 T_UNKNOWN, diff --git a/src/compile/type_inferencer.h b/src/compile/type_inferencer.h index c190263..9f93601 100644 --- a/src/compile/type_inferencer.h +++ b/src/compile/type_inferencer.h @@ -95,6 +95,8 @@ class TypeInferencer { const TrialInferenceContext *ctx = nullptr; std::unordered_map &var_define_nodes; std::set> &shadowed_decls; + // 文件级 local 再赋值收集(主推断传 ir.global_reassigned_vars,试推断传 dummy)。 + std::unordered_set &global_reassigned_vars; [[nodiscard]] bool IsTrialInference() const { return ctx != nullptr; @@ -278,6 +280,9 @@ class TypeInferencer { private: State *s_ = nullptr; std::unordered_map file_level_types_; + // 文件级 local 声明的 initializer 表达式节点。InferAssign 据此判断被赋值变量 + // 是否解析到文件级绑定(含遮蔽场景),进而收集"声明后再被赋值"的变量名。 + std::unordered_set file_level_init_exps_; // 不动点迭代轮次上限(实际通常 2 轮即可收敛)。 static constexpr int kMaxSpecIterations = 16; diff --git a/src/native/README.md b/src/native/README.md index 62b9b59..eef3428 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -316,7 +316,7 @@ On Windows, `io.open` / `loadfile` / `dofile` / `os.getenv` / `os.tmpname` / Boo | `net.udp_server(config)` | Bind a UDP socket (`ip`, `port`; `port=0` is ephemeral). `send(data, ip, port)` | | `net.udp_client(config)` | Connected UDP client (`ip`/`port` are the peer). `send(data)` | | `obj:dispatch(func_name)` | Register Lua callback function name | -| `obj:send(connid, data)` | Send data (server: specify connid; client: omit) | +| `obj:send(connid, data)` | Send data (server: specify connid; client: omit). From a callback context the send is queued and pumped by the tick after dispatch finishes; failures are logged as WARN | | `obj:close()` | Close connection/server | | `obj:close_connection(connid)` | Close single connection (server only) | | `obj:get_events()` | Event history | @@ -402,6 +402,26 @@ Timers go first so callbacks that come due can be picked up by the I/O dispatch them. Within MySQL, pools are driven before connections so heartbeat and reconnect have run before a script acquires a connection this round. +**Pump order and callback timing (important):** the order is fixed: timer → net → http → mysql → +redis. Each module dispatches its Lua callbacks synchronously inside the tick, therefore: + +- **IO issued from inside a callback produces results at the earliest in the next tick.** A mysql + query issued from a net callback cannot have its result dispatched until the next frame; do not + write code that issues a query from a callback and expects the result synchronously. +- A second `conn:query` on the same connection while one is in flight is **queued** (no error) and + starts automatically once the connection is Ready again; at most one queued query starts per tick. + +**Callback context (C++ → Lua) — allowed / forbidden operations:** + +net's on_event and the mysql/http/redis result callbacks all run in a restricted dispatch context: + +| Operation | Supported | Notes | +|------|---------|------| +| Mutating fields of a runtime-created table | ✅ | Attaching a table created inside the callback to a long-lived table is a reliable pattern | +| IO such as `send` / mysql `query` | ✅ | sends are pumped by the tick after dispatch finishes; queries queue asynchronously | +| Assigning **fields** of a file-level `local` table | ❌ | file-level tables get CONST_FLAG after init; mutating content throws "attempt to modify a const table" | +| Rebinding a file-level `local` variable (`x = ...`) | ⚠️ | numeric variables with further assignment points are not emitted const by the JIT and assign fine; numeric variables never reassigned after declaration are optimized to C-level const — assigning them later is a compile error | + --- ## Event @@ -541,9 +561,14 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv | Function | Args | Description | |----------|------|-------------| -| `json.encode(value)` | 1 | Lua value → JSON string; consecutive int keys 1..N → array; floats use `%.17g` | +| `json.encode(value)` | 1 | Lua value → JSON string; consecutive int keys 1..N → array; **an empty table encodes as `[]`**; floats use `%.17g` | +| `json.encode_array(value)` | 1 | Strict array encoding: the top level must be an array-like table (empty table → `[]`; non-empty requires consecutive int keys from 1), otherwise throws | | `json.decode(str)` | 1 | JSON string → Lua value; `null` → `nil` | +> **Empty-table ambiguity:** Lua cannot distinguish an "empty array" from an "empty object". +> `json.encode` applies the pure-array heuristic and encodes empty tables as `[]`; use +> `json.encode_array` when the client protocol must be strict (array-shaped or error). + --- ## MySQL @@ -556,20 +581,44 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv `ssl`: omit/`false`/`"disable"` keeps plaintext (default). `true`/`"require"` demands TLS. `"enable"` uses TLS when the server offers it. Optional `ssl_ca` PEM enables certificate verification. +**Callback arguments:** every `cb` accepts either an in-package global function name (string, e.g. +`"DB.on_result"`) or an **inline closure** (`function(conn, err, result) ... end`). Closures can +capture context and are recommended; passing anything else raises a "bad argument" error instead of +silently dropping the callback. + +**Callback contract:** once `conn:query` is called its callback fires **exactly once** — when the +connection is not ready (handshaking / reconnecting / a previous query still in flight) the query is +queued and starts automatically once the connection is usable; if the connection is closed or in a +terminal error state, the callback receives an error string. All callbacks are driven by +`runtime.tick()`. + | Function/Method | Description | |----------|-------------| -| `mysql.connect(config, cb)` | Async connect; callback `function cb(err, conn)` | +| `mysql.connect(config, cb)` | Async connect; callback `function cb(conn, err, success)` | | `mysql_pool.create(config)` | Create connection pool | -| `conn:query(sql, cb)` | Async query; callback `function cb(err, result)` | +| `conn:query(sql, cb)` | Async query; callback `function cb(conn, err, result)` | | `conn:stmt_prepare(sql, cb)` | Prepare statement | | `conn:stmt_execute(id, params, cb)` | Execute prepared statement | | `conn:stmt_close(id)` | Close prepared statement | | `conn:close()` | Close connection | -| `pool:acquire()` | Get connection from pool | +| `pool:acquire()` | Get connection from pool (nil when none available) | | `pool:release(conn)` | Return connection to pool | +| `pool:with(fn)` | Lease-style usage: acquires a connection and passes it to `fn(conn)`, returning it automatically when fn returns or throws; returns fn's return value. Prevents the "forgot to release / callback never fired so the connection is never returned" leak pattern | | `pool:close()` | Close pool | | `pool:stats()` | Returns `{total, healthy}` | +**Result table layout (SELECT):** `result[1] = true`; `result[2]` is the column info table (each item +`{[1]=column name, [2]=MySQL type code}`); `result[3]` is the rows table, each row indexed by column +position (1-based). + +**Result table layout (INSERT/UPDATE/DELETE/DDL):** `result[1] = false`; `result[4] = affected_rows`; +`result[5] = last_insert_id`; `result[6] = info`. + +**Row value types:** converted by column type — integer columns (TINY/SHORT/LONG/LONGLONG/INT24/YEAR) +return numbers, float/decimal columns (FLOAT/DOUBLE/DECIMAL/NEWDECIMAL) return numbers, all other +columns (strings/dates/BLOBs) return strings, NULL returns nil. Boolean-style `TINYINT(1)` returns a +number (0/1); convert as needed. + --- ## Redis diff --git a/src/native/README.zh.md b/src/native/README.zh.md index 5cfe369..1eec97e 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -312,7 +312,7 @@ Windows 上 `io.open` / `loadfile` / `dofile` / `os.getenv` / `os.tmpname` 以 | `net.udp_server(config)` | 绑定 UDP(`ip`、`port`;`port=0` 为临时端口)。`send(data, ip, port)` | | `net.udp_client(config)` | 已连接 UDP 客户端(`ip`/`port` 为对端)。`send(data)` | | `obj:dispatch(func_name)` | 注册 Lua 回调函数名 | -| `obj:send(connid, data)` | 发送数据(服务端需指定 connid;客户端省略) | +| `obj:send(connid, data)` | 发送数据(服务端需指定 connid;客户端省略)。在回调上下文中调用时先入队,由本轮 tick 派发完后统一泵出;发送失败记 WARN 日志 | | `obj:close()` | 关闭连接/服务端 | | `obj:close_connection(connid)` | 关闭单个连接(仅服务端) | | `obj:get_events()` | 事件历史 | @@ -397,6 +397,25 @@ per-State 对象列表,所以 server/client、连接和连接池都不再有 定时器放最前,让本轮到期的回调能赶上后面的 IO 派发。mysql 内部先池后连接,这样脚本这一轮 取连接之前,心跳和重连已经推进过了。 +**泵序与回调时序(重要):** 顺序固定为 timer → net → http → mysql → redis。各模块的 Lua 回调 +都在 tick 内同步执行,因此: + +- **回调里发起的 IO 最早在下一轮 tick 才有结果**。net 回调里发起的 mysql query,最早要等下一帧 + 才可能派发结果;不要写"回调里发查询并同步等结果"的代码。 +- mysql 同一连接上飞行中再次 `conn:query` 会**排队**(而不是报错),连接回到 Ready 后由后续 + tick 自动启动;每轮 tick 至多启动一条排队的 query。 + +**回调上下文(C++ → Lua)允许/禁止的操作:** + +net 的 on_event、mysql/http/redis 的结果回调都运行在受限的派发上下文中,规则如下: + +| 操作 | 是否支持 | 说明 | +|------|---------|------| +| 改"运行时创建的 table"的字段 | ✅ | 回调里新建的 table 挂到长生命周期 table 上是可靠模式 | +| `send` / mysql `query` 等 IO | ✅ | send 会在派发结束后由 tick 统一泵出;query 异步排队 | +| 给文件级 `local` 表的**字段**赋值 | ❌ | 文件级表初始化后打 CONST_FLAG,改内容会抛 "attempt to modify a const table" | +| 重绑定文件级 `local` 变量(`x = ...`) | ⚠️ | 数值型变量如声明后有再赋值点,JIT 不会标 const,可正常赋值;声明后从未再赋值的数值变量被优化为 C 级 const,运行时再赋值是编译错误 | + --- ## Event(事件系统) @@ -536,9 +555,13 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new | 函数 | 参数 | 说明 | |------|------|------| -| `json.encode(value)` | 1 | Lua 值 → JSON 字符串;连续整数键 1..N → 数组;浮点数用 `%.17g` | +| `json.encode(value)` | 1 | Lua 值 → JSON 字符串;连续整数键 1..N → 数组,**空 table 编码为 `[]`**;浮点数用 `%.17g` | +| `json.encode_array(value)` | 1 | 严格数组编码:顶层必须是数组形 table(空 table → `[]`;非空要求 key 为从 1 起的连续整数),否则抛错 | | `json.decode(str)` | 1 | JSON 字符串 → Lua 值;`null` → `nil` | +> **空 table 的歧义:** Lua 无法区分"空数组"与"空对象"。`json.encode` 按纯数组启发式把空 +> table 编码为 `[]`;需要与客户端协议严格对齐时用 `json.encode_array`(要求数组形,否则报错)。 + --- ## YAML @@ -612,20 +635,50 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new `ssl`:省略/`false`/`"disable"` 保持明文(默认)。`true`/`"require"` 强制 TLS。`"enable"` 在服务器支持时使用 TLS。可选 `ssl_ca` PEM 会校验证书。 +**回调参数:** 所有 `cb` 既支持包内全局函数名(字符串,如 `"DB.on_result"`),也支持**内联闭包** +(`function(conn, err, result) ... end`)。闭包可以捕获上下文,推荐使用;传其他类型会直接报 +"bad argument" 错误(不会静默丢弃回调)。 + +**回调契约:** `conn:query` 一旦被调用,其回调**恰好被调用一次**——连接未就绪(握手中/重连中/ +上一条 query 仍在飞行)时 query 排队,连接可用后自动执行;连接已关闭或进入错误终态时,回调会 +收到错误字符串。所有回调由 `runtime.tick()` 驱动。 + | 函数/方法 | 说明 | |------|------| -| `mysql.connect(config, cb)` | 异步连接;回调 `function cb(err, conn)` | +| `mysql.connect(config, cb)` | 异步连接;回调 `function cb(conn, err, success)` | | `mysql_pool.create(config)` | 创建连接池 | -| `conn:query(sql, cb)` | 异步查询;回调 `function cb(err, result)` | +| `conn:query(sql, cb)` | 异步查询;回调 `function cb(conn, err, result)` | | `conn:stmt_prepare(sql, cb)` | 预处理语句 | | `conn:stmt_execute(id, params, cb)` | 执行预处理语句 | | `conn:stmt_close(id)` | 关闭预处理语句 | | `conn:close()` | 关闭连接 | -| `pool:acquire()` | 从池获取连接 | +| `pool:acquire()` | 从池获取连接(无可用连接返回 nil) | | `pool:release(conn)` | 归还连接到池 | +| `pool:with(fn)` | 租约式用法:取一条连接传给 `fn(conn)`,fn 返回(或抛错)后自动归还,返回 fn 的返回值。避免"忘 release / 回调不触发导致连接永不归还" | | `pool:close()` | 关闭连接池 | | `pool:stats()` | 返回 `{total, healthy}` | +**结果表布局(SELECT):** `result[1] = true`;`result[2]` 为列信息表(每项 `{[1]=列名, [2]=MySQL 类型码}`); +`result[3]` 为行表,每行按列位置(1 起)索引。 + +**结果表布局(INSERT/UPDATE/DELETE/DDL):** `result[1] = false`;`result[4] = affected_rows`; +`result[5] = last_insert_id`;`result[6] = info`。 + +**行值类型:** 按列类型转换——整数列(TINY/SHORT/LONG/LONGLONG/INT24/YEAR)返回 number(整数), +浮点/小数列(FLOAT/DOUBLE/DECIMAL/NEWDECIMAL)返回 number(浮点),其余列(字符串/日期/BLOB 等) +返回 string,NULL 返回 nil。布尔语义的 `TINYINT(1)` 返回数字(0/1),需要自行换算。 + +```lua +-- 租约式用法:fn 返回后连接自动归还,即使中途抛错也不会泄漏 +local ok = pool:with(function(c) + -- query 是异步的:结果在之后的 runtime.tick() 里通过回调到达 + c:query("UPDATE user SET online = 0 WHERE last_login < 100", function(conn, err, result) + if err then print("query failed:", err) end + end) + return true +end) +``` + --- ## Redis diff --git a/src/native/json/native_json.cpp b/src/native/json/native_json.cpp index cf3040b..0b00ee7 100644 --- a/src/native/json/native_json.cpp +++ b/src/native/json/native_json.cpp @@ -112,7 +112,9 @@ static bj::value LuaToJsonValue(CVar v, int depth, std::unordered_set(VarType::Int)) { @@ -126,7 +128,7 @@ static bj::value LuaToJsonValue(CVar v, int depth, std::unordered_set max_idx) max_idx = key; } - if (is_array && (max_idx <= 0 || static_cast(max_idx) != kvs.size())) { + if (is_array && !kvs.empty() && static_cast(max_idx) != kvs.size()) { is_array = false; } @@ -181,10 +183,40 @@ static CVar JsonEncode(State *s, CVar *args, int n) { return inter::NativeToFakeluaString(s, out); } +// 判断 table 是否为数组形(空 table 视为数组;非空要求 key 为从 1 起的连续整数)。 +static bool IsArrayLikeTable(CVar v) { + if (v.type_ != static_cast(VarType::Table) || !v.data_.t) return false; + auto kvs = table::TableHelper::CollectKVPairs(v); + int64_t max_idx = 0; + for (auto &kv: kvs) { + if (kv.key.type_ != static_cast(VarType::Int)) return false; + int64_t key = kv.key.data_.i; + if (key < 1 || key > 1000000) return false; + if (key > max_idx) max_idx = key; + } + return kvs.empty() || static_cast(max_idx) == kvs.size(); +} + +// json.encode_array(value) → JSON string +// 严格数组编码通道:顶层必须是数组形 table(空 table → []),否则报错。 +// 用于协议字段必须为数组的场景,避免 encode 的对象/数组启发式歧义。 +static CVar JsonEncodeArray(State *s, CVar *args, int n) { + if (n < 1) ThrowBadArgument(1, "json.encode_array", "value expected"); + CVar a0 = inter::GetNativeArg(s, args, n, 0); + if (!IsArrayLikeTable(a0)) { + ThrowFakeluaException("json.encode_array: value is not an array-like table"); + } + std::unordered_set visited; + bj::value jv = LuaToJsonValue(a0, 0, visited); + std::string out = bj::serialize(jv); + return inter::NativeToFakeluaString(s, out); +} + void RegisterJsonLibraryApi(State *s) { if (!s) return; RegisterNativeFunction(s, "json.decode", 1, false, JsonDecode); RegisterNativeFunction(s, "json.encode", 1, false, JsonEncode); + RegisterNativeFunction(s, "json.encode_array", 1, false, JsonEncodeArray); } }// namespace fakelua::json diff --git a/src/native/mysql/mysql_connection.cpp b/src/native/mysql/mysql_connection.cpp index 42bf81d..76de584 100644 --- a/src/native/mysql/mysql_connection.cpp +++ b/src/native/mysql/mysql_connection.cpp @@ -37,6 +37,41 @@ int WaitReady(short what) { return ready ? ready : (MYSQL_WAIT_READ | MYSQL_WAIT_WRITE); } +// 按列类型把行值转成 Lua 值:整数列 → int、浮点/小数列 → float,其余(字符串/日期/BLOB 等) +// 保持 string,NULL 一律 nil。解析失败(理论上不该发生)回退为字符串,不丢数据。 +CVar FieldCellToCVar(::fakelua::State *s, const FieldCell &fv, int mysql_type) { + if (fv.is_null) return inter::NativeToFakeluaNil(s); + switch (mysql_type) { + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_YEAR: { + char *end = nullptr; + long long v = std::strtoll(fv.value.c_str(), &end, 10); + if (end && !fv.value.empty() && *end == '\0') { + return inter::NativeToFakeluaLonglong(s, static_cast(v)); + } + break; + } + case MYSQL_TYPE_DECIMAL: + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + case MYSQL_TYPE_NEWDECIMAL: { + char *end = nullptr; + double v = std::strtod(fv.value.c_str(), &end); + if (end && !fv.value.empty() && *end == '\0') { + return inter::NativeToFakeluaDouble(s, v); + } + break; + } + default: + break; + } + return inter::NativeToFakeluaString(s, fv.value); +} + }// namespace MysqlConnection::MysqlConnection(::fakelua::State *state) : io_(state->GetIoContext()) { @@ -174,12 +209,23 @@ void MysqlConnection::FinishConnect(MYSQL *ret) { } void MysqlConnection::Query(const std::string &sql) { - if (close_pending_ || !mysql_) return; - if (state_ != ConnState::Ready || !ready_) { - pending_result_err_ = "connection not ready"; + // 契约:conn:query 一旦被调用,回调恰好被调用一次。 + // - 连接已死(mysql_ 释放 / 已请求关闭):下一轮 tick 给回调派发错误; + // - 连接未就绪或上一条 query 仍在飞行:排队,等 Ready 后自动启动。 + if (!mysql_) { + pending_result_err_ = "connection closed"; + pending_result_ = true; + return; + } + if (close_pending_) { + pending_result_err_ = "connection closing"; pending_result_ = true; return; } + if (state_ != ConnState::Ready || !ready_) { + queued_queries_.push_back(QueuedQuery{sql, result_cb_}); + return; + } last_sql_ = sql; state_ = ConnState::Querying; query_type_ = QueryType::Query; @@ -283,7 +329,11 @@ ResultsetData MysqlConnection::ConsumeResult(MYSQL_RES *res, bool is_resultset) } void MysqlConnection::StmtPrepare(const std::string &sql) { - if (close_pending_ || !mysql_) return; + if (!mysql_ || close_pending_) { + pending_result_err_ = !mysql_ ? "connection closed" : "connection closing"; + pending_result_ = true; + return; + } if (state_ != ConnState::Ready || !ready_) { pending_result_err_ = "connection not ready for prepare"; pending_result_ = true; @@ -327,7 +377,11 @@ void MysqlConnection::StmtPrepare(const std::string &sql) { } void MysqlConnection::StmtExecute(uint32_t stmt_id, const std::vector ¶ms) { - if (close_pending_ || !mysql_) return; + if (!mysql_ || close_pending_) { + pending_result_err_ = !mysql_ ? "connection closed" : "connection closing"; + pending_result_ = true; + return; + } if (state_ != ConnState::Ready || !ready_) { pending_result_err_ = "connection not ready for execute"; pending_result_ = true; @@ -669,6 +723,31 @@ void MysqlConnection::Tick() { pending_result_err_.clear(); pending_results_.clear(); } + + if (!queued_queries_.empty()) { + DrainQueryQueue(); + } +} + +void MysqlConnection::DrainQueryQueue() { + // 连接进入终态(Error/Idle):再也不会 Ready,逐条给排队的 query 派发错误回调, + // 保证"每条 query 恰好一次回调",而不是静默吞掉。 + if (state_ == ConnState::Error || state_ == ConnState::Idle) { + while (!queued_queries_.empty()) { + QueuedQuery q = std::move(queued_queries_.front()); + queued_queries_.erase(queued_queries_.begin()); + DispatchCallbackWithResult(q.cb, "connection not available"); + } + return; + } + // 连接可用:启动下一条排队的 query。每轮 tick 至多启动一条—— + // 若它同步出错(pending_result_ 被占用),剩下的留到下一轮,避免覆盖未派发的结果。 + if (state_ == ConnState::Ready && ready_) { + QueuedQuery q = std::move(queued_queries_.front()); + queued_queries_.erase(queued_queries_.begin()); + result_cb_ = q.cb; + Query(q.sql); + } } MysqlError MysqlConnection::LastError() const { @@ -685,8 +764,8 @@ bool MysqlConnection::IsRetryable(MysqlErrorType type) { } } -void MysqlConnection::SetConnectCallback(const std::string &name) { connect_cb_ = name; } -void MysqlConnection::SetResultCallback(const std::string &name) { result_cb_ = name; } +void MysqlConnection::SetConnectCallback(const ResultCallback &cb) { connect_cb_ = cb; } +void MysqlConnection::SetResultCallback(const ResultCallback &cb) { result_cb_ = cb; } void MysqlConnection::SetState(::fakelua::State *state) { lua_state_ = state; } void MysqlConnection::SetNativeObject(::fakelua::NativeObject *obj) { native_obj_ = obj; } bool MysqlConnection::Connected() const { return ready_; } @@ -702,12 +781,14 @@ void MysqlConnection::SetError(MysqlErrorType type, uint16_t code, const std::st last_error_.sql_state = sql_state; } -void MysqlConnection::DispatchConnect(const char *err_msg) { - TickDepthGuard guard(tick_depth_); - native::IoContext::DispatchScope dispatch_scope(io_); - if (close_pending_) return; - if (!lua_state_ || connect_cb_.empty()) return; - auto func = lua_state_->GetVM().GetFunction(connect_cb_); +void MysqlConnection::InvokeCallback(const ResultCallback &cb, CVar *args, int n) { + // 闭包:DispatchCallClosure 内部处理 func_ptr / load() 源码闭包两种形态。 + if (cb.closure) { + inter::DispatchCallClosure(lua_state_, cb.closure, args, n, JIT_TCC); + return; + } + // 全局函数名:查 VM 注册表。 + auto func = lua_state_->GetVM().GetFunction(cb.name); if (func.Empty()) return; void *addr = func.GetAddr(JIT_TCC); JITType jit_type = JIT_TCC; @@ -716,6 +797,14 @@ void MysqlConnection::DispatchConnect(const char *err_msg) { jit_type = JIT_GCC; } if (!addr) return; + inter::DispatchCall(lua_state_, addr, args, n, jit_type); +} + +void MysqlConnection::DispatchConnect(const char *err_msg) { + TickDepthGuard guard(tick_depth_); + native::IoContext::DispatchScope dispatch_scope(io_); + if (!lua_state_ || connect_cb_.Empty()) return; + CVar args[3]; args[0] = native_obj_ ? inter::NativeToFakeluaNativeObject(lua_state_, native_obj_) : inter::NativeToFakeluaNil(lua_state_); if (err_msg && err_msg[0]) { @@ -725,23 +814,13 @@ void MysqlConnection::DispatchConnect(const char *err_msg) { args[1] = inter::NativeToFakeluaNil(lua_state_); args[2] = inter::NativeToFakeluaInt(lua_state_, 1); } - inter::DispatchCall(lua_state_, addr, args, 3, jit_type); + InvokeCallback(connect_cb_, args, 3); } -void MysqlConnection::DispatchResult(const char *err_msg) { +void MysqlConnection::DispatchCallbackWithResult(const ResultCallback &cb, const char *err_msg) { TickDepthGuard guard(tick_depth_); native::IoContext::DispatchScope dispatch_scope(io_); - if (close_pending_) return; - if (!lua_state_ || result_cb_.empty()) return; - auto func = lua_state_->GetVM().GetFunction(result_cb_); - if (func.Empty()) return; - void *addr = func.GetAddr(JIT_TCC); - JITType jit_type = JIT_TCC; - if (!addr) { - addr = func.GetAddr(JIT_GCC); - jit_type = JIT_GCC; - } - if (!addr) return; + if (!lua_state_ || cb.Empty()) return; const char *msg = err_msg && err_msg[0] ? err_msg : "query failed"; CVar args[3]; @@ -751,31 +830,35 @@ void MysqlConnection::DispatchResult(const char *err_msg) { CVar nil{}; nil.type_ = static_cast(VarType::Nil); args[2] = nil; - inter::DispatchCall(lua_state_, addr, args, 3, jit_type); + InvokeCallback(cb, args, 3); } else if (dispatch_stmt_id_ != 0) { CVar nil{}; nil.type_ = static_cast(VarType::Nil); args[1] = nil; args[2] = inter::NativeToFakeluaInt(lua_state_, static_cast(dispatch_stmt_id_)); - inter::DispatchCall(lua_state_, addr, args, 3, jit_type); + InvokeCallback(cb, args, 3); } else if (pending_results_.empty()) { CVar nil{}; nil.type_ = static_cast(VarType::Nil); args[1] = nil; args[2] = table::TableHelper::CreateTable(lua_state_); - inter::DispatchCall(lua_state_, addr, args, 3, jit_type); + InvokeCallback(cb, args, 3); } else { for (const auto &rs: pending_results_) { CVar nil{}; nil.type_ = static_cast(VarType::Nil); args[1] = nil; args[2] = ResultsetToLua(lua_state_, rs); - inter::DispatchCall(lua_state_, addr, args, 3, jit_type); + InvokeCallback(cb, args, 3); if (close_pending_) break; } } } +void MysqlConnection::DispatchResult(const char *err_msg) { + DispatchCallbackWithResult(result_cb_, err_msg); +} + CVar MysqlConnection::ResultsetToLua(::fakelua::State *s, const ResultsetData &result) { CVar tbl = table::TableHelper::CreateTable(s); if (result.is_resultset) { @@ -795,8 +878,8 @@ CVar MysqlConnection::ResultsetToLua(::fakelua::State *s, const ResultsetData &r CVar row_tbl = table::TableHelper::CreateTable(s); int64_t col_pos = 1; for (const auto &fv: row) { - if (fv.is_null) table::TableHelper::SetTableInt(s, row_tbl, col_pos, inter::NativeToFakeluaNil(s)); - else table::TableHelper::SetTableInt(s, row_tbl, col_pos, inter::NativeToFakeluaString(s, fv.value)); + const int col_type = col_pos <= static_cast(result.columns.size()) ? result.columns[static_cast(col_pos - 1)].second : 0; + table::TableHelper::SetTableInt(s, row_tbl, col_pos, FieldCellToCVar(s, fv, col_type)); ++col_pos; } table::TableHelper::SetTableInt(s, rows_tbl, row_idx++, row_tbl); diff --git a/src/native/mysql/mysql_connection.h b/src/native/mysql/mysql_connection.h index 7e3261b..c1cd27b 100644 --- a/src/native/mysql/mysql_connection.h +++ b/src/native/mysql/mysql_connection.h @@ -26,6 +26,7 @@ namespace fakelua { struct CVar; class State; class NativeObject; +class VarClosure; }// namespace fakelua namespace fakelua::mysql { @@ -35,6 +36,15 @@ struct StmtParam { std::string value; }; +// C++ → Lua 回调:支持全局函数名(字符串)或闭包(VarClosure*)。 +// 闭包由 arena 分配,生命周期覆盖整个 State,裸指针持有是安全的。 +struct ResultCallback { + std::string name; + VarClosure *closure = nullptr; + + [[nodiscard]] bool Empty() const { return name.empty() && closure == nullptr; } +}; + enum class SslMode { Disable = 0, Enable, @@ -94,8 +104,8 @@ class MysqlConnection { MysqlError LastError() const; static bool IsRetryable(MysqlErrorType type); - void SetConnectCallback(const std::string &name); - void SetResultCallback(const std::string &name); + void SetConnectCallback(const ResultCallback &cb); + void SetResultCallback(const ResultCallback &cb); void SetState(::fakelua::State *state); void SetNativeObject(::fakelua::NativeObject *obj); @@ -116,10 +126,18 @@ class MysqlConnection { ::fakelua::State *lua_state_ = nullptr; ::fakelua::NativeObject *native_obj_ = nullptr; - std::string connect_cb_; - std::string result_cb_; + ResultCallback connect_cb_; + ResultCallback result_cb_; std::string last_sql_; + // 同连接飞行中再次发起的 query:排队而不是报 "connection not ready", + // 由 Tick() 在上一条结果派发完毕、连接回到 Ready 后依次启动。 + struct QueuedQuery { + std::string sql; + ResultCallback cb; + }; + std::vector queued_queries_; + enum class QueryType { None, Query, StmtPrepare, StmtExecute, Ping }; QueryType query_type_ = QueryType::None; @@ -164,8 +182,16 @@ class MysqlConnection { void DispatchConnect(const char *err_msg); void DispatchResult(const char *err_msg); + void DispatchCallbackWithResult(const ResultCallback &cb, const char *err_msg); void SetError(MysqlErrorType type, uint16_t code, const std::string &msg, const std::string &sql_state); + // 统一的回调调用入口:闭包走 DispatchCallClosure,函数名走 VM 查表。 + // 回调缺失或不可调用时返回空 CVar(调用方无需关心返回值)。 + void InvokeCallback(const ResultCallback &cb, CVar *args, int n); + // 连接可用时启动下一条排队 query;连接进入终态(Error/Idle)时 + // 逐条给排队的 query 回调派发错误,保证"每条 query 恰好一次回调"。 + void DrainQueryQueue(); + void Teardown(); void ApplySsl(); void ArmWait(int status); diff --git a/src/native/mysql/native_mysql.cpp b/src/native/mysql/native_mysql.cpp index a4f9257..d3f1b7f 100644 --- a/src/native/mysql/native_mysql.cpp +++ b/src/native/mysql/native_mysql.cpp @@ -36,6 +36,21 @@ static std::string CVarToString(CVar v) { return {}; } +// 解析回调参数:支持全局函数名(字符串)或闭包(内联 function 字面量)。 +// 其他类型直接抛错——历史上闭包会被静默转成空串、回调永不触发(P0-1),宁可响亮报错。 +static ResultCallback CVarToCallback(State *s, CVar v, int argno, const char *fname) { + if (v.type_ == static_cast(VarType::Closure) && v.data_.cl) { + return ResultCallback{{}, v.data_.cl}; + } + std::string name = CVarToString(v); + if (!name.empty()) return ResultCallback{std::move(name), nullptr}; + // Closure 类型的 data_.cl 为空同样无法调用。 + if (v.type_ == static_cast(VarType::Closure)) { + ThrowBadArgument(argno, fname, "non-empty function expected"); + } + ThrowBadArgument(argno, fname, "function or global function name expected"); +} + // Retrieve MysqlConnection* from NativeObject MysqlConnection *UnwrapConnNative(NativeObject *self) { if (!self) return nullptr; @@ -193,9 +208,8 @@ static CVar MysqlConnect(State *s, CVar *args, int n) { if (user.empty()) ThrowBadArgument(1, "mysql.connect", "user required"); - // Read callback function name - std::string cb_name = CVarToString(a1); - if (cb_name.empty()) ThrowBadArgument(1, "mysql.connect", "callback function expected"); + // Read callback: global function name or closure + ResultCallback cb = CVarToCallback(s, a1, 2, "mysql.connect"); // Create NativeObject wrapper first (so callbacks can dispatch) int64_t gid = s->GetNativeObjectManager().CreateGroup(); @@ -223,7 +237,7 @@ static CVar MysqlConnect(State *s, CVar *args, int n) { // Create connection (async) auto *conn = new MysqlConnection(s); - conn->SetConnectCallback(cb_name); + conn->SetConnectCallback(cb); conn->SetNativeObject(nat); nat->SetInt("__mysql_conn__", reinterpret_cast(conn)); @@ -248,13 +262,15 @@ CVar ConnQuery(NativeObject *self, State *s, CVar *args, int n) { CVar a0 = inter::GetNativeArg(s, args, n, 0); CVar a1 = inter::GetNativeArg(s, args, n, 1); std::string sql = CVarToString(a0); - std::string cb_name = CVarToString(a1); + ResultCallback cb = CVarToCallback(s, a1, 2, "conn:query"); auto *conn = UnwrapConnNative(self); - if (!conn || !conn->Connected()) error("conn:query: connection is closed"); + if (!conn) error("conn:query: connection is closed"); + // 连接未就绪(握手中/重连中/上一条 query 仍在飞行)不再抛错: + // 回调随 query 入队,由连接的 tick 在可用时自动启动(见 MysqlConnection::Query)。 conn->SetState(s); - conn->SetResultCallback(cb_name); + conn->SetResultCallback(cb); conn->Query(sql); MaybeReleaseOwnedConn(self); @@ -269,13 +285,13 @@ CVar ConnStmtPrepare(NativeObject *self, State *s, CVar *args, int n) { CVar a0 = inter::GetNativeArg(s, args, n, 0); CVar a1 = inter::GetNativeArg(s, args, n, 1); std::string sql = CVarToString(a0); - std::string cb_name = CVarToString(a1); + ResultCallback cb = CVarToCallback(s, a1, 2, "conn:stmt_prepare"); auto *conn = UnwrapConnNative(self); if (!conn || !conn->Connected()) error("conn:stmt_prepare: connection is closed"); conn->SetState(s); - conn->SetResultCallback(cb_name); + conn->SetResultCallback(cb); conn->StmtPrepare(sql); MaybeReleaseOwnedConn(self); @@ -292,7 +308,7 @@ CVar ConnStmtExecute(NativeObject *self, State *s, CVar *args, int n) { CVar a2 = inter::GetNativeArg(s, args, n, 2); uint32_t stmt_id = static_cast(inter::CVarToInteger(a0, 0)); - std::string cb_name = CVarToString(a2); + ResultCallback cb = CVarToCallback(s, a2, 3, "conn:stmt_execute"); std::vector params; if (a1.type_ == static_cast(VarType::Table) && a1.data_.t) { @@ -333,7 +349,7 @@ CVar ConnStmtExecute(NativeObject *self, State *s, CVar *args, int n) { if (!conn || !conn->Connected()) error("conn:stmt_execute: connection is closed"); conn->SetState(s); - conn->SetResultCallback(cb_name); + conn->SetResultCallback(cb); conn->StmtExecute(stmt_id, params); MaybeReleaseOwnedConn(self); diff --git a/src/native/mysql/native_mysql_pool.cpp b/src/native/mysql/native_mysql_pool.cpp index fc21a6e..0f84f80 100644 --- a/src/native/mysql/native_mysql_pool.cpp +++ b/src/native/mysql/native_mysql_pool.cpp @@ -23,6 +23,7 @@ static CVar PoolAcquire(NativeObject *self, State *s, CVar *args, int n); static CVar PoolRelease(NativeObject *self, State *s, CVar *args, int n); static CVar PoolClose(NativeObject *self, State *s, CVar *args, int n); static CVar PoolStats(NativeObject *self, State *s, CVar *args, int n); +static CVar PoolWith(NativeObject *self, State *s, CVar *args, int n); static CVar ConnPoolRelease(NativeObject *self, State *s, CVar *args, int n); static CVar ConnErrorInfo(NativeObject *self, State *s, CVar *args, int n); static MysqlConnection *UnwrapConn(CVar v); @@ -199,20 +200,18 @@ static CVar PoolCreate(State *s, CVar *args, int n) { }); nat->RegisterMethod("acquire", PoolAcquire); nat->RegisterMethod("release", PoolRelease); + nat->RegisterMethod("with", PoolWith); nat->RegisterMethod("close", PoolClose); nat->RegisterMethod("stats", PoolStats); return inter::NativeToFakeluaNativeObject(s, nat); } -// pool:acquire() → connection - -static CVar PoolAcquire(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { - auto *pool_obj = UnwrapPool(self); - if (!pool_obj || !pool_obj->pool) return inter::NativeToFakeluaNil(s); - +// 从池里取一条连接并包成 NativeObject(pool:acquire / pool:with 共用)。 +// 池满/无健康连接时返回 nil。 +static NativeObject *AcquireConnObject(State *s, PoolObject *pool_obj) { auto *conn = pool_obj->pool->Acquire(); - if (!conn) return inter::NativeToFakeluaNil(s); + if (!conn) return nullptr; // Wrap connection in NativeObject for Lua (use a new group for each connection) int64_t conn_gid = s->GetNativeObjectManager().CreateGroup(); @@ -236,10 +235,50 @@ static CVar PoolAcquire(NativeObject *self, State *s, CVar * /*args*/, int /*n*/ conn->SetState(s); conn->SetNativeObject(nat); + return nat; +} + +// pool:acquire() → connection + +static CVar PoolAcquire(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { + auto *pool_obj = UnwrapPool(self); + if (!pool_obj || !pool_obj->pool) return inter::NativeToFakeluaNil(s); + auto *nat = AcquireConnObject(s, pool_obj); + if (!nat) return inter::NativeToFakeluaNil(s); return inter::NativeToFakeluaNativeObject(s, nat); } +// pool:with(fn) → fn 的返回值 +// 租约式用法:从池里取一条连接传给 fn,fn 返回(或抛错)后自动归还连接。 +// 彻底避免"忘了 release / 回调不触发导致连接永不归还"的泄漏模式。 + +static CVar PoolWith(NativeObject *self, State *s, CVar *args, int n) { + auto *pool_obj = UnwrapPool(self); + if (!pool_obj || !pool_obj->pool) return inter::NativeToFakeluaNil(s); + if (n < 1) ThrowBadArgument(1, "pool:with", "function expected"); + CVar a0 = inter::GetNativeArg(s, args, n, 0); + if (a0.type_ != static_cast(VarType::Closure) || !a0.data_.cl) { + ThrowBadArgument(1, "pool:with", "function expected"); + } + + auto *nat = AcquireConnObject(s, pool_obj); + if (!nat) return inter::NativeToFakeluaNil(s); + + CVar conn_arg = inter::NativeToFakeluaNativeObject(s, nat); + CVar result{static_cast(VarType::Nil)}; + try { + result = inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, JIT_TCC); + } catch (...) { + // 无论 fn 是否抛错,连接都必须归还,否则池照样泄漏。 + DetachAcquiredWrapper(nat); + throw; + } + DetachAcquiredWrapper(nat); + // fn 的多返回值以 Multi 形式返回;单个返回值原样透传。 + return result; +} + // pool:release(conn) static CVar PoolRelease(NativeObject *self, State *s, CVar *args, int n) { diff --git a/src/native/net/native_net.cpp b/src/native/net/native_net.cpp index 1bb9999..284df44 100644 --- a/src/native/net/native_net.cpp +++ b/src/native/net/native_net.cpp @@ -3,6 +3,7 @@ #include "native/net/net_event.h" #include "native/object/native_object.h" #include "native/table/native_table.h" +#include "state/state.h" #include "util/logging.h" #include "var/var.h" @@ -45,7 +46,19 @@ struct NetObject { int tick_depth = 0; bool close_pending = false; + // 回调上下文(native dispatch 期间)里的 send 先入队,由本轮 tick 派发完后统一 + // 泵出——消除"回调里 send 是否生效"的平台相关行为(事件驱动改轮询驱动)。 + struct PendingSend { + enum class Kind { Server, Client, UdpPeer, UdpTo } kind; + int connid = -1; // Server: 目标连接 + std::string data; + std::string ip; // UdpTo: 目标地址 + uint16_t port = 0; // UdpTo: 目标端口 + }; + std::vector pending_sends; + static constexpr size_t kMaxEvents = 1024; + static constexpr size_t kMaxPendingSends = 4096; }; // 辅助:从 CVar 提取字符串 @@ -126,6 +139,8 @@ static CVar CallLuaEvent(State *state, const std::string &func_name, const char // "echo", data → 将 data 发回来源连接 // "close" → 延后关闭本对象(tick 回调内安全) +static bool SendOrQueue(NetObject *obj, NetObject::PendingSend ps); + static void HandleCallbackReturn(NetObject *obj, const CVar &ret, int connid) { if (!obj || obj->close_pending) return; // 检查是否为 Multi(多返回值) @@ -143,14 +158,66 @@ static void HandleCallbackReturn(NetObject *obj, const CVar &ret, int connid) { std::string echo_data = CVarToString(data_var); if (obj->udp) { if (obj->udp->Connected()) { - obj->udp->Send(echo_data.data(), echo_data.size()); + SendOrQueue(obj, {NetObject::PendingSend::Kind::UdpPeer, -1, std::move(echo_data)}); } else { - obj->udp->SendTo(echo_data.data(), echo_data.size(), obj->last_peer_ip, obj->last_peer_port); + SendOrQueue(obj, {NetObject::PendingSend::Kind::UdpTo, -1, std::move(echo_data), obj->last_peer_ip, obj->last_peer_port}); } } else if (obj->is_server) { - obj->server->Send(connid, echo_data.data(), echo_data.size()); + SendOrQueue(obj, {NetObject::PendingSend::Kind::Server, connid, std::move(echo_data)}); } else { - obj->client->Send(echo_data.data(), echo_data.size()); + SendOrQueue(obj, {NetObject::PendingSend::Kind::Client, -1, std::move(echo_data)}); + } + } +} + +// send 的统一实现:按 PendingSend 的 kind 写 socket。返回 false 表示引擎侧 +// 拒绝(连接未就绪/已关闭),调用方记日志,不再静默。 +static bool DoSend(NetObject *obj, const NetObject::PendingSend &ps) { + using Kind = NetObject::PendingSend::Kind; + switch (ps.kind) { + case Kind::Server: + if (obj->server) return obj->server->Send(ps.connid, ps.data.data(), ps.data.size()); + return false; + case Kind::Client: + if (obj->client) return obj->client->Send(ps.data.data(), ps.data.size()); + return false; + case Kind::UdpPeer: + if (obj->udp && obj->udp->Connected()) return obj->udp->Send(ps.data.data(), ps.data.size()); + return false; + case Kind::UdpTo: + if (obj->udp) return obj->udp->SendTo(ps.data.data(), ps.data.size(), ps.ip, ps.port); + return false; + } + return false; +} + +// 判断是否处于 C++→Lua 回调派发上下文(on_event / mysql/http 回调等)。 +static bool InDispatchContext(NetObject *obj) { + return obj && obj->state && obj->state->GetIoContext().InDispatch(); +} + +// send 的统一入口:回调上下文里不直接写 socket,而是入队,由本轮 tick 派发完后 +// 统一泵出——消除"回调里 send 偶尔静默丢失"的平台相关行为;非派发上下文立即发送。 +static bool SendOrQueue(NetObject *obj, NetObject::PendingSend ps) { + if (InDispatchContext(obj)) { + if (obj->pending_sends.size() >= NetObject::kMaxPendingSends) { + LOG_WARN(obj->state, "net", "pending send queue full ({}), dropping {} bytes", obj->pending_sends.size(), ps.data.size()); + return false; + } + obj->pending_sends.push_back(std::move(ps)); + return true; + } + return DoSend(obj, ps); +} + +// 派发完成后统一泵出积压的 send。此时已脱离 DispatchScope,直接写 socket 是安全的。 +static void FlushPendingSends(NetObject *obj) { + if (!obj || obj->pending_sends.empty()) return; + auto sends = std::move(obj->pending_sends); + obj->pending_sends.clear(); + for (const auto &ps: sends) { + if (!DoSend(obj, ps)) { + LOG_WARN(obj->state, "net", "deferred send failed (kind={} connid={} len={})", static_cast(ps.kind), ps.connid, ps.data.size()); } } } @@ -294,10 +361,13 @@ static void TickNetObject(NativeObject *self) { } } catch (...) { obj->tick_depth--; + FlushPendingSends(obj); finish_tick(); throw; } obj->tick_depth--; + // 回调期间积压的 send 在这里统一泵出(socket 写在派发上下文之外执行)。 + FlushPendingSends(obj); finish_tick(); } @@ -314,6 +384,7 @@ void TickAll(State *s) { } // server:send(connid, data) / client:send(data) +// 返回 true 表示已被引擎接受(直接写出,或回调上下文中入队待泵出)。 static CVar NetSend(NativeObject *self, State *s, CVar *args, int n) { auto *obj = Unwrap(self); if (!obj) return inter::NativeToFakeluaBool(s, false); @@ -322,7 +393,7 @@ static CVar NetSend(NativeObject *self, State *s, CVar *args, int n) { if (obj->udp->Connected()) { if (n < 1) ThrowBadArgument(1, "send", "data expected"); std::string data = CVarToString(inter::GetNativeArg(s, args, n, 0)); - bool ok = obj->udp->Send(data.data(), data.size()); + bool ok = SendOrQueue(obj, {NetObject::PendingSend::Kind::UdpPeer, -1, std::move(data)}); return inter::NativeToFakeluaBool(s, ok); } if (n < 3) ThrowBadArgument(1, "send", "data, ip and port expected"); @@ -332,7 +403,7 @@ static CVar NetSend(NativeObject *self, State *s, CVar *args, int n) { if (port_val <= 0 || port_val > 65535) { ThrowFakeluaException(std::format("net: port {} out of range (1-65535)", port_val)); } - bool ok = obj->udp->SendTo(data.data(), data.size(), ip, static_cast(port_val)); + bool ok = SendOrQueue(obj, {NetObject::PendingSend::Kind::UdpTo, -1, std::move(data), std::move(ip), static_cast(port_val)}); return inter::NativeToFakeluaBool(s, ok); } @@ -351,7 +422,7 @@ static CVar NetSend(NativeObject *self, State *s, CVar *args, int n) { } return std::string(); }(); - bool ok = obj->server->Send(connid, data.data(), data.size()); + bool ok = SendOrQueue(obj, {NetObject::PendingSend::Kind::Server, connid, std::move(data)}); return inter::NativeToFakeluaBool(s, ok); } else if (!obj->is_server && obj->client) { // client: send(data) @@ -367,7 +438,7 @@ static CVar NetSend(NativeObject *self, State *s, CVar *args, int n) { } return std::string(); }(); - bool ok = obj->client->Send(data.data(), data.size()); + bool ok = SendOrQueue(obj, {NetObject::PendingSend::Kind::Client, -1, std::move(data)}); return inter::NativeToFakeluaBool(s, ok); } diff --git a/test/lua/infer/test_global_const_reassigned.lua b/test/lua/infer/test_global_const_reassigned.lua new file mode 100644 index 0000000..e7e2733 --- /dev/null +++ b/test/lua/infer/test_global_const_reassigned.lua @@ -0,0 +1,19 @@ +-- 文件级数值变量声明后有多个再赋值点(且全部是编译期常量)。 +-- 曾被 JIT 误标为 static const,导致生成的 C 代码编译失败: +-- error: cannot assign to variable 'next_bot_id' with const-qualified type +-- 修复后:有再赋值点的变量发射为非 const 的 static int64_t,赋值语义与 Lua 一致。 +local next_bot_id = 0 + +function init() + next_bot_id = 800000 + return next_bot_id +end + +function add() + next_bot_id = next_bot_id + 1 + return next_bot_id +end + +function get() + return next_bot_id +end diff --git a/test/lua/json/test_json_edge.lua b/test/lua/json/test_json_edge.lua index b3eef44..807d1c0 100644 --- a/test/lua/json/test_json_edge.lua +++ b/test/lua/json/test_json_edge.lua @@ -249,11 +249,38 @@ function test_encode_control_chars() return 1 end --- 测试 JSON 编码:空对象 +-- 测试 JSON 编码:空 table function test_encode_empty_object() local s = json.encode({}) - -- 空表应编码为空对象 - if s ~= "{}" then return 0 end + -- 空 table 按纯数组启发式编码为 [](客户端按数组解析协议字段) + if s ~= "[]" then return 0 end + return 1 +end + +-- 测试 JSON 编码:空数组字段嵌在对象里 +function test_encode_empty_array_field() + local s = json.encode({ type = "rank", list = {} }) + -- 空 list 字段应为 "list":[] 而不是 "list":{}(string.find 为 ECMAScript 正则,[ 需转义) + if not string.find(s, '"list":\\[\\]') then return 0 end + return 1 +end + +-- 测试 json.encode_array:数组形 table 正常编码 +function test_encode_array_ok() + if json.encode_array({}) ~= "[]" then return 0 end + if json.encode_array({ 1, 2, 3 }) ~= "[1,2,3]" then return 0 end + if json.encode_array({ [1] = "a", [2] = "b" }) ~= '["a","b"]' then return 0 end + return 1 +end + +-- 测试 json.encode_array:非数组形 table 报错 +function test_encode_array_reject() + local ok1 = pcall(function() json.encode_array({ a = 1 }) end) + if ok1 then return 0 end + local ok2 = pcall(function() json.encode_array({ [1] = "a", [3] = "c" }) end) + if ok2 then return 0 end + local ok3 = pcall(function() json.encode_array("not a table") end) + if ok3 then return 0 end return 1 end diff --git a/test/lua/mysql/test_mysql_contract.lua b/test/lua/mysql/test_mysql_contract.lua new file mode 100644 index 0000000..aa0bcd2 --- /dev/null +++ b/test/lua/mysql/test_mysql_contract.lua @@ -0,0 +1,97 @@ +package "MysqlContractTest" + +-- MySQL 回调契约测试(无需真实 MySQL 服务器:连到死端口验证回调行为) +-- 回归: +-- P0-1 内联闭包回调曾被静默丢弃(CVarToString 转成空串,GetFunction("") 查不到) +-- P0-2 连接未就绪时 query 的回调曾被静默吞掉(Query 提前 return 不设 pending_result_) +-- P1-4 同连接飞行中再 query 曾直接报 "connection not ready";现在排队,回调恰好一次 + +-- 场景 1:连接未就绪时用闭包发起 query —— 连接失败后回调恰好收到一次错误 +function test_query_closure_exactly_once() + local state = { connect_calls = 0, query_calls = 0, query_err = nil } + + local conn = mysql.connect({ + host = "127.0.0.1", + port = 1, + user = "root", + password = "x", + db = "test", + timeout_ms = 500 + }, function(c, err, success) + state.connect_calls = state.connect_calls + 1 + end) + + -- 连接尚未就绪:query 排队,连接进入错误终态后回调收到错误 + conn:query("SELECT 1", function(c, err, result) + state.query_calls = state.query_calls + 1 + state.query_err = err + end) + + for i = 1, 2000 do + runtime.tick() + if state.query_calls > 0 then break end + os.sleep(1) + end + + if state.connect_calls ~= 1 then + print("connect callback count = ", state.connect_calls) + return 0 + end + if state.query_calls ~= 1 then + print("query callback count = ", state.query_calls) + return 0 + end + if type(state.query_err) ~= "string" or #state.query_err == 0 then + print("query callback missing error, got:", state.query_err) + return 0 + end + return 1 +end + +-- 场景 2:回调参数传非函数/非字符串名 —— 必须响亮报错(曾静默丢弃) +function test_bad_callback_type() + local conn = mysql.connect({ + host = "127.0.0.1", + port = 1, + user = "root", + password = "x", + db = "test", + timeout_ms = 500 + }, function(c, err, success) end) + + local ok1 = pcall(function() conn:query("SELECT 1", 123) end) + if ok1 then return 0 end + local ok2 = pcall(function() conn:query("SELECT 1", nil) end) + if ok2 then return 0 end + local ok3 = pcall(function() conn:query("SELECT 1", {}) end) + if ok3 then return 0 end + + conn:close() + return 1 +end + +-- 场景 3:pool:with —— 无健康连接时返回 nil(fn 不执行);非函数参数报错 +function test_pool_with() + local pool = mysql_pool.create({ + host = "127.0.0.1", + port = 1, + user = "root", + password = "x", + db = "test", + pool_size = 1, + timeout_ms = 300 + }) + + -- 没有可用连接:with 返回 nil + local r = pool:with(function(c) + return 42 + end) + if r ~= nil then return 0 end + + -- 非函数参数必须报错 + local ok = pcall(function() pool:with(123) end) + if ok then return 0 end + + pool:close() + return 1 +end diff --git a/test/lua/net/test_net_server_client.lua b/test/lua/net/test_net_server_client.lua index fe7bd6a..84866cd 100644 --- a/test/lua/net/test_net_server_client.lua +++ b/test/lua/net/test_net_server_client.lua @@ -140,3 +140,48 @@ function test_send_buffer_full() cli:close() return ok and 0 or 1 end + +-- 回归(P1-6):在 on_event 回调里直接调用 obj:send()(不走返回 "echo" 的路径)。 +-- 派发上下文中的 send 会先入队,由本轮 tick 派发完后统一泵出;客户端随后收到数据。 +local send_in_cb_server = nil +local send_in_cb_hits = 0 + +function on_send_in_cb(type, connid, data, len, reason) + if type == "recv" then + send_in_cb_hits = send_in_cb_hits + 1 + if send_in_cb_server then + send_in_cb_server:send(connid, "direct:" .. data) + end + end +end + +function test_send_in_callback() + send_in_cb_hits = 0 + local server = net.server({ port = 19987, maxconn = 4 }) + server:dispatch("NetTest.on_send_in_cb") + send_in_cb_server = server + + local client = net.client({ port = 19987 }) + client:dispatch("NetTest.on_client_event") + + for i = 1, 50 do + runtime.tick() + if server:get_conn_count() >= 1 then break end + os.sleep(1) + end + + client:send("ping") + + -- 回调里直接 send 的数据应经入队泵出后到达客户端 + for i = 1, 50 do + runtime.tick() + if client:get_last_data() == "direct:ping" then break end + os.sleep(1) + end + + local ok = client:get_last_data() == "direct:ping" + send_in_cb_server = nil + server:close() + client:close() + return ok and 1 or 0 +end diff --git a/test/test_exception.cpp b/test/test_exception.cpp index e65d6a0..5d8af77 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1125,12 +1125,18 @@ TEST(exception, const_no_init) { EXPECT_THROW(CompileFile(s, "./exception/test_const_no_init.lua", {}), std::exception); } +// 回归:文件级数值变量声明后再赋值曾是编译错误(被误标为 C const)。 +// 修复后(pitfalls P1-3):有再赋值点的变量不做 const 优化,赋值语义与 Lua 一致。 +// 从未再赋值的文件级常量仍保持 const,见 infer.test_global_const_int。 TEST(exception, const_reassign) { FakeluaStateGuard sg; auto s = sg.GetState(); ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - EXPECT_THROW(CompileFile(s, "./exception/test_const_reassign.lua", {}), std::exception); + EXPECT_NO_THROW(CompileFile(s, "./exception/test_const_reassign.lua", {})); + int ret = 0; + Call(s, JIT_TCC, "test", ret); + EXPECT_EQ(ret, 4); } TEST(exception, top_level_bare_local) { diff --git a/test/test_infer.cpp b/test/test_infer.cpp index c99920b..24c4a63 100644 --- a/test/test_infer.cpp +++ b/test/test_infer.cpp @@ -3023,6 +3023,27 @@ TEST(infer, test_global_const_float) { }); } +// 回归:文件级数值变量声明后有多个再赋值点(且全部是编译期常量)时, +// 曾被误标为 static const,JIT 生成的 C 代码编译失败。 +// 修复后:有再赋值点的变量发射为非 const 的 static int64_t;从未再赋值的仍保持 const。 +TEST(infer, test_global_const_reassigned_not_const) { + const auto code = InferGetCCode("./infer/test_global_const_reassigned.lua"); + // 有再赋值点 → 不允许 const。 + ASSERT_NE(code.find("static int64_t next_bot_id = 0;"), std::string::npos); + ASSERT_EQ(code.find("static const int64_t next_bot_id"), std::string::npos); + + InferRunHelper([](State *s, JITType type, bool debug_mode) { + CompileFile(s, "./infer/test_global_const_reassigned.lua", {.debug_mode = debug_mode}); + int ret = 0; + Call(s, type, "init", ret); + Call(s, type, "add", ret); + Call(s, type, "add", ret); + int64_t v = 0; + Call(s, type, "get", v); + ASSERT_EQ(v, 800002); + }); +} + // --------------------------------------------------------------------------- // func() + func() and func(func()) — return expression contains function-call // results used in arithmetic or as arguments to another call. These patterns diff --git a/test/test_json.cpp b/test/test_json.cpp index 58ea29a..c0cbaf0 100644 --- a/test/test_json.cpp +++ b/test/test_json.cpp @@ -628,6 +628,32 @@ TEST(test_json, encode_empty_object) { FakeluaDeleteState(s); } +// 空 list 字段在对象里应编码为 "list":[](回归:曾产出 "list":{} 导致客户端按数组解析崩溃) +TEST(test_json, encode_empty_array_field) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./json/test_json_edge.lua", config); + int64_t ret = 0; + CallAll(s, "JsonTest.test_encode_empty_array_field", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +} + +// json.encode_array:数组形 table 正常编码,非数组形报错 +TEST(test_json, encode_array_strict) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./json/test_json_edge.lua", config); + int64_t ret = 0; + CallAll(s, "JsonTest.test_encode_array_ok", ret); + EXPECT_EQ(ret, 1); + CallAll(s, "JsonTest.test_encode_array_reject", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +} + TEST(test_json, encode_nested_too_deep) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); diff --git a/test/test_mysql.cpp b/test/test_mysql.cpp index 53c8693..9ad9be3 100644 --- a/test/test_mysql.cpp +++ b/test/test_mysql.cpp @@ -182,4 +182,40 @@ TEST(test_mysql, bad_port) { CallAll(s, "MysqlTest.test_bad_port", ret); EXPECT_EQ(ret, 1); FakeluaDeleteState(s); -} \ No newline at end of file +} +// 回归(P0-1/P0-2/P1-4):连接未就绪时用闭包发起 query, +// 连接失败后回调应恰好收到一次错误(曾被静默吞掉或报错丢失)。 +TEST(test_mysql, query_closure_callback_exactly_once) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + int64_t ret = 0; + CallAll(s, "MysqlContractTest.test_query_closure_exactly_once", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +} + +// 回归(P0-1):回调参数传非法类型必须响亮报错,而不是静默丢弃回调。 +TEST(test_mysql, bad_callback_type_throws) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + int64_t ret = 0; + CallAll(s, "MysqlContractTest.test_bad_callback_type", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +} + +// 回归(P1-4):pool:with 基础行为 —— 无可用连接返回 nil,非函数参数报错。 +TEST(test_mysql, pool_with_lease_api) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + int64_t ret = 0; + CallAll(s, "MysqlContractTest.test_pool_with", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +} diff --git a/test/test_net.cpp b/test/test_net.cpp index 6164838..a9bea51 100644 --- a/test/test_net.cpp +++ b/test/test_net.cpp @@ -384,3 +384,21 @@ TEST(test_net, test_udp_echo_lua) { FakeluaDeleteState(s); } + +// 回归(P1-6):在 on_event 回调里直接 obj:send() 必须可靠送达。 +// 派发上下文中的 send 入队,由本轮 tick 派发完后统一泵出。 +// 注意:C++ → Lua 回调按 VM 查表固定派发 TCC 编译的函数,文件级可变状态在各后端 +// 有独立副本,驱动端必须与回调同后端(TCC),因此这里不走 CallAll。 +TEST(test_net, test_send_in_callback) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + + CompileConfig config; + CompileFile(s, "./net/test_net_server_client.lua", config); + + int64_t ret = 0; + Call(s, JIT_TCC, "NetTest.test_send_in_callback", ret); + EXPECT_EQ(ret, 1); + + FakeluaDeleteState(s); +} From ca1061dcf78d83fa71910a8f8401ee0ee2a6e8fd Mon Sep 17 00:00:00 2001 From: esrrhs Date: Wed, 30 Sep 2026 21:17:27 +0800 Subject: [PATCH 2/8] fix: keep mysql closures alive across arena reset Runtime values sit on the temporary arena, which State::Reset() clears at each top-level Call, so a raw VarClosure* stored for a later tick was dangling. Callbacks are now copied onto the const arena, which lives with the State. Also document the json.encode({}) break, DECIMAL precision loss, and InDispatch, and cover pool:with throw plus numeric column conversion. Co-authored-by: Cursor --- src/compile/c_runtime_header.h | 9 + src/native/README.md | 20 ++- src/native/README.zh.md | 18 +- src/native/arena_pin.cpp | 191 +++++++++++++++++++++ src/native/arena_pin.h | 17 ++ src/native/mysql/mysql_connection.cpp | 13 +- src/native/mysql/mysql_connection.h | 18 +- src/native/mysql/mysql_connection_pool.cpp | 10 ++ src/native/mysql/mysql_connection_pool.h | 3 + src/native/mysql/mysql_field_convert.h | 31 ++++ src/native/mysql/native_mysql.cpp | 7 +- src/native/mysql/native_mysql.h | 4 + src/native/mysql/native_mysql_pool.cpp | 61 ++++++- src/native/native_common.h | 7 + src/native/native_io_context.h | 8 + src/native/object/native_object.cpp | 2 + src/state/heap.cpp | 10 ++ src/state/heap.h | 7 + src/var/var_table.h | 5 + test/lua/mysql/test_mysql_contract.lua | 43 +++++ test/test_mysql.cpp | 143 +++++++++++++++ 21 files changed, 604 insertions(+), 23 deletions(-) create mode 100644 src/native/arena_pin.cpp create mode 100644 src/native/arena_pin.h create mode 100644 src/native/mysql/mysql_field_convert.h diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index 9651bf9..eb7aa5f 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -82,6 +82,10 @@ struct VarTable { 需要重算——因此把 VarTable 整体清零的分配路径天然落在安全的重算分支上。 */ uint32_t seq_len_valid_; int64_t seq_len_; + /* spec 块字节数。0 = 无 spec。与 src/var/var_table.h 同布局。 */ + uint32_t spec_bytes; + /* >0:spec 是连续 CVar(表特化)。0:不透明块,按 spec_bytes 整体搬迁。 */ + uint32_t spec_cvars; }; typedef struct State State; @@ -647,6 +651,8 @@ static inline uint32_t FlHashString(const char *str, int len) { __t->spec_count = 0; \ __t->seq_len_valid_ = 1; \ __t->seq_len_ = 0; \ + __t->spec_bytes = 0; \ + __t->spec_cvars = 0; \ assert(sizeof(__t->quick_data_) == 8 * sizeof(VarEntry)); \ { int __i; for (__i = 0; __i < 8; ++__i) { \ __t->quick_data_[__i].key.type_ = VAR_NIL; \ @@ -667,6 +673,9 @@ static inline uint32_t FlHashString(const char *str, int len) { (v).data_.t->spec_keys = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \ (v).data_.t->spec_vals = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \ (v).data_.t->spec_count = (field_count); \ + (v).data_.t->spec_bytes = (uint32_t)sizeof(SpecType); \ + (v).data_.t->spec_cvars = (uint32_t)(field_count); \ + assert(sizeof(SpecType) == (field_count) * sizeof(CVar)); \ } while(0) #define FL_SPEC(SpecType, v, field) (((SpecType *)(v).data_.t->spec)->field) diff --git a/src/native/README.md b/src/native/README.md index eef3428..38ca020 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -413,7 +413,11 @@ redis. Each module dispatches its Lua callbacks synchronously inside the tick, t **Callback context (C++ → Lua) — allowed / forbidden operations:** -net's on_event and the mysql/http/redis result callbacks all run in a restricted dispatch context: +net's on_event and the mysql/http/redis result callbacks all run in a restricted dispatch context. +Each entry wraps the call in `IoContext::DispatchScope`, which increments `InDispatch()` +(net `DrainEventsWith`, mysql connect/result dispatch, http `CallNamed`, redis result dispatch). +The flag is an integer depth on the State, so Linux, macOS and Windows behave the same way. +`send` queues while a dispatch is active and the tick pumps the queue after the scope ends. | Operation | Supported | Notes | |------|---------|------| @@ -568,6 +572,10 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv > **Empty-table ambiguity:** Lua cannot distinguish an "empty array" from an "empty object". > `json.encode` applies the pure-array heuristic and encodes empty tables as `[]`; use > `json.encode_array` when the client protocol must be strict (array-shaped or error). +> +> **Breaking change:** `json.encode({})` used to produce `{}` and now produces `[]`. Callers that +> need an empty object should encode a table with a string key, or accept both shapes on the client. +> Use `json.encode_array` when the value must be an array. --- @@ -583,7 +591,10 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv **Callback arguments:** every `cb` accepts either an in-package global function name (string, e.g. `"DB.on_result"`) or an **inline closure** (`function(conn, err, result) ... end`). Closures can -capture context and are recommended; passing anything else raises a "bad argument" error instead of +capture context. FakeLua has no GC: runtime values live on the temporary arena, and a top-level +`Call` resets that arena. Registering a callback copies the closure and the tables/strings it +captures onto the const arena, which is not reset and lives as long as the State, so the callback +survives the next frame. Passing anything else raises a "bad argument" error instead of silently dropping the callback. **Callback contract:** once `conn:query` is called its callback fires **exactly once** — when the @@ -619,6 +630,11 @@ return numbers, float/decimal columns (FLOAT/DOUBLE/DECIMAL/NEWDECIMAL) return n columns (strings/dates/BLOBs) return strings, NULL returns nil. Boolean-style `TINYINT(1)` returns a number (0/1); convert as needed. +> **DECIMAL / NEWDECIMAL precision:** these columns become IEEE 754 doubles, about 15–16 significant +> decimal digits. A `DECIMAL(18,6)` (and similar high-precision definitions) can lose the last digit +> or two, so it is a poor sole representation of money. Cast to a string in SQL +> (`CAST(price AS CHAR)`) and keep it as a string in Lua when the value must be exact. + --- ## Redis diff --git a/src/native/README.zh.md b/src/native/README.zh.md index 1eec97e..239fc06 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -407,7 +407,10 @@ per-State 对象列表,所以 server/client、连接和连接池都不再有 **回调上下文(C++ → Lua)允许/禁止的操作:** -net 的 on_event、mysql/http/redis 的结果回调都运行在受限的派发上下文中,规则如下: +net 的 on_event、mysql/http/redis 的结果回调都运行在受限的派发上下文中。派发入口用 +`IoContext::DispatchScope` 把 `InDispatch()` 加一(net 的 DrainEventsWith、mysql 的结果/连接回调、 +http 的 `CallNamed`、redis 的结果回调)。这是 State 上的一个整数深度,Linux / macOS / Windows +行为相同。`send` 看到仍在派发中就入队,本轮 tick 在 scope 结束后统一泵出。规则如下: | 操作 | 是否支持 | 说明 | |------|---------|------| @@ -561,6 +564,9 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new > **空 table 的歧义:** Lua 无法区分"空数组"与"空对象"。`json.encode` 按纯数组启发式把空 > table 编码为 `[]`;需要与客户端协议严格对齐时用 `json.encode_array`(要求数组形,否则报错)。 +> +> **破坏性变更:** `json.encode({})` 从产出 `{}` 改为产出 `[]`。依赖空对象的调用方要改成带字符串键的表, +> 或在客户端同时接受空数组和空对象。明确要数组时用 `json.encode_array`。 --- @@ -636,8 +642,10 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new `ssl`:省略/`false`/`"disable"` 保持明文(默认)。`true`/`"require"` 强制 TLS。`"enable"` 在服务器支持时使用 TLS。可选 `ssl_ca` PEM 会校验证书。 **回调参数:** 所有 `cb` 既支持包内全局函数名(字符串,如 `"DB.on_result"`),也支持**内联闭包** -(`function(conn, err, result) ... end`)。闭包可以捕获上下文,推荐使用;传其他类型会直接报 -"bad argument" 错误(不会静默丢弃回调)。 +(`function(conn, err, result) ... end`)。闭包可以捕获上下文。fakelua 没有 GC:运行期值在临时 +arena 上,顶层 `Call` 会 `Reset` 这块内存。登记回调时闭包及其捕获的表/字符串会被复制到不参与 +Reset 的 const arena(与 State 同寿),所以回调可以跨帧存活。传其他类型会直接报 "bad argument" +错误(不会静默丢弃回调)。 **回调契约:** `conn:query` 一旦被调用,其回调**恰好被调用一次**——连接未就绪(握手中/重连中/ 上一条 query 仍在飞行)时 query 排队,连接可用后自动执行;连接已关闭或进入错误终态时,回调会 @@ -668,6 +676,10 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new 浮点/小数列(FLOAT/DOUBLE/DECIMAL/NEWDECIMAL)返回 number(浮点),其余列(字符串/日期/BLOB 等) 返回 string,NULL 返回 nil。布尔语义的 `TINYINT(1)` 返回数字(0/1),需要自行换算。 +> **DECIMAL / NEWDECIMAL 精度:** 这两类列转成 IEEE 754 `double`,大约 15–16 位有效十进制数字。 +> `DECIMAL(18,6)` 这类高精度定义可能丢掉末尾 1–2 位,不适合作为金额的唯一表示。 +> 需要精确小数时在 SQL 里 `CAST(price AS CHAR)`,在 Lua 里按字符串处理。 + ```lua -- 租约式用法:fn 返回后连接自动归还,即使中途抛错也不会泄漏 local ok = pool:with(function(c) diff --git a/src/native/arena_pin.cpp b/src/native/arena_pin.cpp new file mode 100644 index 0000000..e7dc205 --- /dev/null +++ b/src/native/arena_pin.cpp @@ -0,0 +1,191 @@ +#include "native/arena_pin.h" + +#include "interp/func_proto.h" +#include "native/table/native_table.h" +#include "state/state.h" +#include "var/var_closure.h" +#include "var/var_multi.h" +#include "var/var_string.h" +#include "var/var_table.h" + +#include +#include +#include +#include + +namespace fakelua { + +namespace { + +struct PinSeen { + std::unordered_map tables; + std::unordered_map closures; + std::unordered_map strings; +}; + +CVar PinCVar(State *s, CVar v, PinSeen &seen); + +VarString *PinString(State *s, const VarString *src, PinSeen &seen) { + if (!src) return nullptr; + if (s->GetHeap().OwnsConst(src)) return const_cast(src); + if (const auto it = seen.strings.find(src); it != seen.strings.end()) return it->second; + auto &alloc = s->GetHeap().GetAllocator(true); + auto *dst = static_cast(alloc.Alloc(sizeof(VarString) + src->Size())); + new (dst) VarString(src->Str()); + seen.strings.emplace(src, dst); + return dst; +} + +const char *PinCodeStr(State *s, const char *code) { + if (!code || code == kInterpClosureMagic) return code; + const size_t n = std::strlen(code); + auto &alloc = s->GetHeap().GetAllocator(true); + auto *dst = static_cast(alloc.Alloc(n + 1)); + std::memcpy(dst, code, n + 1); + return dst; +} + +VarClosure *PinClosure(State *s, VarClosure *cl, PinSeen &seen) { + if (!cl) return nullptr; + // 已经在 const arena 上的闭包(上次登记钉过)直接复用,避免再复制一份捕获表。 + if (s->GetHeap().OwnsConst(cl)) return cl; + if (const auto it = seen.closures.find(cl); it != seen.closures.end()) return it->second; + + const int nup = cl->upvalue_count > 0 ? cl->upvalue_count : 0; + auto &alloc = s->GetHeap().GetAllocator(true); + auto *out = static_cast(alloc.Alloc(sizeof(VarClosure) + static_cast(nup) * sizeof(CVar *))); + out->func_ptr = cl->func_ptr; + out->upvalue_count = nup; + out->expected_arg_count = cl->expected_arg_count; + out->is_vararg = cl->is_vararg; + out->code_str = PinCodeStr(s, cl->code_str); + seen.closures.emplace(cl, out); + + for (int i = 0; i < nup; ++i) { + CVar *src = cl->upvalues[i]; + if (!src) { + out->upvalues[i] = nullptr; + continue; + } + CVar pinned = PinCVar(s, *src, seen); + *src = pinned; + auto *box = static_cast(alloc.Alloc(sizeof(CVar))); + *box = pinned; + out->upvalues[i] = box; + } + return out; +} + +CVar PinTable(State *s, CVar src, PinSeen &seen) { + VarTable *t = src.data_.t; + if (!t) return inter::NativeToFakeluaNil(s); + // 同一帧里第二个闭包再钉时,表已经在 const arena 上,必须返回同一份, + // 否则连接回调和查询回调会各改各的副本。 + if (s->GetHeap().OwnsConst(t)) return src; + if (const auto it = seen.tables.find(t); it != seen.tables.end()) return it->second; + + // CreateTable 在外层 ConstAllocScope 里走 const arena。 + CVar result = table::TableHelper::CreateTable(s); + result.flag_ = 0; + seen.tables.emplace(t, result); + VarTable *out = result.data_.t; + out->spec_get = t->spec_get; + out->spec_set = t->spec_set; + out->spec_count = t->spec_count; + out->spec_bytes = t->spec_bytes; + out->spec_cvars = t->spec_cvars; + + constexpr uint32_t kMaxSpecBytes = 1u << 20; + if (t->spec && t->spec_bytes > 0 && t->spec_bytes <= kMaxSpecBytes) { + auto &alloc = s->GetHeap().GetAllocator(true); + auto *blob = alloc.Alloc(t->spec_bytes); + std::memcpy(blob, t->spec, t->spec_bytes); + out->spec = blob; + if (t->spec_cvars > 0) { + if (t->spec_bytes != t->spec_cvars * sizeof(CVar)) { + ThrowFakeluaException("pin closure: spec table layout does not match CVar array"); + } + auto *fields = static_cast(blob); + for (uint32_t i = 0; i < t->spec_cvars; ++i) { + fields[i] = PinCVar(s, fields[i], seen); + } + } + } else { + out->spec = t->spec; + } + + if (t->spec_count > 0 && t->spec_keys && t->spec_vals) { + auto &alloc = s->GetHeap().GetAllocator(true); + out->spec_keys = static_cast(alloc.Alloc(sizeof(CVar) * t->spec_count)); + out->spec_vals = static_cast(alloc.Alloc(sizeof(CVar) * t->spec_count)); + for (uint32_t i = 0; i < t->spec_count; ++i) { + out->spec_keys[i] = PinCVar(s, t->spec_keys[i], seen); + if (t->spec_cvars > 0 && out->spec) { + out->spec_vals[i] = static_cast(out->spec)[i]; + } else { + out->spec_vals[i] = PinCVar(s, t->spec_vals[i], seen); + } + } + } + + // 特化表和普通表:按键重放到新表上。spec_set 会写回上面的 const spec 块。 + // 不透明 spec(NativeObject)的字段不在哈希部分,避免再走 spec_set。 + const bool rebuild_hash = t->spec_cvars > 0 || t->spec == nullptr; + if (rebuild_hash) { + table::TableHelper::ForEachKV(src, [&](CVar key, CVar val) { + CVar pk = PinCVar(s, key, seen); + CVar pv = PinCVar(s, val, seen); + table::TableHelper::SetTable(s, result, pk, pv); + }); + } else if (t->bucket_count_ == 0) { + out->count_ = t->count_; + for (uint32_t i = 0; i < t->count_ && i < VarTable::QUICK_DATA_SIZE; ++i) { + static_cast(out->quick_data_[i].key) = PinCVar(s, t->quick_data_[i].key, seen); + static_cast(out->quick_data_[i].val) = PinCVar(s, t->quick_data_[i].val, seen); + out->quick_data_[i].hash = t->quick_data_[i].hash; + } + } + + result.flag_ = src.flag_; + seen.tables[t] = result; + return result; +} + +CVar PinCVar(State *s, CVar v, PinSeen &seen) { + switch (static_cast(v.type_)) { + case VarType::String: + v.data_.s = PinString(s, v.data_.s, seen); + return v; + case VarType::Table: + return PinTable(s, v, seen); + case VarType::Closure: { + VarClosure *pinned = PinClosure(s, v.data_.cl, seen); + v.data_.cl = pinned; + return v; + } + case VarType::Multi: { + VarMulti *m = v.data_.m; + if (!m) return v; + const int n = static_cast(m->GetCount()); + CVar out = inter::AllocMultiCVar(s, n); + for (int i = 0; i < n; ++i) { + inter::SetMultiCVarElement(out, i, PinCVar(s, m->GetVars()[i], seen)); + } + return out; + } + default: + return v; + } +} + +}// namespace + +VarClosure *PinClosureForAsync(State *s, VarClosure *cl) { + if (!s || !cl) return nullptr; + // 嵌套的表重哈希 / Multi 分配都走 GetValueAllocator,这里切到 const arena。 + State::ConstAllocScope const_alloc(s); + PinSeen seen; + return PinClosure(s, cl, seen); +} + +}// namespace fakelua diff --git a/src/native/arena_pin.h b/src/native/arena_pin.h new file mode 100644 index 0000000..fdbedf1 --- /dev/null +++ b/src/native/arena_pin.h @@ -0,0 +1,17 @@ +#pragma once + +#include "fakelua.h" + +namespace fakelua { + +class VarClosure; + +// 把闭包(以及它捕获的表、字符串、嵌套闭包)复制到 const arena。 +// fakelua 没有 GC。运行期值默认落在临时 arena 上,每次顶层 Call 开头的 +// State::Reset() 会把这块 arena 的水位清零,裸指针跨帧就会悬空。 +// const arena 不参与 Reset,和 State 同寿,因此异步回调可以一直持有这里返回的指针。 +// 不做引用计数:State 销毁时整块 arena 一起释放。 +// 同时把捕获盒子里的值改写成复制后的对象,这样同一帧内调用方和回调看到的是同一份表。 +VarClosure *PinClosureForAsync(State *s, VarClosure *cl); + +}// namespace fakelua diff --git a/src/native/mysql/mysql_connection.cpp b/src/native/mysql/mysql_connection.cpp index 76de584..230fe28 100644 --- a/src/native/mysql/mysql_connection.cpp +++ b/src/native/mysql/mysql_connection.cpp @@ -37,8 +37,11 @@ int WaitReady(short what) { return ready ? ready : (MYSQL_WAIT_READ | MYSQL_WAIT_WRITE); } +}// namespace + // 按列类型把行值转成 Lua 值:整数列 → int、浮点/小数列 → float,其余(字符串/日期/BLOB 等) // 保持 string,NULL 一律 nil。解析失败(理论上不该发生)回退为字符串,不丢数据。 +// 暴露给测试,声明在 mysql_connection.h。 CVar FieldCellToCVar(::fakelua::State *s, const FieldCell &fv, int mysql_type) { if (fv.is_null) return inter::NativeToFakeluaNil(s); switch (mysql_type) { @@ -72,8 +75,6 @@ CVar FieldCellToCVar(::fakelua::State *s, const FieldCell &fv, int mysql_type) { return inter::NativeToFakeluaString(s, fv.value); } -}// namespace - MysqlConnection::MysqlConnection(::fakelua::State *state) : io_(state->GetIoContext()) { lua_state_ = state; } @@ -769,6 +770,11 @@ void MysqlConnection::SetResultCallback(const ResultCallback &cb) { result_cb_ = void MysqlConnection::SetState(::fakelua::State *state) { lua_state_ = state; } void MysqlConnection::SetNativeObject(::fakelua::NativeObject *obj) { native_obj_ = obj; } bool MysqlConnection::Connected() const { return ready_; } +void MysqlConnection::MarkConnectedForTest() { + state_ = ConnState::Ready; + ready_ = true; + close_pending_ = false; +} bool MysqlConnection::Connecting() const { return state_ == ConnState::Connecting || state_ == ConnState::Handshaking; } int MysqlConnection::TickDepth() const { return tick_depth_; } bool MysqlConnection::ClosePending() const { return close_pending_; } @@ -784,7 +790,8 @@ void MysqlConnection::SetError(MysqlErrorType type, uint16_t code, const std::st void MysqlConnection::InvokeCallback(const ResultCallback &cb, CVar *args, int n) { // 闭包:DispatchCallClosure 内部处理 func_ptr / load() 源码闭包两种形态。 if (cb.closure) { - inter::DispatchCallClosure(lua_state_, cb.closure, args, n, JIT_TCC); + // kNativeCallbackJit 只决定异常边界,函数地址用闭包自己的 func_ptr。 + inter::DispatchCallClosure(lua_state_, cb.closure, args, n, kNativeCallbackJit); return; } // 全局函数名:查 VM 注册表。 diff --git a/src/native/mysql/mysql_connection.h b/src/native/mysql/mysql_connection.h index c1cd27b..0049a98 100644 --- a/src/native/mysql/mysql_connection.h +++ b/src/native/mysql/mysql_connection.h @@ -29,6 +29,10 @@ class NativeObject; class VarClosure; }// namespace fakelua +// FieldCell 和 FieldCellToCVar 声明在独立的轻量头文件中(不依赖 mysql.h/libevent), +// 可直接在测试代码里 include。 +#include "native/mysql/mysql_field_convert.h" + namespace fakelua::mysql { struct StmtParam { @@ -36,8 +40,11 @@ struct StmtParam { std::string value; }; + // C++ → Lua 回调:支持全局函数名(字符串)或闭包(VarClosure*)。 -// 闭包由 arena 分配,生命周期覆盖整个 State,裸指针持有是安全的。 +// fakelua 没有 GC。运行期闭包默认在临时 arena 上,顶层 Call 的 State::Reset() +// 会回收它。登记回调时 PinClosureForAsync 把闭包复制到 const arena(不参与 Reset, +// 与 State 同寿),因此这里持有裸指针是安全的,不需要引用计数。 struct ResultCallback { std::string name; VarClosure *closure = nullptr; @@ -69,12 +76,8 @@ struct MysqlError { std::string sql_state; }; -struct FieldCell { - bool is_null = false; - std::string value; -}; - struct ResultsetData { + bool is_resultset = false; std::vector> columns; std::vector> rows; @@ -112,6 +115,9 @@ class MysqlConnection { bool Connected() const; bool Connecting() const; + // 单测:跳过握手,把连接标成可被池 Acquire。生产路径不会调用。 + void MarkConnectedForTest(); + int TickDepth() const; bool ClosePending() const; void RequestClose(); diff --git a/src/native/mysql/mysql_connection_pool.cpp b/src/native/mysql/mysql_connection_pool.cpp index bd651b8..33488f0 100644 --- a/src/native/mysql/mysql_connection_pool.cpp +++ b/src/native/mysql/mysql_connection_pool.cpp @@ -183,6 +183,16 @@ size_t MysqlConnectionPool::HealthyCount() const { return count; } +void MysqlConnectionPool::MarkConnectedForTest() { + std::lock_guard lock(mutex_); + for (auto &entry: pool_) { + if (!entry.conn) continue; + entry.conn->MarkConnectedForTest(); + entry.healthy = true; + entry.in_use = false; + } +} + // Private helpers void MysqlConnectionPool::SendHeartbeat(PoolEntry &entry) { if (!entry.conn || !entry.healthy) return; diff --git a/src/native/mysql/mysql_connection_pool.h b/src/native/mysql/mysql_connection_pool.h index e87122c..9e2453a 100644 --- a/src/native/mysql/mysql_connection_pool.h +++ b/src/native/mysql/mysql_connection_pool.h @@ -68,6 +68,9 @@ class MysqlConnectionPool { size_t TotalCount() const; size_t HealthyCount() const; + // 单测:把已创建的连接标成 healthy + Connected,使 Acquire 不必等真实握手。 + void MarkConnectedForTest(); + private: PoolConfig config_; ::fakelua::State *state_ = nullptr; diff --git a/src/native/mysql/mysql_field_convert.h b/src/native/mysql/mysql_field_convert.h new file mode 100644 index 0000000..866c688 --- /dev/null +++ b/src/native/mysql/mysql_field_convert.h @@ -0,0 +1,31 @@ +#pragma once + +// mysql_field_convert.h — 行值类型转换:FieldCell → Lua CVar。 +// 此头文件不依赖 mysql.h 或 libevent,可直接在测试代码中 include。 + +#include +#include + +namespace fakelua { +struct CVar; +class State; +}// namespace fakelua + +namespace fakelua::mysql { + +// MySQL 结果行中的一个字段值。 +struct FieldCell { + bool is_null = false; + std::string value; +}; + +// 按列类型把行值转成 Lua CVar。 +// mysql_type 为 MariaDB/MySQL 的 enum_field_types 整数值(mysql.h MYSQL_TYPE_*)。 +// 整数列(TINY/SHORT/LONG/LONGLONG/INT24/YEAR)→ VarType::Int +// 浮点/小数列(FLOAT/DOUBLE/DECIMAL/NEWDECIMAL)→ VarType::Float +// 其他列(字符串/日期/BLOB 等)→ VarType::String +// NULL 字段(is_null == true)→ VarType::Nil +// 解析失败(如整数列收到非数字值)→ 回退为 VarType::String,不丢数据。 +CVar FieldCellToCVar(::fakelua::State *s, const FieldCell &fv, int mysql_type); + +}// namespace fakelua::mysql diff --git a/src/native/mysql/native_mysql.cpp b/src/native/mysql/native_mysql.cpp index d3f1b7f..5070191 100644 --- a/src/native/mysql/native_mysql.cpp +++ b/src/native/mysql/native_mysql.cpp @@ -1,3 +1,4 @@ +#include "native/arena_pin.h" #include "native/mysql/native_mysql.h" #include "native/mysql/mysql_connection.h" #include "native/mysql/mysql_connection_pool.h" @@ -40,7 +41,11 @@ static std::string CVarToString(CVar v) { // 其他类型直接抛错——历史上闭包会被静默转成空串、回调永不触发(P0-1),宁可响亮报错。 static ResultCallback CVarToCallback(State *s, CVar v, int argno, const char *fname) { if (v.type_ == static_cast(VarType::Closure) && v.data_.cl) { - return ResultCallback{{}, v.data_.cl}; + // 闭包本体在临时 arena 上,下一次顶层 Call 的 Reset() 会回收。 + // 异步回调要跨 tick,复制到 const arena(无 GC,和 State 同寿)。 + VarClosure *pinned = PinClosureForAsync(s, v.data_.cl); + if (!pinned) ThrowBadArgument(argno, fname, "non-empty function expected"); + return ResultCallback{{}, pinned}; } std::string name = CVarToString(v); if (!name.empty()) return ResultCallback{std::move(name), nullptr}; diff --git a/src/native/mysql/native_mysql.h b/src/native/mysql/native_mysql.h index 551651f..2fdd513 100644 --- a/src/native/mysql/native_mysql.h +++ b/src/native/mysql/native_mysql.h @@ -36,4 +36,8 @@ void OnStateDeleted(State *s); // 驱动本 State 上所有连接池和连接。由 runtime.tick() 调用。 void TickAll(State *s); +// 单测:pool:with 的 fn 抛错后连接必须回到可再次 Acquire 的状态。 +// 归还成功返回 1;fn 没有抛错返回 -1;归还失败(仍被占用)返回 0。 +int TestPoolWithFnThrowReturnsConnection(State *s, CVar fn); + }// namespace fakelua::mysql diff --git a/src/native/mysql/native_mysql_pool.cpp b/src/native/mysql/native_mysql_pool.cpp index 0f84f80..30f3432 100644 --- a/src/native/mysql/native_mysql_pool.cpp +++ b/src/native/mysql/native_mysql_pool.cpp @@ -266,17 +266,62 @@ static CVar PoolWith(NativeObject *self, State *s, CVar *args, int n) { if (!nat) return inter::NativeToFakeluaNil(s); CVar conn_arg = inter::NativeToFakeluaNativeObject(s, nat); - CVar result{static_cast(VarType::Nil)}; + // 析构时归还:fn 正常返回和抛错都走这里,避免 catch 漏掉某条路径。 + struct LeaseGuard { + NativeObject *nat; + ~LeaseGuard() { DetachAcquiredWrapper(nat); } + } lease{nat}; + // kNativeCallbackJit 只选择异常边界。fn 的地址在闭包的 func_ptr 里, + // GCC / 解释器闭包同样会走到自己的代码,不会因为标记是 TCC 而静默失败。 + return inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, kNativeCallbackJit); +} + +int TestPoolWithFnThrowReturnsConnection(State *s, CVar fn) { + PoolConfig config; + config.host = "127.0.0.1"; + config.port = 1; + config.user = "root"; + config.password = "x"; + config.database = "test"; + config.pool_size = 1; + config.connect_timeout_ms = 200; + config.read_timeout_ms = 200; + config.heartbeat_interval_ms = 0; + config.max_retries = 0; + + auto *pool_obj = new PoolObject(); + pool_obj->config = config; + pool_obj->pool = std::make_unique(config, s); + pool_obj->pool->Initialize(); + pool_obj->pool->MarkConnectedForTest(); + + int64_t gid = s->GetNativeObjectManager().CreateGroup(); + auto *nat = s->GetNativeObjectManager().Create(gid, "mysql_pool"); + nat->SetInt("__mysql_pool__", reinterpret_cast(pool_obj)); + RegisterMysqlNativeWrapper(s, nat, true); + nat->SetFinalizer([](NativeObject *self) { + UnregisterMysqlNativeWrapper(self); + auto *p = UnwrapPool(self); + if (p) { + InvalidateAcquiredWrappers(p); + delete p; + self->SetInt("__mysql_pool__", 0); + } + }); + + bool threw = false; try { - result = inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, JIT_TCC); + CVar args[1] = {fn}; + PoolWith(nat, s, args, 1); } catch (...) { - // 无论 fn 是否抛错,连接都必须归还,否则池照样泄漏。 - DetachAcquiredWrapper(nat); - throw; + threw = true; } - DetachAcquiredWrapper(nat); - // fn 的多返回值以 Multi 形式返回;单个返回值原样透传。 - return result; + if (!threw) return -1; + // 归还成功则能再次取到同一条连接;仍被占用则 Acquire 返回空。 + MysqlConnection *again = pool_obj->pool->Acquire(); + if (!again) return 0; + pool_obj->pool->Release(again); + return 1; } // pool:release(conn) diff --git a/src/native/native_common.h b/src/native/native_common.h index bd276ac..e5806fe 100644 --- a/src/native/native_common.h +++ b/src/native/native_common.h @@ -10,6 +10,13 @@ namespace fakelua { +// C++ 调用 Lua 闭包时传给 DispatchCall / DispatchCallClosure 的后端标记。 +// 这个值不选择函数地址:闭包的机器码在 VarClosure::func_ptr 里,具名函数由调用方 +// 按 VM 注册表取地址(TCC 没有再回退 GCC)。它只决定异常能否穿过代码页—— +// TCC 没有展开表,必须走错误边界;GCC 多包一层边界同样安全。解释器闭包由 +// IsInterpClosure 识别,与这里的标记无关,因此不会在 GCC/解释器后端静默失败。 +inline constexpr JITType kNativeCallbackJit = JIT_TCC; + // Shared helpers for native library argument validation and error reporting. // Used across native_math, native_table, native_utf8, native_string, native_io. diff --git a/src/native/native_io_context.h b/src/native/native_io_context.h index 2145ad0..4c3228a 100644 --- a/src/native/native_io_context.h +++ b/src/native/native_io_context.h @@ -38,6 +38,14 @@ class IoContext { std::size_t Poll(); + // C++→Lua 回调嵌套深度。DispatchScope 在各派发入口 +1,析构时 -1。 + // 纯计数器,不依赖平台:Linux / macOS / Windows 行为一致。 + // 当前包住回调的位置: + // net TcpServer / TcpClient / UdpSocket::DrainEventsWith + // mysql MysqlConnection::DispatchConnect / DispatchCallbackWithResult + // http CallNamed + // redis CallNamed,以及连接/命令结果派发 + // net 的 send 看到 InDispatch() 为真时改为入队,由本轮 tick 在 scope 结束后泵出。 bool InDispatch() const { return dispatch_depth_ > 0; } diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 7e5e44a..1e570ce 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -492,6 +492,8 @@ CVar NativeObject::Wrap(State *s) const { vtbl->spec = spec; vtbl->spec_get = reinterpret_cast(NativeSpecGet); vtbl->spec_set = reinterpret_cast(NativeSpecSet); + vtbl->spec_bytes = static_cast(sizeof(NativeObjectSpec)); + vtbl->spec_cvars = 0; // 填充 spec_keys / spec_vals(供 pairs() 迭代) RefreshSpecKeys(vtbl, this, s); diff --git a/src/state/heap.cpp b/src/state/heap.cpp index 2bcf95f..375651b 100644 --- a/src/state/heap.cpp +++ b/src/state/heap.cpp @@ -70,4 +70,14 @@ size_t HeapAllocator::Size() const { return current_block_index_ * BLOCK_SIZE + current_block_offset_; } +bool HeapAllocator::Contains(const void *p) const { + if (!p) return false; + const auto *addr = static_cast(p); + for (const void *block: blocks_) { + const auto *base = static_cast(block); + if (addr >= base && addr < base + BLOCK_SIZE) return true; + } + return false; +} + }// namespace fakelua diff --git a/src/state/heap.h b/src/state/heap.h index 8515d16..76b7b9b 100644 --- a/src/state/heap.h +++ b/src/state/heap.h @@ -29,6 +29,9 @@ class HeapAllocator { // 当前临时内存使用 [[nodiscard]] size_t Size() const; + // p 是否落在本分配器已经切出的块里。const arena 不 Reset,用来识别已经钉住的对象。 + [[nodiscard]] bool Contains(const void *p) const; + private: struct DestructorInfo { void (*destroyer)(void *); @@ -58,6 +61,10 @@ class Heap { // const_allocator_ 不重置,常量内存一直保留 } + [[nodiscard]] bool OwnsConst(const void *p) const { + return const_allocator_.Contains(p); + } + private: HeapAllocator temp_allocator_; // 临时内存分配器,编译过程中使用,编译结束后重置 HeapAllocator const_allocator_;// 常量内存分配器,编译过程中使用,编译结束后不重置 diff --git a/src/var/var_table.h b/src/var/var_table.h index fb9e8c6..0a23d18 100644 --- a/src/var/var_table.h +++ b/src/var/var_table.h @@ -40,6 +40,11 @@ struct VarTable { // 需要重算——因此把 VarTable 整体清零的分配路径天然落在安全的重算分支上。 uint32_t seq_len_valid_; int64_t seq_len_; + // spec 块字节数。0 表示没有需要搬迁的 spec(spec 指针为空)。 + // 与 c_runtime_header.h 的 VarTable 保持同布局。 + uint32_t spec_bytes; + // >0 时 spec 块是连续 spec_cvars 个 CVar(JIT 表特化)。0 表示不透明块(如 NativeObjectSpec)。 + uint32_t spec_cvars; }; }// namespace fakelua diff --git a/test/lua/mysql/test_mysql_contract.lua b/test/lua/mysql/test_mysql_contract.lua index aa0bcd2..c884322 100644 --- a/test/lua/mysql/test_mysql_contract.lua +++ b/test/lua/mysql/test_mysql_contract.lua @@ -95,3 +95,46 @@ function test_pool_with() pool:close() return 1 end + +-- 给 C++ 单测用:返回一个会 error() 的闭包,用来验证 pool:with 抛错后仍归还连接。 +function make_thrower() + return function(c) + error("intentional failure") + end +end + +-- 跨帧闭包(P0-1):闭包和它捕获的表必须活过下一次顶层 Call 的 arena Reset。 +-- arm 在一次 Call 里登记回调后返回;pump 是下一次 Call(开头会 Reset 临时 arena), +-- 再 tick 到回调。若裸指针悬空,读 ctx.n 会坏掉或回调根本跑不起来。 +local cross_hits = nil + +function arm_cross_frame() + local ctx = { n = 7, tag = "ok" } + local conn = mysql.connect({ + host = "127.0.0.1", + port = 1, + user = "root", + password = "x", + db = "test", + timeout_ms = 500 + }, function(c, err, success) end) + + conn:query("SELECT 1", function(c, err, result) + if ctx.tag == "ok" and ctx.n == 7 and type(err) == "string" then + cross_hits = ctx.n + else + cross_hits = -1 + end + end) + return 1 +end + +function pump_cross_frame() + for i = 1, 2000 do + runtime.tick() + if cross_hits ~= nil then break end + os.sleep(1) + end + if cross_hits == nil then return 0 end + return cross_hits +end diff --git a/test/test_mysql.cpp b/test/test_mysql.cpp index 9ad9be3..dea187c 100644 --- a/test/test_mysql.cpp +++ b/test/test_mysql.cpp @@ -1,8 +1,13 @@ #include "fakelua.h" +#include "native/mysql/mysql_field_convert.h" +#include "native/mysql/native_mysql.h" +#include "native/native_common.h" #include "test_jit.h" #include "gtest/gtest.h" using namespace fakelua; +using namespace fakelua::mysql; + // MySQL 模块测试 @@ -219,3 +224,141 @@ TEST(test_mysql, pool_with_lease_api) { EXPECT_EQ(ret, 1); FakeluaDeleteState(s); } + +// 回归(P1-4):pool:with 的 fn 抛错后连接必须归还,否则下一次 Acquire 拿不到。 +// 不依赖真实 MySQL:测试入口把池里的连接标成已连接再走真正的 PoolWith。 +TEST(test_mysql, pool_with_fn_throw_returns_connection) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + + int arg_count = 0; + bool is_vararg = false; + void *addr = inter::GetFuncAddr(s, JIT_TCC, "MysqlContractTest.make_thrower", arg_count, is_vararg); + ASSERT_NE(addr, nullptr); + CVar fn = inter::DispatchCall(s, addr, nullptr, arg_count, JIT_TCC); + ASSERT_EQ(fn.type_, static_cast(VarType::Closure)); + + const int released = mysql::TestPoolWithFnThrowReturnsConnection(s, fn); + EXPECT_EQ(released, 1); + FakeluaDeleteState(s); +} + +// 回归(P0-1):闭包回调跨过下一次顶层 Call 的临时 arena Reset 后仍然能读到捕获的表。 +TEST(test_mysql, closure_callback_survives_frame_reset) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + + int64_t armed = 0; + CallAll(s, "MysqlContractTest.arm_cross_frame", armed); + EXPECT_EQ(armed, 1); + // 顶层 Call 返回后显式再 Reset 一次,模拟宿主在帧末回收临时 arena。 + inter::Reset(s); + + int64_t hits = 0; + CallAll(s, "MysqlContractTest.pump_cross_frame", hits); + EXPECT_EQ(hits, 7); + FakeluaDeleteState(s); +} + +// 单元测试(P2-7):FieldCellToCVar 行值类型转换,无需真实 MySQL 连接。 +// 验证整数列返回 Int、浮点/DECIMAL 列返回 Float、其他列返回 String、NULL 返回 nil。 +// MYSQL_TYPE_* 常量取自 MariaDB/MySQL 协议规范(固定值,不随版本变化): +// LONG=3, LONGLONG=8, DOUBLE=5, NEWDECIMAL=246, VAR_STRING=253 +TEST(test_mysql, field_cell_to_cvar_type_conversion) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + + // 整数列 TINY(MYSQL_TYPE_TINY = 1) + { + FieldCell fv; + fv.is_null = false; + fv.value = "1"; + CVar v = FieldCellToCVar(s, fv, 1 /*MYSQL_TYPE_TINY*/); + EXPECT_EQ(v.type_, static_cast(VarType::Int)); + EXPECT_EQ(v.data_.i, 1); + } + // 整数列(MYSQL_TYPE_LONG = 3) + { + FieldCell fv; + fv.is_null = false; + fv.value = "42"; + CVar v = FieldCellToCVar(s, fv, 3 /*MYSQL_TYPE_LONG*/); + EXPECT_EQ(v.type_, static_cast(VarType::Int)); + EXPECT_EQ(v.data_.i, 42); + } + // 负整数(MYSQL_TYPE_LONGLONG = 8) + { + FieldCell fv; + fv.is_null = false; + fv.value = "-99"; + CVar v = FieldCellToCVar(s, fv, 8 /*MYSQL_TYPE_LONGLONG*/); + EXPECT_EQ(v.type_, static_cast(VarType::Int)); + EXPECT_EQ(v.data_.i, -99); + } + // 浮点列(MYSQL_TYPE_FLOAT = 4) + { + FieldCell fv; + fv.is_null = false; + fv.value = "1.5"; + CVar v = FieldCellToCVar(s, fv, 4 /*MYSQL_TYPE_FLOAT*/); + EXPECT_EQ(v.type_, static_cast(VarType::Float)); + EXPECT_NEAR(v.data_.f, 1.5, 1e-6); + } + // 浮点列(MYSQL_TYPE_DOUBLE = 5) + { + FieldCell fv; + fv.is_null = false; + fv.value = "3.14"; + CVar v = FieldCellToCVar(s, fv, 5 /*MYSQL_TYPE_DOUBLE*/); + EXPECT_EQ(v.type_, static_cast(VarType::Float)); + EXPECT_NEAR(v.data_.f, 3.14, 1e-9); + } + // DECIMAL 列(MYSQL_TYPE_DECIMAL = 0)与 NEWDECIMAL 都落成 double,精度见 README。 + { + FieldCell fv; + fv.is_null = false; + fv.value = "9.5"; + CVar v = FieldCellToCVar(s, fv, 0 /*MYSQL_TYPE_DECIMAL*/); + EXPECT_EQ(v.type_, static_cast(VarType::Float)); + EXPECT_NEAR(v.data_.f, 9.5, 1e-9); + } + // DECIMAL 列(MYSQL_TYPE_NEWDECIMAL = 246) + { + FieldCell fv; + fv.is_null = false; + fv.value = "123.456"; + CVar v = FieldCellToCVar(s, fv, 246 /*MYSQL_TYPE_NEWDECIMAL*/); + EXPECT_EQ(v.type_, static_cast(VarType::Float)); + EXPECT_NEAR(v.data_.f, 123.456, 1e-9); + } + // 字符串列(MYSQL_TYPE_VAR_STRING = 253) + { + FieldCell fv; + fv.is_null = false; + fv.value = "hello"; + CVar v = FieldCellToCVar(s, fv, 253 /*MYSQL_TYPE_VAR_STRING*/); + EXPECT_EQ(v.type_, static_cast(VarType::String)); + } + // NULL 字段(任意列类型) + { + FieldCell fv; + fv.is_null = true; + fv.value = ""; + CVar v = FieldCellToCVar(s, fv, 3 /*MYSQL_TYPE_LONG*/); + EXPECT_EQ(v.type_, static_cast(VarType::Nil)); + } + // 整数列但值为非数字 → 回退为 string(不崩溃) + { + FieldCell fv; + fv.is_null = false; + fv.value = "not_a_number"; + CVar v = FieldCellToCVar(s, fv, 3 /*MYSQL_TYPE_LONG*/); + EXPECT_EQ(v.type_, static_cast(VarType::String)); + } + + FakeluaDeleteState(s); +} From a5eef53772e0ecd6812227170ed7ebd8d0422a01 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Wed, 30 Sep 2026 21:46:55 +0800 Subject: [PATCH 3/8] fix: reject reassignment of file-level numeric constants A file-level numeric literal stays static const, and assigning it from a user function is a compile error with a source location. local x = func() is unchanged: the declaration is lowered to nil and __fakelua_init assigns it once. Co-authored-by: Cursor --- src/compile/c_gen.cpp | 13 ++---- src/compile/compile_common.h | 3 -- src/compile/type_inferencer.cpp | 46 ++++++++++++++----- src/compile/type_inferencer.h | 9 ++-- src/native/README.md | 2 +- src/native/README.zh.md | 2 +- .../test_global_const_reassign_dynamic.lua | 7 +++ .../infer/test_global_const_reassigned.lua | 16 ++----- test/lua/net/test_net_server_client.lua | 3 -- test/test_exception.cpp | 16 ++++--- test/test_infer.cpp | 46 ++++++++++++------- 11 files changed, 94 insertions(+), 69 deletions(-) create mode 100644 test/lua/infer/test_global_const_reassign_dynamic.lua diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index cb585bd..83eb38c 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -488,21 +488,18 @@ void CGen::GenerateGlobal(const SyntaxTreeInterfacePtr &chunk) { InferredType global_type = ir().global_const_vars.at(name); const auto cname = CIdent(name); const auto exp_node = std::dynamic_pointer_cast(exp); - // 声明后仍有再赋值点的变量不能 const:const + 赋值 = C 编译错误。 - // (此前只看推断类型,多赋值点全为编译期常量时会被误标 const。) - const bool reassigned = ir().global_reassigned_vars.contains(name); - const std::string qualifier = reassigned ? "static " : "static const "; + // 文件级数值 local 是常量。再赋值在类型推断阶段就已经报错,这里一律 static const。 if (global_type == T_INT) { if (!exp_node || exp_node->GetExpKind() == ExpKind::kNil) { - Out() << qualifier << "int64_t " << cname << " = 0;\n"; + Out() << "static const int64_t " << cname << " = 0;\n"; } else { - Out() << qualifier << "int64_t " << cname << " = " << CompileNumericExp(exp) << ";\n"; + Out() << "static const int64_t " << cname << " = " << CompileNumericExp(exp) << ";\n"; } } else if (global_type == T_FLOAT) { if (!exp_node || exp_node->GetExpKind() == ExpKind::kNil) { - Out() << qualifier << "double " << cname << " = 0.0;\n"; + Out() << "static const double " << cname << " = 0.0;\n"; } else { - Out() << qualifier << "double " << cname << " = " << CompileNumericExp(exp) << ";\n"; + Out() << "static const double " << cname << " = " << CompileNumericExp(exp) << ";\n"; } } else { // 非数值字面量:保留 static CVar 形式。 diff --git a/src/compile/compile_common.h b/src/compile/compile_common.h index a61f111..44ff6a6 100644 --- a/src/compile/compile_common.h +++ b/src/compile/compile_common.h @@ -584,9 +584,6 @@ struct InferResult { EvalTypeSnapshot main_eval_types; // 文件级/全局数值常量及其推断类型映射 std::unordered_map global_const_vars; - // 声明后仍被再赋值过的文件级 local 变量名。这些变量即使推断为数值类型也不能 - // 发射成 C 的 const 声明(const + 后续赋值 = C 编译错误),CGen 据此去掉 const。 - std::unordered_set global_reassigned_vars; // table 特化信息:table constructor 节点 → 特化信息 std::unordered_map table_spec_infos; // 流敏感 table 特化标注:Var 引用节点(kDot/kSquare 的 prefixexp 所指 Var 节点)→ 该程序点该变量的 spec 类型名。 diff --git a/src/compile/type_inferencer.cpp b/src/compile/type_inferencer.cpp index 1adb6e8..b6180c2 100644 --- a/src/compile/type_inferencer.cpp +++ b/src/compile/type_inferencer.cpp @@ -32,6 +32,24 @@ std::string SpecStringFieldCName(const std::string &key) { return out; } +// 预处理生成的文件级初始化函数。它的函数体里对 local x = func() 降级出来的 +// 绑定做一次赋值,不能当成用户再赋值。 +bool IsFakeluaInitFunction(const std::shared_ptr &func) { + if (!func) { + return false; + } + const auto fn = std::dynamic_pointer_cast(func->Funcname()); + if (!fn) { + return false; + } + const auto fnl = std::dynamic_pointer_cast(fn->FuncNameList()); + if (!fnl) { + return false; + } + const auto &names = fnl->Funcnames(); + return names.size() == 1 && names[0] == kInitFunctionName; +} + void UniquifySpecFieldCNames(std::vector &fields) { std::unordered_set used; for (auto &f: fields) { @@ -383,12 +401,13 @@ InferredType TypeInferencer::TypeEnvironment::MergeType(const InferredType old_t InferResult TypeInferencer::InferTypes(const ParseResult &pr, const CompileConfig &cfg) { LOG_DEBUG(s_, "engine", "InferTypes: start for {}", pr.file_name); + file_name_ = pr.file_name; file_level_types_.clear(); file_level_init_exps_.clear(); InferResult ir; EvalTypeSnapshot current_map; TypeEnvironment env; - TraversalContext tctx{current_map, env, nullptr, ir.var_define_nodes, ir.shadowed_decls, ir.global_reassigned_vars}; + TraversalContext tctx{current_map, env, nullptr, ir.var_define_nodes, ir.shadowed_decls}; LOG_DEBUG(s_, "engine", "InferTypes: step 1 - InferNode"); InferNode(pr.chunk, tctx); @@ -468,7 +487,12 @@ InferredType TypeInferencer::InferNode(const SyntaxTreeInterfacePtr &node, Trave } case SyntaxTreeType::Function: { const auto func = std::dynamic_pointer_cast(node); + const bool prev_init = in_init_function_; + if (IsFakeluaInitFunction(func)) { + in_init_function_ = true; + } InferNode(func->Funcbody(), tctx); + in_init_function_ = prev_init; return RecordType(current_map, node.get(), T_UNKNOWN); } case SyntaxTreeType::LocalFunction: { @@ -611,8 +635,8 @@ InferredType TypeInferencer::InferLocalVar(const std::shared_ptr if (const auto *init = tctx.env.LookupInitNode(name)) { tctx.var_define_nodes[var.get()] = init; - // 赋值目标解析到文件级 local 的 initializer → 这是一个再赋值点。 - // 有再赋值点的数值变量不能被 CGen 发射成 const(否则 C 编译报错, - // 见 global_reassigned_vars 的说明)。 - if (file_level_init_exps_.contains(init)) { - tctx.global_reassigned_vars.insert(name); + // 文件级数值字面量是 static const,用户函数里再赋值直接报编译期错误。 + // __fakelua_init 除外:local x = func() 不能做 C 静态初值,预处理把它改成 + // local x = nil,并只在 __fakelua_init 里写一次 x = func()。那是初始化,不是再赋值。 + if (!in_init_function_ && file_level_init_exps_.contains(init)) { + ThrowFakeluaException(std::format("cannot reassign file-level constant '{}' at {}", name, SyntaxTreeLocationStr(file_name_, assign))); } } @@ -1104,11 +1128,9 @@ EvalTypeSnapshot TypeInferencer::RunTrialInference(const SyntaxTreeInterfacePtr // 运行函数体类型推断(不新开作用域,参数已在当前作用域中定义)。 // Trial 推断不消费 shadow 信息(shadow 只与 AST 结构相关,主推断已覆盖), - // 也不收集再赋值信息(主推断已覆盖),但 TraversalContext 需要引用, - // 因此用丢弃式的容器占位。 + // 但 TraversalContext 需要一个引用,因此用丢弃式的容器占位。 std::set> dummy_shadowed_decls; - std::unordered_set dummy_reassigned_vars; - TraversalContext tctx{current_map, env, &ctx, var_define_nodes, dummy_shadowed_decls, dummy_reassigned_vars}; + TraversalContext tctx{current_map, env, &ctx, var_define_nodes, dummy_shadowed_decls}; InferBlock(std::dynamic_pointer_cast(func_block), false, tctx); // 快照本轮推断结果:为未被推断触及的节点补 T_UNKNOWN, diff --git a/src/compile/type_inferencer.h b/src/compile/type_inferencer.h index 9f93601..6f2a2c4 100644 --- a/src/compile/type_inferencer.h +++ b/src/compile/type_inferencer.h @@ -95,8 +95,6 @@ class TypeInferencer { const TrialInferenceContext *ctx = nullptr; std::unordered_map &var_define_nodes; std::set> &shadowed_decls; - // 文件级 local 再赋值收集(主推断传 ir.global_reassigned_vars,试推断传 dummy)。 - std::unordered_set &global_reassigned_vars; [[nodiscard]] bool IsTrialInference() const { return ctx != nullptr; @@ -279,9 +277,12 @@ class TypeInferencer { private: State *s_ = nullptr; + std::string file_name_; + // 正在推断 __fakelua_init。它里面的 x = func() 是文件级复杂初值的唯一赋值点,不是再赋值。 + bool in_init_function_ = false; std::unordered_map file_level_types_; - // 文件级 local 声明的 initializer 表达式节点。InferAssign 据此判断被赋值变量 - // 是否解析到文件级绑定(含遮蔽场景),进而收集"声明后再被赋值"的变量名。 + // 文件级数值字面量 local 的 initializer 节点。InferAssign 用它判断赋值目标是不是 + // 这条常量绑定(对函数内同名遮蔽免疫)。__fakelua_init 里的赋值不查这张表。 std::unordered_set file_level_init_exps_; // 不动点迭代轮次上限(实际通常 2 轮即可收敛)。 diff --git a/src/native/README.md b/src/native/README.md index 38ca020..b1c39d8 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -424,7 +424,7 @@ The flag is an integer depth on the State, so Linux, macOS and Windows behave th | Mutating fields of a runtime-created table | ✅ | Attaching a table created inside the callback to a long-lived table is a reliable pattern | | IO such as `send` / mysql `query` | ✅ | sends are pumped by the tick after dispatch finishes; queries queue asynchronously | | Assigning **fields** of a file-level `local` table | ❌ | file-level tables get CONST_FLAG after init; mutating content throws "attempt to modify a const table" | -| Rebinding a file-level `local` variable (`x = ...`) | ⚠️ | numeric variables with further assignment points are not emitted const by the JIT and assign fine; numeric variables never reassigned after declaration are optimized to C-level const — assigning them later is a compile error | +| Rebinding a file-level numeric `local` (`x = ...`) | ❌ | a file-level numeric local with a literal initializer is a constant; assigning it later is a compile error that names the source line. `local x = func()` cannot be a C static initializer: the declaration is lowered to `local x = nil` and `__fakelua_init` assigns it once when the shared library is loaded | --- diff --git a/src/native/README.zh.md b/src/native/README.zh.md index 239fc06..d1250f4 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -417,7 +417,7 @@ http 的 `CallNamed`、redis 的结果回调)。这是 State 上的一个整 | 改"运行时创建的 table"的字段 | ✅ | 回调里新建的 table 挂到长生命周期 table 上是可靠模式 | | `send` / mysql `query` 等 IO | ✅ | send 会在派发结束后由 tick 统一泵出;query 异步排队 | | 给文件级 `local` 表的**字段**赋值 | ❌ | 文件级表初始化后打 CONST_FLAG,改内容会抛 "attempt to modify a const table" | -| 重绑定文件级 `local` 变量(`x = ...`) | ⚠️ | 数值型变量如声明后有再赋值点,JIT 不会标 const,可正常赋值;声明后从未再赋值的数值变量被优化为 C 级 const,运行时再赋值是编译错误 | +| 重绑定文件级数值 `local`(`x = ...`) | ❌ | 字面量初值的文件级数值 local 是常量,函数里再赋值是带行号的编译期错误。`local x = func()` 不能做 C 静态初值:声明先写成 `local x = nil`,加载 so 时由 `__fakelua_init` 赋值一次 | --- diff --git a/test/lua/infer/test_global_const_reassign_dynamic.lua b/test/lua/infer/test_global_const_reassign_dynamic.lua new file mode 100644 index 0000000..73f0ea9 --- /dev/null +++ b/test/lua/infer/test_global_const_reassign_dynamic.lua @@ -0,0 +1,7 @@ +-- 右值是运行时值也同样禁止:常量与否不取决于赋值能不能在编译期算出来。 +local map_width = 2000 + +function init(w) + map_width = w + return map_width +end diff --git a/test/lua/infer/test_global_const_reassigned.lua b/test/lua/infer/test_global_const_reassigned.lua index e7e2733..a84f2f2 100644 --- a/test/lua/infer/test_global_const_reassigned.lua +++ b/test/lua/infer/test_global_const_reassigned.lua @@ -1,19 +1,9 @@ --- 文件级数值变量声明后有多个再赋值点(且全部是编译期常量)。 --- 曾被 JIT 误标为 static const,导致生成的 C 代码编译失败: --- error: cannot assign to variable 'next_bot_id' with const-qualified type --- 修复后:有再赋值点的变量发射为非 const 的 static int64_t,赋值语义与 Lua 一致。 +-- 有初值的文件级数值 local 是常量。函数里再赋值必须在编译期报错, +-- 并带上 Lua 文件位置,而不是等生成的 C 代码因为 const 赋值才失败。 +-- local x = func() 不在此列:声明会降成 nil,只由 __fakelua_init 赋值一次。 local next_bot_id = 0 function init() next_bot_id = 800000 return next_bot_id end - -function add() - next_bot_id = next_bot_id + 1 - return next_bot_id -end - -function get() - return next_bot_id -end diff --git a/test/lua/net/test_net_server_client.lua b/test/lua/net/test_net_server_client.lua index 84866cd..b2709ac 100644 --- a/test/lua/net/test_net_server_client.lua +++ b/test/lua/net/test_net_server_client.lua @@ -144,11 +144,9 @@ end -- 回归(P1-6):在 on_event 回调里直接调用 obj:send()(不走返回 "echo" 的路径)。 -- 派发上下文中的 send 会先入队,由本轮 tick 派发完后统一泵出;客户端随后收到数据。 local send_in_cb_server = nil -local send_in_cb_hits = 0 function on_send_in_cb(type, connid, data, len, reason) if type == "recv" then - send_in_cb_hits = send_in_cb_hits + 1 if send_in_cb_server then send_in_cb_server:send(connid, "direct:" .. data) end @@ -156,7 +154,6 @@ function on_send_in_cb(type, connid, data, len, reason) end function test_send_in_callback() - send_in_cb_hits = 0 local server = net.server({ port = 19987, maxconn = 4 }) server:dispatch("NetTest.on_send_in_cb") send_in_cb_server = server diff --git a/test/test_exception.cpp b/test/test_exception.cpp index 5d8af77..307e62d 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1125,18 +1125,20 @@ TEST(exception, const_no_init) { EXPECT_THROW(CompileFile(s, "./exception/test_const_no_init.lua", {}), std::exception); } -// 回归:文件级数值变量声明后再赋值曾是编译错误(被误标为 C const)。 -// 修复后(pitfalls P1-3):有再赋值点的变量不做 const 优化,赋值语义与 Lua 一致。 -// 从未再赋值的文件级常量仍保持 const,见 infer.test_global_const_int。 +// 文件级数值字面量是常量,函数里再赋值是编译期错误。 TEST(exception, const_reassign) { FakeluaStateGuard sg; auto s = sg.GetState(); ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - EXPECT_NO_THROW(CompileFile(s, "./exception/test_const_reassign.lua", {})); - int ret = 0; - Call(s, JIT_TCC, "test", ret); - EXPECT_EQ(ret, 4); + try { + CompileFile(s, "./exception/test_const_reassign.lua", {}); + FAIL() << "reassigning file-level constant a should fail"; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find("cannot reassign file-level constant 'a'"), std::string::npos); + EXPECT_NE(msg.find("test_const_reassign.lua"), std::string::npos); + } } TEST(exception, top_level_bare_local) { diff --git a/test/test_infer.cpp b/test/test_infer.cpp index 24c4a63..6725194 100644 --- a/test/test_infer.cpp +++ b/test/test_infer.cpp @@ -3023,25 +3023,37 @@ TEST(infer, test_global_const_float) { }); } -// 回归:文件级数值变量声明后有多个再赋值点(且全部是编译期常量)时, -// 曾被误标为 static const,JIT 生成的 C 代码编译失败。 -// 修复后:有再赋值点的变量发射为非 const 的 static int64_t;从未再赋值的仍保持 const。 +// 文件级数值字面量是常量。函数里再赋值必须在编译期报错,并带上 Lua 位置。 +// local x = func() 不走这条路径:C 静态初值不能调用函数,预处理改成 local x = nil, +// 只在 __fakelua_init 里赋值一次。见 test_global_init_multi_names_funcall。 TEST(infer, test_global_const_reassigned_not_const) { - const auto code = InferGetCCode("./infer/test_global_const_reassigned.lua"); - // 有再赋值点 → 不允许 const。 - ASSERT_NE(code.find("static int64_t next_bot_id = 0;"), std::string::npos); - ASSERT_EQ(code.find("static const int64_t next_bot_id"), std::string::npos); + const auto s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + try { + CompileFile(s, "./infer/test_global_const_reassigned.lua", {}); + FakeluaDeleteState(s); + FAIL() << "reassigning a file-level numeric constant should fail at compile time"; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find("cannot reassign file-level constant 'next_bot_id'"), std::string::npos); + EXPECT_NE(msg.find("test_global_const_reassigned.lua"), std::string::npos); + FakeluaDeleteState(s); + } +} - InferRunHelper([](State *s, JITType type, bool debug_mode) { - CompileFile(s, "./infer/test_global_const_reassigned.lua", {.debug_mode = debug_mode}); - int ret = 0; - Call(s, type, "init", ret); - Call(s, type, "add", ret); - Call(s, type, "add", ret); - int64_t v = 0; - Call(s, type, "get", v); - ASSERT_EQ(v, 800002); - }); +TEST(infer, test_global_const_reassign_dynamic) { + const auto s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + try { + CompileFile(s, "./infer/test_global_const_reassign_dynamic.lua", {}); + FakeluaDeleteState(s); + FAIL() << "reassigning a file-level numeric constant with a runtime value should fail"; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find("cannot reassign file-level constant 'map_width'"), std::string::npos); + EXPECT_NE(msg.find("test_global_const_reassign_dynamic.lua"), std::string::npos); + FakeluaDeleteState(s); + } } // --------------------------------------------------------------------------- From c81b8ff34cc00eab8b35d0fdd9b37f5d904263f2 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 1 Oct 2026 07:22:20 +0800 Subject: [PATCH 4/8] test: align mysql integration scripts with typed row values and async close-error contract PR20 changed mysql result rows to convert by column type (integer columns return numbers) and changed conn:query on a closed connection to deliver the error via callback instead of throwing. The C++ unit tests and README were updated, but seven legacy Lua integration scripts still asserted the old string values / pcall behavior, failing the Build pipeline (debug, release, cov). - stmt/multi_result/pool/query_error/datatypes/stmt_params: compare integer columns against numbers instead of strings - lifecycle: expect the closed-connection query error through the result callback after runtime.tick(), not a synchronous pcall error --- test/lua/mysql/test_mysql_datatypes.lua | 6 ++--- test/lua/mysql/test_mysql_lifecycle.lua | 26 +++++++++++++++++----- test/lua/mysql/test_mysql_multi_result.lua | 6 ++--- test/lua/mysql/test_mysql_pool.lua | 2 +- test/lua/mysql/test_mysql_query_error.lua | 2 +- test/lua/mysql/test_mysql_stmt.lua | 3 ++- test/lua/mysql/test_mysql_stmt_params.lua | 6 ++--- 7 files changed, 33 insertions(+), 18 deletions(-) diff --git a/test/lua/mysql/test_mysql_datatypes.lua b/test/lua/mysql/test_mysql_datatypes.lua index 7892fc8..30ca1ea 100644 --- a/test/lua/mysql/test_mysql_datatypes.lua +++ b/test/lua/mysql/test_mysql_datatypes.lua @@ -113,9 +113,9 @@ function test_datatypes() local row = result[3][1] - -- 验证 id 为 "1" - if row[1] ~= "1" then - print("id mismatch, expected '1', got:", tostring(row[1])) + -- 验证 id 为整数 1(INT 列按列类型返回 number) + if row[1] ~= 1 then + print("id mismatch, expected 1, got:", tostring(row[1])) conn:close() return 0 end diff --git a/test/lua/mysql/test_mysql_lifecycle.lua b/test/lua/mysql/test_mysql_lifecycle.lua index 35209e5..d8ce794 100644 --- a/test/lua/mysql/test_mysql_lifecycle.lua +++ b/test/lua/mysql/test_mysql_lifecycle.lua @@ -67,18 +67,32 @@ function test_lifecycle() conn:close() conn:close() - -- 3. 验证对已关闭连接调用 query 能够受控捕获错误 - local ok, err_msg = pcall(function() + -- 3. 验证对已关闭连接调用 query:不抛错,而是在后续 tick 由回调收到 "closed" 错误 + -- (契约:conn:query 的回调恰好触发一次,连接已关闭时回调收到错误字符串) + conn.query_done = false + conn.query_err = nil + local ok, call_err = pcall(function() conn:query("SELECT 1", "on_result") end) - if ok then - print("expected error when querying closed connection, but pcall succeeded") + if not ok then + print("query on closed connection should notify via callback, not throw:", tostring(call_err)) return 0 end - if not string.find(tostring(err_msg), "closed") then - print("error message should mention 'closed', got:", tostring(err_msg)) + for i = 1, 100 do + runtime.tick() + if conn.query_done then break end + os.sleep(1) + end + + if not conn.query_done then + print("closed-connection query callback never fired") + return 0 + end + + if conn.query_err == nil or not string.find(tostring(conn.query_err), "closed") then + print("error message should mention 'closed', got:", tostring(conn.query_err)) return 0 end diff --git a/test/lua/mysql/test_mysql_multi_result.lua b/test/lua/mysql/test_mysql_multi_result.lua index ac832ab..54db96b 100644 --- a/test/lua/mysql/test_mysql_multi_result.lua +++ b/test/lua/mysql/test_mysql_multi_result.lua @@ -90,9 +90,9 @@ function test_multi_result() conn:close() return 0 end - local expected = tostring(i) - if result[3][1][1] ~= expected then - print("result", i, "value mismatch:", result[3][1][1], "expected:", expected) + -- SELECT 字面整数按列类型返回 number(整数) + if result[3][1][1] ~= i then + print("result", i, "value mismatch:", result[3][1][1], "expected:", i) conn:close() return 0 end diff --git a/test/lua/mysql/test_mysql_pool.lua b/test/lua/mysql/test_mysql_pool.lua index 7df1dd4..f665cc7 100644 --- a/test/lua/mysql/test_mysql_pool.lua +++ b/test/lua/mysql/test_mysql_pool.lua @@ -65,7 +65,7 @@ function test_pool() pool:close() return 0 end - if result[3][1][1] ~= "1" then + if result[3][1][1] ~= 1 then print("pool query result mismatch:", result[3][1][1]) pool:release(conn) pool:close() diff --git a/test/lua/mysql/test_mysql_query_error.lua b/test/lua/mysql/test_mysql_query_error.lua index 31742bd..e7c1f38 100644 --- a/test/lua/mysql/test_mysql_query_error.lua +++ b/test/lua/mysql/test_mysql_query_error.lua @@ -108,7 +108,7 @@ function test_query_error() end local res = conn.query_result - if not res or res[1] ~= true or res[3][1][1] ~= "42" then + if not res or res[1] ~= true or res[3][1][1] ~= 42 then print("recovery query result mismatch") conn:close() return 0 diff --git a/test/lua/mysql/test_mysql_stmt.lua b/test/lua/mysql/test_mysql_stmt.lua index ca433b0..9fc0fa1 100644 --- a/test/lua/mysql/test_mysql_stmt.lua +++ b/test/lua/mysql/test_mysql_stmt.lua @@ -186,7 +186,8 @@ function test_stmt() end local row = result[3][1] - if row[1] ~= "1" or row[2] ~= "alice" then + -- INT 列按列类型返回 number(整数),VARCHAR 列返回 string + if row[1] ~= 1 or row[2] ~= "alice" then print("row mismatch:", row[1], row[2]) conn:stmt_close(conn.stmt_id) conn:close() diff --git a/test/lua/mysql/test_mysql_stmt_params.lua b/test/lua/mysql/test_mysql_stmt_params.lua index 67d3cb6..d5003a5 100644 --- a/test/lua/mysql/test_mysql_stmt_params.lua +++ b/test/lua/mysql/test_mysql_stmt_params.lua @@ -167,8 +167,8 @@ function test_stmt_params() end local r1 = res1[3][1] - if r1[1] ~= "1" then - print("r1 id expected '1', got:", tostring(r1[1])) + if r1[1] ~= 1 then + print("r1 id expected 1, got:", tostring(r1[1])) conn:stmt_close(select_stmt_id) conn:close() return 0 @@ -205,7 +205,7 @@ function test_stmt_params() end local r2 = res2[3][1] - if r2[1] ~= "2" or r2[2] ~= "bob" then + if r2[1] ~= 2 or r2[2] ~= "bob" then print("r2 values mismatch:", tostring(r2[1]), tostring(r2[2])) conn:stmt_close(select_stmt_id) conn:close() From 22d32a9c594b57d4873dc87a3f9505de17f96699 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 1 Oct 2026 18:52:03 +0800 Subject: [PATCH 5/8] refactor(mysql): replace async closure pin with named callback + serialized bound args Inline closures cannot be held safely across ticks: runtime values live on the temporary arena that every top-level Call resets, and pinning closure graphs into the const arena made that arena grow for the whole State lifetime (no GC). Async mysql callbacks (connect/query/stmt_prepare/stmt_execute) now accept only an in-package global function name, optionally followed by pure-data bound arguments: conn:query(sql, "on_result", tag, ctx_table) -> on_result(conn, err, result, tag, ctx_table) Bound args are encoded at registration into a self-owned byte blob using the existing serialize wire format (new internal wire_codec module shared with serialize.encode/decode; the Lua wire format is unchanged), survive any number of arena Resets, and are decoded in the dispatch frame and appended after the fixed callback arguments. The blob is released when the query completes or the connection is destroyed, so memory tracks in-flight queries instead of growing the const arena. Non-serializable values (closures, native objects) anywhere in bound args raise bad argument instead of being silently dropped. pool:with still takes an inline closure since it invokes fn synchronously in-frame. - remove arena_pin.{h,cpp} (const-arena closure deep copy) - extract wire codec from native_serialize into reusable wire_codec - update contract tests/README; cross-frame test driven on TCC only (named callbacks dispatch to the TCC artifact, file-level locals are per-engine statics and not shared across CallAll's engines) --- src/native/README.md | 34 +- src/native/README.zh.md | 40 ++- src/native/arena_pin.cpp | 191 ----------- src/native/arena_pin.h | 17 - src/native/mysql/mysql_connection.cpp | 82 +++-- src/native/mysql/mysql_connection.h | 20 +- src/native/mysql/native_mysql.cpp | 47 +-- src/native/serialize/native_serialize.cpp | 292 +---------------- src/native/serialize/wire_codec.cpp | 381 ++++++++++++++++++++++ src/native/serialize/wire_codec.h | 46 +++ test/lua/mysql/test_mysql_contract.lua | 143 ++++---- test/test_mysql.cpp | 10 +- 12 files changed, 665 insertions(+), 638 deletions(-) delete mode 100644 src/native/arena_pin.cpp delete mode 100644 src/native/arena_pin.h create mode 100644 src/native/serialize/wire_codec.cpp create mode 100644 src/native/serialize/wire_codec.h diff --git a/src/native/README.md b/src/native/README.md index b1c39d8..e72296b 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -589,13 +589,25 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv `ssl`: omit/`false`/`"disable"` keeps plaintext (default). `true`/`"require"` demands TLS. `"enable"` uses TLS when the server offers it. Optional `ssl_ca` PEM enables certificate verification. -**Callback arguments:** every `cb` accepts either an in-package global function name (string, e.g. -`"DB.on_result"`) or an **inline closure** (`function(conn, err, result) ... end`). Closures can -capture context. FakeLua has no GC: runtime values live on the temporary arena, and a top-level -`Call` resets that arena. Registering a callback copies the closure and the tables/strings it -captures onto the const arena, which is not reset and lives as long as the State, so the callback -survives the next frame. Passing anything else raises a "bad argument" error instead of -silently dropping the callback. +**Callback arguments:** every async `cb` accepts only an in-package global function name +(string, e.g. `"on_result"` or `"DB.on_result"`); **inline closures are not supported**. FakeLua +has no GC: runtime values live on the temporary arena, and a top-level `Call` resets that arena, so +a raw closure pointer cannot be held safely across ticks. To pass context, append **bound +arguments** (pure data) after the function name: `conn:query(sql, "on_result", tag, ctx_table)`. +Bound arguments are serialized into a self-owned byte blob at registration time (same wire format +as `serialize.encode`), survive any number of `Reset`s, and are deserialized in the dispatch frame +and appended after the fixed callback arguments. The blob is released when the query completes or +the connection is destroyed, so memory usage tracks in-flight queries only; the const arena is not +used. + +- Bound arguments may be nil/boolean/number/string and nested tables thereof (no cycles); + non-serializable values (closures, native objects, ...) anywhere in the nesting raise a + "bad argument" error — fields are never silently dropped. +- Bound arguments are a **registration-time snapshot** (copy by value): mutating the original + table after registration is not visible to the callback. Put mutable cross-frame state on the + connection object itself (e.g. `conn.query_done`). +- `pool:with(fn)` invokes fn **synchronously in the same frame** and never stores it across + ticks, so inline closures remain supported there. **Callback contract:** once `conn:query` is called its callback fires **exactly once** — when the connection is not ready (handshaking / reconnecting / a previous query still in flight) the query is @@ -605,11 +617,11 @@ terminal error state, the callback receives an error string. All callbacks are d | Function/Method | Description | |----------|-------------| -| `mysql.connect(config, cb)` | Async connect; callback `function cb(conn, err, success)` | +| `mysql.connect(config, cb, ...)` | Async connect; callback `cb(conn, err, success, ...)`; `...` are bound args appended after the fixed args | | `mysql_pool.create(config)` | Create connection pool | -| `conn:query(sql, cb)` | Async query; callback `function cb(conn, err, result)` | -| `conn:stmt_prepare(sql, cb)` | Prepare statement | -| `conn:stmt_execute(id, params, cb)` | Execute prepared statement | +| `conn:query(sql, cb, ...)` | Async query; callback `cb(conn, err, result, ...)`; `...` are bound args | +| `conn:stmt_prepare(sql, cb, ...)` | Prepare statement; callback `cb(conn, err, stmt_id, ...)` | +| `conn:stmt_execute(id, params, cb, ...)` | Execute prepared statement; bound args start at the 4th argument | | `conn:stmt_close(id)` | Close prepared statement | | `conn:close()` | Close connection | | `pool:acquire()` | Get connection from pool (nil when none available) | diff --git a/src/native/README.zh.md b/src/native/README.zh.md index d1250f4..3432b03 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -641,11 +641,19 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new `ssl`:省略/`false`/`"disable"` 保持明文(默认)。`true`/`"require"` 强制 TLS。`"enable"` 在服务器支持时使用 TLS。可选 `ssl_ca` PEM 会校验证书。 -**回调参数:** 所有 `cb` 既支持包内全局函数名(字符串,如 `"DB.on_result"`),也支持**内联闭包** -(`function(conn, err, result) ... end`)。闭包可以捕获上下文。fakelua 没有 GC:运行期值在临时 -arena 上,顶层 `Call` 会 `Reset` 这块内存。登记回调时闭包及其捕获的表/字符串会被复制到不参与 -Reset 的 const arena(与 State 同寿),所以回调可以跨帧存活。传其他类型会直接报 "bad argument" -错误(不会静默丢弃回调)。 +**回调参数:** 所有异步回调 `cb` 只接受**包内全局函数名**(字符串,如 `"on_result"` 或 +`"DB.on_result"`),**不支持内联闭包**。fakelua 没有 GC:运行期值在临时 arena 上,顶层 +`Call` 会 `Reset` 这块内存,无法安全地跨 tick 持有闭包裸指针。需要上下文时在函数名后面追加 +**绑定参数**(纯数据):`conn:query(sql, "on_result", tag, ctx_table)`。绑定参数在登记当下被 +序列化成独立字节串暂存(与 `serialize.encode` 同一 wire 格式),跨任意次 `Reset` 不丢; +结果回来时在派发帧内反序列化,追加在固定参数之后调用。字节串随 query 完成/连接销毁释放, +内存只与在途 query 数相关,不占用 const arena。 + +- 绑定参数支持 nil/boolean/number/string 及其嵌套表(不可有环);闭包、native 对象等 + 不可序列化类型出现在任意嵌套位置都会直接报 "bad argument",不会静默丢字段。 +- 绑定参数是**登记时刻的值快照**(按值复制),登记之后调用方再改原表,回调看不到; + 需要跨帧共享的可变状态请挂在连接对象字段上(如 `conn.query_done`)。 +- `pool:with(fn)` 的 fn 是**同帧同步调用**、不跨 tick 暂存,因此仍然支持内联闭包。 **回调契约:** `conn:query` 一旦被调用,其回调**恰好被调用一次**——连接未就绪(握手中/重连中/ 上一条 query 仍在飞行)时 query 排队,连接可用后自动执行;连接已关闭或进入错误终态时,回调会 @@ -653,11 +661,11 @@ Reset 的 const arena(与 State 同寿),所以回调可以跨帧存活。 | 函数/方法 | 说明 | |------|------| -| `mysql.connect(config, cb)` | 异步连接;回调 `function cb(conn, err, success)` | +| `mysql.connect(config, cb, ...)` | 异步连接;回调 `cb(conn, err, success, ...)`;`...` 为绑定参数,追加在固定参数后 | | `mysql_pool.create(config)` | 创建连接池 | -| `conn:query(sql, cb)` | 异步查询;回调 `function cb(conn, err, result)` | -| `conn:stmt_prepare(sql, cb)` | 预处理语句 | -| `conn:stmt_execute(id, params, cb)` | 执行预处理语句 | +| `conn:query(sql, cb, ...)` | 异步查询;回调 `cb(conn, err, result, ...)`;`...` 为绑定参数 | +| `conn:stmt_prepare(sql, cb, ...)` | 预处理语句;回调 `cb(conn, err, stmt_id, ...)` | +| `conn:stmt_execute(id, params, cb, ...)` | 执行预处理语句;绑定参数从第 4 个参数起 | | `conn:stmt_close(id)` | 关闭预处理语句 | | `conn:close()` | 关闭连接 | | `pool:acquire()` | 从池获取连接(无可用连接返回 nil) | @@ -681,14 +689,18 @@ Reset 的 const arena(与 State 同寿),所以回调可以跨帧存活。 > 需要精确小数时在 SQL 里 `CAST(price AS CHAR)`,在 Lua 里按字符串处理。 ```lua --- 租约式用法:fn 返回后连接自动归还,即使中途抛错也不会泄漏 +-- 租约式用法:fn 是同帧同步调用,仍可用内联闭包;fn 返回后连接自动归还,抛错也不泄漏 local ok = pool:with(function(c) - -- query 是异步的:结果在之后的 runtime.tick() 里通过回调到达 - c:query("UPDATE user SET online = 0 WHERE last_login < 100", function(conn, err, result) - if err then print("query failed:", err) end - end) + -- query 是异步的:结果在之后的 runtime.tick() 里通过【具名回调】到达。 + -- 绑定参数(这里的 "kick")登记时序列化暂存,派发时追加在 result 之后。 + c:query("UPDATE user SET online = 0 WHERE last_login < 100", "on_user_offline", "kick") return true end) + +function on_user_offline(conn, err, result, tag) + -- tag == "kick",是登记 query 时传入的绑定参数快照 + if err then print("query failed:", err, tag) end +end ``` --- diff --git a/src/native/arena_pin.cpp b/src/native/arena_pin.cpp deleted file mode 100644 index e7dc205..0000000 --- a/src/native/arena_pin.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "native/arena_pin.h" - -#include "interp/func_proto.h" -#include "native/table/native_table.h" -#include "state/state.h" -#include "var/var_closure.h" -#include "var/var_multi.h" -#include "var/var_string.h" -#include "var/var_table.h" - -#include -#include -#include -#include - -namespace fakelua { - -namespace { - -struct PinSeen { - std::unordered_map tables; - std::unordered_map closures; - std::unordered_map strings; -}; - -CVar PinCVar(State *s, CVar v, PinSeen &seen); - -VarString *PinString(State *s, const VarString *src, PinSeen &seen) { - if (!src) return nullptr; - if (s->GetHeap().OwnsConst(src)) return const_cast(src); - if (const auto it = seen.strings.find(src); it != seen.strings.end()) return it->second; - auto &alloc = s->GetHeap().GetAllocator(true); - auto *dst = static_cast(alloc.Alloc(sizeof(VarString) + src->Size())); - new (dst) VarString(src->Str()); - seen.strings.emplace(src, dst); - return dst; -} - -const char *PinCodeStr(State *s, const char *code) { - if (!code || code == kInterpClosureMagic) return code; - const size_t n = std::strlen(code); - auto &alloc = s->GetHeap().GetAllocator(true); - auto *dst = static_cast(alloc.Alloc(n + 1)); - std::memcpy(dst, code, n + 1); - return dst; -} - -VarClosure *PinClosure(State *s, VarClosure *cl, PinSeen &seen) { - if (!cl) return nullptr; - // 已经在 const arena 上的闭包(上次登记钉过)直接复用,避免再复制一份捕获表。 - if (s->GetHeap().OwnsConst(cl)) return cl; - if (const auto it = seen.closures.find(cl); it != seen.closures.end()) return it->second; - - const int nup = cl->upvalue_count > 0 ? cl->upvalue_count : 0; - auto &alloc = s->GetHeap().GetAllocator(true); - auto *out = static_cast(alloc.Alloc(sizeof(VarClosure) + static_cast(nup) * sizeof(CVar *))); - out->func_ptr = cl->func_ptr; - out->upvalue_count = nup; - out->expected_arg_count = cl->expected_arg_count; - out->is_vararg = cl->is_vararg; - out->code_str = PinCodeStr(s, cl->code_str); - seen.closures.emplace(cl, out); - - for (int i = 0; i < nup; ++i) { - CVar *src = cl->upvalues[i]; - if (!src) { - out->upvalues[i] = nullptr; - continue; - } - CVar pinned = PinCVar(s, *src, seen); - *src = pinned; - auto *box = static_cast(alloc.Alloc(sizeof(CVar))); - *box = pinned; - out->upvalues[i] = box; - } - return out; -} - -CVar PinTable(State *s, CVar src, PinSeen &seen) { - VarTable *t = src.data_.t; - if (!t) return inter::NativeToFakeluaNil(s); - // 同一帧里第二个闭包再钉时,表已经在 const arena 上,必须返回同一份, - // 否则连接回调和查询回调会各改各的副本。 - if (s->GetHeap().OwnsConst(t)) return src; - if (const auto it = seen.tables.find(t); it != seen.tables.end()) return it->second; - - // CreateTable 在外层 ConstAllocScope 里走 const arena。 - CVar result = table::TableHelper::CreateTable(s); - result.flag_ = 0; - seen.tables.emplace(t, result); - VarTable *out = result.data_.t; - out->spec_get = t->spec_get; - out->spec_set = t->spec_set; - out->spec_count = t->spec_count; - out->spec_bytes = t->spec_bytes; - out->spec_cvars = t->spec_cvars; - - constexpr uint32_t kMaxSpecBytes = 1u << 20; - if (t->spec && t->spec_bytes > 0 && t->spec_bytes <= kMaxSpecBytes) { - auto &alloc = s->GetHeap().GetAllocator(true); - auto *blob = alloc.Alloc(t->spec_bytes); - std::memcpy(blob, t->spec, t->spec_bytes); - out->spec = blob; - if (t->spec_cvars > 0) { - if (t->spec_bytes != t->spec_cvars * sizeof(CVar)) { - ThrowFakeluaException("pin closure: spec table layout does not match CVar array"); - } - auto *fields = static_cast(blob); - for (uint32_t i = 0; i < t->spec_cvars; ++i) { - fields[i] = PinCVar(s, fields[i], seen); - } - } - } else { - out->spec = t->spec; - } - - if (t->spec_count > 0 && t->spec_keys && t->spec_vals) { - auto &alloc = s->GetHeap().GetAllocator(true); - out->spec_keys = static_cast(alloc.Alloc(sizeof(CVar) * t->spec_count)); - out->spec_vals = static_cast(alloc.Alloc(sizeof(CVar) * t->spec_count)); - for (uint32_t i = 0; i < t->spec_count; ++i) { - out->spec_keys[i] = PinCVar(s, t->spec_keys[i], seen); - if (t->spec_cvars > 0 && out->spec) { - out->spec_vals[i] = static_cast(out->spec)[i]; - } else { - out->spec_vals[i] = PinCVar(s, t->spec_vals[i], seen); - } - } - } - - // 特化表和普通表:按键重放到新表上。spec_set 会写回上面的 const spec 块。 - // 不透明 spec(NativeObject)的字段不在哈希部分,避免再走 spec_set。 - const bool rebuild_hash = t->spec_cvars > 0 || t->spec == nullptr; - if (rebuild_hash) { - table::TableHelper::ForEachKV(src, [&](CVar key, CVar val) { - CVar pk = PinCVar(s, key, seen); - CVar pv = PinCVar(s, val, seen); - table::TableHelper::SetTable(s, result, pk, pv); - }); - } else if (t->bucket_count_ == 0) { - out->count_ = t->count_; - for (uint32_t i = 0; i < t->count_ && i < VarTable::QUICK_DATA_SIZE; ++i) { - static_cast(out->quick_data_[i].key) = PinCVar(s, t->quick_data_[i].key, seen); - static_cast(out->quick_data_[i].val) = PinCVar(s, t->quick_data_[i].val, seen); - out->quick_data_[i].hash = t->quick_data_[i].hash; - } - } - - result.flag_ = src.flag_; - seen.tables[t] = result; - return result; -} - -CVar PinCVar(State *s, CVar v, PinSeen &seen) { - switch (static_cast(v.type_)) { - case VarType::String: - v.data_.s = PinString(s, v.data_.s, seen); - return v; - case VarType::Table: - return PinTable(s, v, seen); - case VarType::Closure: { - VarClosure *pinned = PinClosure(s, v.data_.cl, seen); - v.data_.cl = pinned; - return v; - } - case VarType::Multi: { - VarMulti *m = v.data_.m; - if (!m) return v; - const int n = static_cast(m->GetCount()); - CVar out = inter::AllocMultiCVar(s, n); - for (int i = 0; i < n; ++i) { - inter::SetMultiCVarElement(out, i, PinCVar(s, m->GetVars()[i], seen)); - } - return out; - } - default: - return v; - } -} - -}// namespace - -VarClosure *PinClosureForAsync(State *s, VarClosure *cl) { - if (!s || !cl) return nullptr; - // 嵌套的表重哈希 / Multi 分配都走 GetValueAllocator,这里切到 const arena。 - State::ConstAllocScope const_alloc(s); - PinSeen seen; - return PinClosure(s, cl, seen); -} - -}// namespace fakelua diff --git a/src/native/arena_pin.h b/src/native/arena_pin.h deleted file mode 100644 index fdbedf1..0000000 --- a/src/native/arena_pin.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "fakelua.h" - -namespace fakelua { - -class VarClosure; - -// 把闭包(以及它捕获的表、字符串、嵌套闭包)复制到 const arena。 -// fakelua 没有 GC。运行期值默认落在临时 arena 上,每次顶层 Call 开头的 -// State::Reset() 会把这块 arena 的水位清零,裸指针跨帧就会悬空。 -// const arena 不参与 Reset,和 State 同寿,因此异步回调可以一直持有这里返回的指针。 -// 不做引用计数:State 销毁时整块 arena 一起释放。 -// 同时把捕获盒子里的值改写成复制后的对象,这样同一帧内调用方和回调看到的是同一份表。 -VarClosure *PinClosureForAsync(State *s, VarClosure *cl); - -}// namespace fakelua diff --git a/src/native/mysql/mysql_connection.cpp b/src/native/mysql/mysql_connection.cpp index 230fe28..4306cfe 100644 --- a/src/native/mysql/mysql_connection.cpp +++ b/src/native/mysql/mysql_connection.cpp @@ -1,5 +1,6 @@ #include "native/mysql/mysql_connection.h" #include "native/native_common.h" +#include "native/serialize/wire_codec.h" #include "native/table/native_table.h" #include "util/logging.h" #include "var/var.h" @@ -788,12 +789,6 @@ void MysqlConnection::SetError(MysqlErrorType type, uint16_t code, const std::st } void MysqlConnection::InvokeCallback(const ResultCallback &cb, CVar *args, int n) { - // 闭包:DispatchCallClosure 内部处理 func_ptr / load() 源码闭包两种形态。 - if (cb.closure) { - // kNativeCallbackJit 只决定异常边界,函数地址用闭包自己的 func_ptr。 - inter::DispatchCallClosure(lua_state_, cb.closure, args, n, kNativeCallbackJit); - return; - } // 全局函数名:查 VM 注册表。 auto func = lua_state_->GetVM().GetFunction(cb.name); if (func.Empty()) return; @@ -807,21 +802,43 @@ void MysqlConnection::InvokeCallback(const ResultCallback &cb, CVar *args, int n inter::DispatchCall(lua_state_, addr, args, n, jit_type); } +namespace { + +// 把 3 个固定参数和登记时暂存的绑定参数拼成完整回调参数: +// 固定参数在前,绑定参数(登记时刻的值快照)追加在后。 +// DecodedArgs 持有解码字符串的后备内存,必须活到回调调用结束。 +std::vector BuildCallbackArgs(CVar a0, CVar a1, CVar a2, serialize::DecodedArgs &decoded) { + std::vector all; + all.reserve(3 + decoded.vars.size()); + all.push_back(a0); + all.push_back(a1); + all.push_back(a2); + for (CVar v: decoded.vars) all.push_back(v); + return all; +} + +}// namespace + void MysqlConnection::DispatchConnect(const char *err_msg) { TickDepthGuard guard(tick_depth_); native::IoContext::DispatchScope dispatch_scope(io_); if (!lua_state_ || connect_cb_.Empty()) return; - CVar args[3]; - args[0] = native_obj_ ? inter::NativeToFakeluaNativeObject(lua_state_, native_obj_) : inter::NativeToFakeluaNil(lua_state_); + // 在派发帧内 decode 绑定参数到临时 arena;decoded 生命周期覆盖整个函数。 + serialize::DecodedArgs decoded = serialize::WireDecodeCallbackArgs(lua_state_, connect_cb_.bound); + + CVar a0 = native_obj_ ? inter::NativeToFakeluaNativeObject(lua_state_, native_obj_) : inter::NativeToFakeluaNil(lua_state_); + CVar a1; + CVar a2; if (err_msg && err_msg[0]) { - args[1] = inter::NativeToFakeluaString(lua_state_, err_msg); - args[2] = inter::NativeToFakeluaInt(lua_state_, 0); + a1 = inter::NativeToFakeluaString(lua_state_, err_msg); + a2 = inter::NativeToFakeluaInt(lua_state_, 0); } else { - args[1] = inter::NativeToFakeluaNil(lua_state_); - args[2] = inter::NativeToFakeluaInt(lua_state_, 1); + a1 = inter::NativeToFakeluaNil(lua_state_); + a2 = inter::NativeToFakeluaInt(lua_state_, 1); } - InvokeCallback(connect_cb_, args, 3); + std::vector args = BuildCallbackArgs(a0, a1, a2, decoded); + InvokeCallback(connect_cb_, args.data(), static_cast(args.size())); } void MysqlConnection::DispatchCallbackWithResult(const ResultCallback &cb, const char *err_msg) { @@ -829,34 +846,29 @@ void MysqlConnection::DispatchCallbackWithResult(const ResultCallback &cb, const native::IoContext::DispatchScope dispatch_scope(io_); if (!lua_state_ || cb.Empty()) return; + // 多结果集可能在本函数内多次调用同一回调;绑定参数只 decode 一次,各次调用复用。 + // decoded 必须活到最后一次 InvokeCallback 返回。 + serialize::DecodedArgs decoded = serialize::WireDecodeCallbackArgs(lua_state_, cb.bound); + const char *msg = err_msg && err_msg[0] ? err_msg : "query failed"; - CVar args[3]; - args[0] = native_obj_ ? inter::NativeToFakeluaNativeObject(lua_state_, native_obj_) : inter::NativeToFakeluaNil(lua_state_); + CVar a0 = native_obj_ ? inter::NativeToFakeluaNativeObject(lua_state_, native_obj_) : inter::NativeToFakeluaNil(lua_state_); + CVar nil{}; + nil.type_ = static_cast(VarType::Nil); + if (err_msg) { - args[1] = inter::NativeToFakeluaString(lua_state_, msg); - CVar nil{}; - nil.type_ = static_cast(VarType::Nil); - args[2] = nil; - InvokeCallback(cb, args, 3); + std::vector args = BuildCallbackArgs(a0, inter::NativeToFakeluaString(lua_state_, msg), nil, decoded); + InvokeCallback(cb, args.data(), static_cast(args.size())); } else if (dispatch_stmt_id_ != 0) { - CVar nil{}; - nil.type_ = static_cast(VarType::Nil); - args[1] = nil; - args[2] = inter::NativeToFakeluaInt(lua_state_, static_cast(dispatch_stmt_id_)); - InvokeCallback(cb, args, 3); + std::vector args = BuildCallbackArgs(a0, nil, + inter::NativeToFakeluaInt(lua_state_, static_cast(dispatch_stmt_id_)), decoded); + InvokeCallback(cb, args.data(), static_cast(args.size())); } else if (pending_results_.empty()) { - CVar nil{}; - nil.type_ = static_cast(VarType::Nil); - args[1] = nil; - args[2] = table::TableHelper::CreateTable(lua_state_); - InvokeCallback(cb, args, 3); + std::vector args = BuildCallbackArgs(a0, nil, table::TableHelper::CreateTable(lua_state_), decoded); + InvokeCallback(cb, args.data(), static_cast(args.size())); } else { for (const auto &rs: pending_results_) { - CVar nil{}; - nil.type_ = static_cast(VarType::Nil); - args[1] = nil; - args[2] = ResultsetToLua(lua_state_, rs); - InvokeCallback(cb, args, 3); + std::vector args = BuildCallbackArgs(a0, nil, ResultsetToLua(lua_state_, rs), decoded); + InvokeCallback(cb, args.data(), static_cast(args.size())); if (close_pending_) break; } } diff --git a/src/native/mysql/mysql_connection.h b/src/native/mysql/mysql_connection.h index 0049a98..da8d2e5 100644 --- a/src/native/mysql/mysql_connection.h +++ b/src/native/mysql/mysql_connection.h @@ -26,7 +26,6 @@ namespace fakelua { struct CVar; class State; class NativeObject; -class VarClosure; }// namespace fakelua // FieldCell 和 FieldCellToCVar 声明在独立的轻量头文件中(不依赖 mysql.h/libevent), @@ -41,15 +40,19 @@ struct StmtParam { }; -// C++ → Lua 回调:支持全局函数名(字符串)或闭包(VarClosure*)。 -// fakelua 没有 GC。运行期闭包默认在临时 arena 上,顶层 Call 的 State::Reset() -// 会回收它。登记回调时 PinClosureForAsync 把闭包复制到 const arena(不参与 Reset, -// 与 State 同寿),因此这里持有裸指针是安全的,不需要引用计数。 +// C++ → Lua 异步回调:全局函数名 + 绑定参数。 +// fakelua 没有 GC,运行期闭包活在临时 arena 上,顶层 Call 的 State::Reset() 会回收, +// 无法安全地跨 tick 持有闭包裸指针。因此异步回调只接受【全局函数名】;脚本需要上下文时 +// 用绑定参数(mysql.connect / conn:query 等名字后面的多余实参): +// 登记当下把纯数据参数序列化成自有的字节串 bound(nil/bool/number/string/table), +// 跨任意次 Reset 暂存;结果派发时在当前 tick 帧内 decode 到临时 arena,追加在固定参数 +// (conn, err, result, ...)之后调用函数。bound 随 query 完成/连接销毁释放, +// 内存只与在途 query 数相关,不占用 const arena。 struct ResultCallback { std::string name; - VarClosure *closure = nullptr; + std::string bound;// wire 编码的绑定参数元组;空串表示无绑定参数 - [[nodiscard]] bool Empty() const { return name.empty() && closure == nullptr; } + [[nodiscard]] bool Empty() const { return name.empty(); } }; enum class SslMode { @@ -191,7 +194,8 @@ class MysqlConnection { void DispatchCallbackWithResult(const ResultCallback &cb, const char *err_msg); void SetError(MysqlErrorType type, uint16_t code, const std::string &msg, const std::string &sql_state); - // 统一的回调调用入口:闭包走 DispatchCallClosure,函数名走 VM 查表。 + // 统一的回调调用入口:按函数名查 VM 注册表并派发。 + // 调用方需已把固定参数和 decode 后的绑定参数拼进 args。 // 回调缺失或不可调用时返回空 CVar(调用方无需关心返回值)。 void InvokeCallback(const ResultCallback &cb, CVar *args, int n); // 连接可用时启动下一条排队 query;连接进入终态(Error/Idle)时 diff --git a/src/native/mysql/native_mysql.cpp b/src/native/mysql/native_mysql.cpp index 5070191..ea99aea 100644 --- a/src/native/mysql/native_mysql.cpp +++ b/src/native/mysql/native_mysql.cpp @@ -1,9 +1,9 @@ -#include "native/arena_pin.h" #include "native/mysql/native_mysql.h" #include "native/mysql/mysql_connection.h" #include "native/mysql/mysql_connection_pool.h" #include "native/native_common.h" #include "native/object/native_object.h" +#include "native/serialize/wire_codec.h" #include "native/table/native_table.h" #include "var/var.h" @@ -37,23 +37,25 @@ static std::string CVarToString(CVar v) { return {}; } -// 解析回调参数:支持全局函数名(字符串)或闭包(内联 function 字面量)。 -// 其他类型直接抛错——历史上闭包会被静默转成空串、回调永不触发(P0-1),宁可响亮报错。 -static ResultCallback CVarToCallback(State *s, CVar v, int argno, const char *fname) { - if (v.type_ == static_cast(VarType::Closure) && v.data_.cl) { - // 闭包本体在临时 arena 上,下一次顶层 Call 的 Reset() 会回收。 - // 异步回调要跨 tick,复制到 const arena(无 GC,和 State 同寿)。 - VarClosure *pinned = PinClosureForAsync(s, v.data_.cl); - if (!pinned) ThrowBadArgument(argno, fname, "non-empty function expected"); - return ResultCallback{{}, pinned}; +// 解析异步回调:只接受全局函数名(字符串)。 +// fakelua 没有 GC,内联闭包活在临时 arena 上,无法安全地跨多次 tick/Reset 保存, +// 因此异步回调不支持闭包;需要上下文时在函数名后追加纯数据绑定参数: +// conn:query(sql, "on_result", tag, ctx_table) +// 绑定参数在登记当下序列化成自有字节串随回调暂存,派发时 decode 追加在固定参数后。 +// 名称参数非法(闭包/数字/空串等)直接抛错——历史上坏回调会被静默转成空串、永不触发。 +static ResultCallback BuildCallback(State *s, CVar name_v, int name_argno, CVar *args, int bound_first, int n, + const char *fname) { + if (name_v.type_ == static_cast(VarType::Closure)) { + ThrowBadArgument(name_argno, fname, + "global callback function name expected; inline closures are not supported for async callbacks, pass a name string plus data args"); } - std::string name = CVarToString(v); - if (!name.empty()) return ResultCallback{std::move(name), nullptr}; - // Closure 类型的 data_.cl 为空同样无法调用。 - if (v.type_ == static_cast(VarType::Closure)) { - ThrowBadArgument(argno, fname, "non-empty function expected"); + std::string name = CVarToString(name_v); + if (name.empty()) { + ThrowBadArgument(name_argno, fname, "callback function name expected"); } - ThrowBadArgument(argno, fname, "function or global function name expected"); + // 绑定参数严格校验 + 序列化(含闭包/native 对象等直接抛 bad argument)。 + std::string bound = serialize::WireEncodeCallbackArgs(s, args, bound_first, n, fname, bound_first + 1); + return ResultCallback{std::move(name), std::move(bound)}; } // Retrieve MysqlConnection* from NativeObject @@ -213,8 +215,8 @@ static CVar MysqlConnect(State *s, CVar *args, int n) { if (user.empty()) ThrowBadArgument(1, "mysql.connect", "user required"); - // Read callback: global function name or closure - ResultCallback cb = CVarToCallback(s, a1, 2, "mysql.connect"); + // Read callback: global function name; args after it are serialized bound params + ResultCallback cb = BuildCallback(s, a1, 2, args, 2, n, "mysql.connect"); // Create NativeObject wrapper first (so callbacks can dispatch) int64_t gid = s->GetNativeObjectManager().CreateGroup(); @@ -267,7 +269,7 @@ CVar ConnQuery(NativeObject *self, State *s, CVar *args, int n) { CVar a0 = inter::GetNativeArg(s, args, n, 0); CVar a1 = inter::GetNativeArg(s, args, n, 1); std::string sql = CVarToString(a0); - ResultCallback cb = CVarToCallback(s, a1, 2, "conn:query"); + ResultCallback cb = BuildCallback(s, a1, 2, args, 2, n, "conn:query"); auto *conn = UnwrapConnNative(self); if (!conn) error("conn:query: connection is closed"); @@ -290,7 +292,7 @@ CVar ConnStmtPrepare(NativeObject *self, State *s, CVar *args, int n) { CVar a0 = inter::GetNativeArg(s, args, n, 0); CVar a1 = inter::GetNativeArg(s, args, n, 1); std::string sql = CVarToString(a0); - ResultCallback cb = CVarToCallback(s, a1, 2, "conn:stmt_prepare"); + ResultCallback cb = BuildCallback(s, a1, 2, args, 2, n, "conn:stmt_prepare"); auto *conn = UnwrapConnNative(self); if (!conn || !conn->Connected()) error("conn:stmt_prepare: connection is closed"); @@ -313,7 +315,7 @@ CVar ConnStmtExecute(NativeObject *self, State *s, CVar *args, int n) { CVar a2 = inter::GetNativeArg(s, args, n, 2); uint32_t stmt_id = static_cast(inter::CVarToInteger(a0, 0)); - ResultCallback cb = CVarToCallback(s, a2, 3, "conn:stmt_execute"); + ResultCallback cb = BuildCallback(s, a2, 3, args, 3, n, "conn:stmt_execute"); std::vector params; if (a1.type_ == static_cast(VarType::Table) && a1.data_.t) { @@ -417,7 +419,8 @@ CVar ConnPing(NativeObject *self, State *s, CVar * /*args*/, int /*n*/) { void RegisterMysqlLibraryApi(State *s) { if (!s) return; - RegisterNativeFunction(s, "mysql.connect", 2, false, MysqlConnect); + // 可变参数:config、回调函数名之后可跟任意个纯数据绑定参数。 + RegisterNativeFunction(s, "mysql.connect", 2, true, MysqlConnect); } }// namespace fakelua::mysql diff --git a/src/native/serialize/native_serialize.cpp b/src/native/serialize/native_serialize.cpp index 50c3c03..9887acc 100644 --- a/src/native/serialize/native_serialize.cpp +++ b/src/native/serialize/native_serialize.cpp @@ -1,4 +1,5 @@ #include "native/serialize/native_serialize.h" +#include "native/serialize/wire_codec.h" #include "native/native_common.h" #include "native/table/native_table.h" #include "util/logging.h" @@ -15,45 +16,21 @@ #include #include #include -#include #include #include #include -#include -#include #include #include namespace fakelua::serialize { -// Wire format(类 protobuf 编码) -// 每个值 = [type_tag(1 byte)] [payload] -// 0x00 nil -// 0x01 false -// 0x02 true -// 0x03 + varint 整数(zigzag 编码:小绝对值 → 小编码) -// 0x04 + 8 bytes double(小端 memcpy) -// 0x05 + varint(len) + bytes 新字符串,加入字典 -// 0x06 + varint(id) 字典中的字符串引用 -// 0x07 + varint(count) + N*(key,value) 表 -// 不支持的类型(闭包等)在表中跳过,顶层编码则抛错。 +// 紧凑 wire 格式(nil/bool/int/float/string/table)的编解码在 wire_codec 中实现, +// Lua serialize.encode/decode 与内部异步回调绑定参数共用同一格式,这里只做 Lua 层封装。 -enum Tag : uint8_t { - TAG_NIL = 0x00, - TAG_FALSE = 0x01, - TAG_TRUE = 0x02, - TAG_INT = 0x03, - TAG_DOUBLE = 0x04, - TAG_STR_NEW = 0x05, - TAG_STR_REF = 0x06, - TAG_TABLE = 0x07, -}; - -// 辅助:从 CVar 提取字符串(二进制安全) +// 辅助:从 CVar 提取字符串(二进制安全),供 Boost.Serialization 树使用。 static std::string CVarToString(CVar v) { if (v.type_ == static_cast(VarType::String) && v.data_.s) { - auto sv = v.data_.s->Str(); - return std::string(sv.data(), sv.size()); + return std::string(v.data_.s->Str()); } if (v.type_ == static_cast(VarType::StringId) && v.data_.i) { const char *ptr = reinterpret_cast(v.data_.i); @@ -63,270 +40,21 @@ static std::string CVarToString(CVar v) { return {}; } -static std::string_view CVarToStringView(CVar v) { - if (v.type_ == static_cast(VarType::String) && v.data_.s) { - return v.data_.s->Str(); - } - if (v.type_ == static_cast(VarType::StringId) && v.data_.i) { - const char *ptr = reinterpret_cast(v.data_.i); - int sz = *reinterpret_cast(ptr); - return std::string_view(ptr + 8, sz); - } - return {}; -} - -// 类型判断 -static bool IsSupported(CVar v) { - switch (v.type_) { - case static_cast(VarType::Nil): - case static_cast(VarType::Bool): - case static_cast(VarType::Int): - case static_cast(VarType::Float): - case static_cast(VarType::String): - case static_cast(VarType::StringId): - case static_cast(VarType::Table): - return true; - default: - return false; - } -} - -// Varint(LEB128 无符号) -static void WriteVarint(std::string &out, uint64_t v) { - while (v >= 0x80) { - out.push_back(static_cast((v & 0x7f) | 0x80)); - v >>= 7; - } - out.push_back(static_cast(v)); -} - -static uint64_t ReadVarint(const std::string &in, size_t &pos) { - uint64_t result = 0; - int shift = 0; - bool terminated = false; - while (pos < in.size()) { - uint8_t b = static_cast(in[pos++]); - result |= static_cast(b & 0x7f) << shift; - if ((b & 0x80) == 0) { - terminated = true; - break; - } - shift += 7; - if (shift >= 64) { - ThrowFakeluaException("serialize.decode: varint too long"); - } - } - if (!terminated) { - ThrowFakeluaException("serialize.decode: truncated varint"); - } - return result; -} - -// Zigzag(有符号整数 ↔ 无符号) -static uint64_t ZigzagEncode(int64_t n) { - return (static_cast(n) << 1) ^ static_cast(n >> 63); -} - -static int64_t ZigzagDecode(uint64_t u) { - return static_cast((u >> 1) ^ (-(u & 1))); -} - -// Double(小端 memcpy) -static void WriteDouble(std::string &out, double v) { - uint8_t buf[8]; - std::memcpy(buf, &v, 8); - out.append(reinterpret_cast(buf), 8); -} - -static double ReadDouble(const std::string &in, size_t &pos) { - if (pos + 8 > in.size()) { - ThrowFakeluaException("serialize.decode: truncated double"); - } - double v; - std::memcpy(&v, in.data() + pos, 8); - pos += 8; - return v; -} - -// 编码 -struct EncodeState { - std::unordered_map dict;// 字符串 → 字典 id - std::unordered_set visited; - int depth = 0; -}; - -static void EncodeValue(std::string &out, CVar v, EncodeState &state) { - switch (v.type_) { - case static_cast(VarType::Nil): - out.push_back(TAG_NIL); - return; - case static_cast(VarType::Bool): - out.push_back(AsVar(v).GetBool() ? TAG_TRUE : TAG_FALSE); - return; - case static_cast(VarType::Int): - out.push_back(TAG_INT); - WriteVarint(out, ZigzagEncode(v.data_.i)); - return; - case static_cast(VarType::Float): - out.push_back(TAG_DOUBLE); - WriteDouble(out, v.data_.f); - return; - case static_cast(VarType::String): - case static_cast(VarType::StringId): { - auto sv = CVarToStringView(v); - auto it = state.dict.find(sv); - if (it != state.dict.end()) { - out.push_back(TAG_STR_REF); - WriteVarint(out, it->second); - } else { - uint32_t id = static_cast(state.dict.size()); - state.dict.emplace(sv, id); - out.push_back(TAG_STR_NEW); - WriteVarint(out, static_cast(sv.size())); - if (!sv.empty()) { - out.append(sv.data(), sv.size()); - } - } - return; - } - case static_cast(VarType::Table): { - VarTable *t = v.data_.t; - if (!t) { - out.push_back(TAG_TABLE); - WriteVarint(out, 0); - return; - } - if (state.depth >= 64) { - ThrowFakeluaException("serialize.encode: nesting too deep"); - } - if (!state.visited.insert(t).second) { - ThrowFakeluaException("serialize.encode: cyclic table"); - } - state.depth++; - std::vector keys; - std::vector vals; - try { - table::TableHelper::ForEachKV(v, [&](CVar k, CVar val) { - if (IsSupported(k) && IsSupported(val)) { - keys.push_back(k); - vals.push_back(val); - } - }); - out.push_back(TAG_TABLE); - WriteVarint(out, static_cast(keys.size())); - for (size_t i = 0; i < keys.size(); ++i) { - EncodeValue(out, keys[i], state); - EncodeValue(out, vals[i], state); - } - } catch (...) { - state.depth--; - state.visited.erase(t); - throw; - } - state.depth--; - state.visited.erase(t); - return; - } - default: - ThrowFakeluaException("serialize.encode: unsupported type: " + VarTypeToString(static_cast(v.type_))); - } -} - -// 解码 -struct DecodeState { - std::vector dict;// id → 字符串 - int depth = 0; -}; - -static CVar DecodeValue(const std::string &in, size_t &pos, DecodeState &state, State *s) { - if (pos >= in.size()) { - ThrowFakeluaException("serialize.decode: unexpected end of input"); - } - uint8_t tag = static_cast(in[pos++]); - switch (tag) { - case TAG_NIL: - return inter::NativeToFakeluaNil(s); - case TAG_FALSE: - return inter::NativeToFakeluaBool(s, false); - case TAG_TRUE: - return inter::NativeToFakeluaBool(s, true); - case TAG_INT: { - uint64_t u = ReadVarint(in, pos); - return inter::NativeToFakeluaLonglong(s, ZigzagDecode(u)); - } - case TAG_DOUBLE: - return inter::NativeToFakeluaDouble(s, ReadDouble(in, pos)); - case TAG_STR_NEW: { - uint64_t len = ReadVarint(in, pos); - if (len > in.size() - pos) { - ThrowFakeluaException("serialize.decode: truncated string"); - } - std::string str(in, pos, static_cast(len)); - pos += static_cast(len); - uint32_t id = static_cast(state.dict.size()); - state.dict.push_back(str); - return inter::NativeToFakeluaString(s, state.dict[id]); - } - case TAG_STR_REF: { - uint64_t id = ReadVarint(in, pos); - if (id >= state.dict.size()) { - ThrowFakeluaException("serialize.decode: bad string ref id"); - } - return inter::NativeToFakeluaString(s, state.dict[id]); - } - case TAG_TABLE: { - if (state.depth >= 64) { - ThrowFakeluaException("serialize.decode: nesting too deep"); - } - uint64_t count = ReadVarint(in, pos); - // Each key/value pair is at least two tags. - if (count > (in.size() - pos) / 2) { - ThrowFakeluaException("serialize.decode: table too large"); - } - CVar tbl = table::TableHelper::CreateTable(s); - state.depth++; - try { - for (uint64_t i = 0; i < count; ++i) { - CVar key = DecodeValue(in, pos, state, s); - CVar val = DecodeValue(in, pos, state, s); - table::TableHelper::SetTable(s, tbl, key, val); - } - } catch (...) { - state.depth--; - throw; - } - state.depth--; - return tbl; - } - default: - ThrowFakeluaException("serialize.decode: unknown tag: " + std::to_string(tag)); - } -} - // 原生函数 static CVar SerializeEncode(State *s, CVar *args, int n) { CVar v = inter::GetNativeArg(s, args, n, 0); - if (!IsSupported(v)) { + if (!WireIsSerializable(v)) { LOG_ERROR(s, "serialize", "serialize.encode: unsupported type: {}", VarTypeToString(static_cast(v.type_))); ThrowFakeluaException("serialize.encode: unsupported type: " + VarTypeToString(static_cast(v.type_))); } - std::string out; - out.reserve(64); - EncodeState state; - EncodeValue(out, v, state); + std::string out = WireEncode(v); LOG_DEBUG(s, "serialize", "serialize.encode: bytes={}", out.size()); return inter::NativeToFakeluaString(s, out); } static CVar SerializeDecode(State *s, CVar *args, int n) { std::string in = CVarToString(inter::GetNativeArg(s, args, n, 0)); - size_t pos = 0; - DecodeState state; - CVar result = DecodeValue(in, pos, state, s); - if (pos != in.size()) { - LOG_ERROR(s, "serialize", "serialize.decode: trailing bytes (read={} total={})", pos, in.size()); - ThrowFakeluaException("serialize.decode: trailing bytes"); - } + CVar result = WireDecode(s, in); LOG_DEBUG(s, "serialize", "serialize.decode: bytes={}", in.size()); return result; } @@ -392,7 +120,7 @@ static SerNode CVarToSerNode(CVar v, std::unordered_set &visited, in n.kind = 5; if (t) { table::TableHelper::ForEachKV(v, [&](CVar k, CVar val) { - if (IsSupported(k) && IsSupported(val)) { + if (WireIsSerializable(k) && WireIsSerializable(val)) { n.keys.push_back(CVarToSerNode(k, visited, depth + 1, op)); n.vals.push_back(CVarToSerNode(val, visited, depth + 1, op)); } @@ -436,7 +164,7 @@ static CVar SerNodeToCVar(const SerNode &n, State *s, int depth) { static SerNode RequireSerNode(State *s, CVar *args, int n, const char *op) { CVar v = inter::GetNativeArg(s, args, n, 0); - if (!IsSupported(v)) { + if (!WireIsSerializable(v)) { ThrowFakeluaException(std::string(op) + ": unsupported type: " + VarTypeToString(static_cast(v.type_))); } std::unordered_set visited; diff --git a/src/native/serialize/wire_codec.cpp b/src/native/serialize/wire_codec.cpp new file mode 100644 index 0000000..6a8d36c --- /dev/null +++ b/src/native/serialize/wire_codec.cpp @@ -0,0 +1,381 @@ +#include "native/serialize/wire_codec.h" + +#include "native/native_common.h" +#include "native/table/native_table.h" +#include "var/var.h" +#include "var/var_string.h" +#include "var/var_table.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fakelua::serialize { + +// Wire format(类 protobuf 编码)——与 native_serialize.cpp 文档保持一致: +// 每个值 = [type_tag(1 byte)] [payload] +// 0x00 nil +// 0x01 false +// 0x02 true +// 0x03 + varint 整数(zigzag 编码:小绝对值 → 小编码) +// 0x04 + 8 bytes double(小端 memcpy) +// 0x05 + varint(len) + bytes 新字符串,加入字典 +// 0x06 + varint(id) 字典中的字符串引用 +// 0x07 + varint(count) + N*(key,value) 表 + +namespace { + +enum Tag : uint8_t { + TAG_NIL = 0x00, + TAG_FALSE = 0x01, + TAG_TRUE = 0x02, + TAG_INT = 0x03, + TAG_DOUBLE = 0x04, + TAG_STR_NEW = 0x05, + TAG_STR_REF = 0x06, + TAG_TABLE = 0x07, +}; + +constexpr int kWireMaxDepth = 64; + +// 辅助:从 CVar 提取字符串(二进制安全) +std::string CVarToString(CVar v) { + if (v.type_ == static_cast(VarType::String) && v.data_.s) { + auto sv = v.data_.s->Str(); + return std::string(sv.data(), sv.size()); + } + if (v.type_ == static_cast(VarType::StringId) && v.data_.i) { + const char *ptr = reinterpret_cast(v.data_.i); + int sz = *reinterpret_cast(ptr); + return std::string(ptr + 8, sz); + } + return {}; +} + +std::string_view CVarToStringView(CVar v) { + if (v.type_ == static_cast(VarType::String) && v.data_.s) { + return v.data_.s->Str(); + } + if (v.type_ == static_cast(VarType::StringId) && v.data_.i) { + const char *ptr = reinterpret_cast(v.data_.i); + int sz = *reinterpret_cast(ptr); + return std::string_view(ptr + 8, sz); + } + return {}; +} + +bool IsSupported(CVar v) { + switch (v.type_) { + case static_cast(VarType::Nil): + case static_cast(VarType::Bool): + case static_cast(VarType::Int): + case static_cast(VarType::Float): + case static_cast(VarType::String): + case static_cast(VarType::StringId): + case static_cast(VarType::Table): + return true; + default: + return false; + } +} + +// Varint(LEB128 无符号) +void WriteVarint(std::string &out, uint64_t v) { + while (v >= 0x80) { + out.push_back(static_cast((v & 0x7f) | 0x80)); + v >>= 7; + } + out.push_back(static_cast(v)); +} + +uint64_t ReadVarint(std::string_view in, size_t &pos) { + uint64_t result = 0; + int shift = 0; + bool terminated = false; + while (pos < in.size()) { + uint8_t b = static_cast(in[pos++]); + result |= static_cast(b & 0x7f) << shift; + if ((b & 0x80) == 0) { + terminated = true; + break; + } + shift += 7; + if (shift >= 64) { + ThrowFakeluaException("wire.decode: varint too long"); + } + } + if (!terminated) { + ThrowFakeluaException("wire.decode: truncated varint"); + } + return result; +} + +// Zigzag(有符号整数 ↔ 无符号) +uint64_t ZigzagEncode(int64_t n) { + return (static_cast(n) << 1) ^ static_cast(n >> 63); +} + +int64_t ZigzagDecode(uint64_t u) { + return static_cast((u >> 1) ^ (-(u & 1))); +} + +void WriteDouble(std::string &out, double v) { + uint8_t buf[8]; + std::memcpy(buf, &v, 8); + out.append(reinterpret_cast(buf), 8); +} + +double ReadDouble(std::string_view in, size_t &pos) { + if (pos + 8 > in.size()) { + ThrowFakeluaException("wire.decode: truncated double"); + } + double v; + std::memcpy(&v, in.data() + pos, 8); + pos += 8; + return v; +} + +// 编码 +struct EncodeState { + std::unordered_map dict;// 字符串 → 字典 id + std::unordered_set visited; + int depth = 0; +}; + +void EncodeValue(std::string &out, CVar v, EncodeState &state) { + switch (v.type_) { + case static_cast(VarType::Nil): + out.push_back(TAG_NIL); + return; + case static_cast(VarType::Bool): + out.push_back(AsVar(v).GetBool() ? TAG_TRUE : TAG_FALSE); + return; + case static_cast(VarType::Int): + out.push_back(TAG_INT); + WriteVarint(out, ZigzagEncode(v.data_.i)); + return; + case static_cast(VarType::Float): + out.push_back(TAG_DOUBLE); + WriteDouble(out, v.data_.f); + return; + case static_cast(VarType::String): + case static_cast(VarType::StringId): { + auto sv = CVarToStringView(v); + auto it = state.dict.find(sv); + if (it != state.dict.end()) { + out.push_back(TAG_STR_REF); + WriteVarint(out, it->second); + } else { + uint32_t id = static_cast(state.dict.size()); + state.dict.emplace(sv, id); + out.push_back(TAG_STR_NEW); + WriteVarint(out, static_cast(sv.size())); + if (!sv.empty()) { + out.append(sv.data(), sv.size()); + } + } + return; + } + case static_cast(VarType::Table): { + VarTable *t = v.data_.t; + if (!t) { + out.push_back(TAG_TABLE); + WriteVarint(out, 0); + return; + } + if (state.depth >= kWireMaxDepth) { + ThrowFakeluaException("wire.encode: nesting too deep"); + } + if (!state.visited.insert(t).second) { + ThrowFakeluaException("wire.encode: cyclic table"); + } + state.depth++; + std::vector keys; + std::vector vals; + try { + table::TableHelper::ForEachKV(v, [&](CVar k, CVar val) { + if (IsSupported(k) && IsSupported(val)) { + keys.push_back(k); + vals.push_back(val); + } + }); + out.push_back(TAG_TABLE); + WriteVarint(out, static_cast(keys.size())); + for (size_t i = 0; i < keys.size(); ++i) { + EncodeValue(out, keys[i], state); + EncodeValue(out, vals[i], state); + } + } catch (...) { + state.depth--; + state.visited.erase(t); + throw; + } + state.depth--; + state.visited.erase(t); + return; + } + default: + ThrowFakeluaException("wire.encode: unsupported type: " + VarTypeToString(static_cast(v.type_))); + } +} + +// 解码 +struct DecodeState { + std::vector dict;// id → 字符串 + int depth = 0; +}; + +CVar DecodeValue(std::string_view in, size_t &pos, DecodeState &state, State *s) { + if (pos >= in.size()) { + ThrowFakeluaException("wire.decode: unexpected end of input"); + } + uint8_t tag = static_cast(in[pos++]); + switch (tag) { + case TAG_NIL: + return inter::NativeToFakeluaNil(s); + case TAG_FALSE: + return inter::NativeToFakeluaBool(s, false); + case TAG_TRUE: + return inter::NativeToFakeluaBool(s, true); + case TAG_INT: { + uint64_t u = ReadVarint(in, pos); + return inter::NativeToFakeluaLonglong(s, ZigzagDecode(u)); + } + case TAG_DOUBLE: + return inter::NativeToFakeluaDouble(s, ReadDouble(in, pos)); + case TAG_STR_NEW: { + uint64_t len = ReadVarint(in, pos); + if (len > in.size() - pos) { + ThrowFakeluaException("wire.decode: truncated string"); + } + std::string str(in.substr(pos, static_cast(len))); + pos += static_cast(len); + uint32_t id = static_cast(state.dict.size()); + state.dict.push_back(std::move(str)); + return inter::NativeToFakeluaString(s, state.dict[id]); + } + case TAG_STR_REF: { + uint64_t id = ReadVarint(in, pos); + if (id >= state.dict.size()) { + ThrowFakeluaException("wire.decode: bad string ref id"); + } + return inter::NativeToFakeluaString(s, state.dict[id]); + } + case TAG_TABLE: { + if (state.depth >= kWireMaxDepth) { + ThrowFakeluaException("wire.decode: nesting too deep"); + } + uint64_t count = ReadVarint(in, pos); + // Each key/value pair is at least two tags. + if (count > (in.size() - pos) / 2) { + ThrowFakeluaException("wire.decode: table too large"); + } + CVar tbl = table::TableHelper::CreateTable(s); + state.depth++; + try { + for (uint64_t i = 0; i < count; ++i) { + CVar key = DecodeValue(in, pos, state, s); + CVar val = DecodeValue(in, pos, state, s); + table::TableHelper::SetTable(s, tbl, key, val); + } + } catch (...) { + state.depth--; + throw; + } + state.depth--; + return tbl; + } + default: + ThrowFakeluaException("wire.decode: unknown tag: " + std::to_string(tag)); + } +} + +// 严格校验:回调绑定参数不允许静默丢字段。任意位置出现不可序列化类型都响亮报错。 +void ValidateStrict(CVar v, int depth, std::unordered_set &visited, const char *fname, int argno) { + if (!IsSupported(v)) { + ThrowBadArgument(argno, fname, + "callback args must be nil, boolean, number, string or a table thereof (closures and native objects are not allowed)"); + } + if (v.type_ != static_cast(VarType::Table) || !v.data_.t) return; + VarTable *t = v.data_.t; + if (depth >= kWireMaxDepth) { + ThrowFakeluaException("callback args: nesting too deep"); + } + if (!visited.insert(t).second) { + ThrowFakeluaException("callback args: cyclic table"); + } + table::TableHelper::ForEachKV(v, [&](CVar k, CVar val) { + ValidateStrict(k, depth + 1, visited, fname, argno); + ValidateStrict(val, depth + 1, visited, fname, argno); + }); + visited.erase(t); +} + +}// namespace + +bool WireIsSerializable(CVar v) { + return IsSupported(v); +} + +std::string WireEncode(CVar v) { + std::string out; + out.reserve(64); + EncodeState state; + EncodeValue(out, v, state); + return out; +} + +CVar WireDecode(State *s, std::string_view in) { + size_t pos = 0; + DecodeState state; + CVar result = DecodeValue(in, pos, state, s); + if (pos != in.size()) { + ThrowFakeluaException("wire.decode: trailing bytes"); + } + return result; +} + +std::string WireEncodeCallbackArgs(State * /*s*/, CVar *args, int first, int n, const char *fname, int argno_base) { + std::string out; + out.reserve(32); + const int count = n > first ? n - first : 0; + WriteVarint(out, static_cast(count)); + if (count == 0) return out; + + // 先整体严格校验,再编码:不允许走到编码器的"跳过不支持字段"静默分支。 + std::unordered_set visited; + for (int i = 0; i < count; ++i) { + ValidateStrict(args[first + i], 0, visited, fname, argno_base + i); + } + + EncodeState state; + for (int i = 0; i < count; ++i) { + EncodeValue(out, args[first + i], state); + } + return out; +} + +DecodedArgs WireDecodeCallbackArgs(State *s, std::string_view blob) { + DecodedArgs out; + if (blob.empty()) return out; + size_t pos = 0; + uint64_t count = ReadVarint(blob, pos); + out.vars.reserve(static_cast(count)); + DecodeState state; + for (uint64_t i = 0; i < count; ++i) { + out.vars.push_back(DecodeValue(blob, pos, state, s)); + } + if (pos != blob.size()) { + ThrowFakeluaException("wire.decode: trailing bytes in callback args"); + } + // 解码出的字符串可能引用 state.dict 里 std::string 的内容,把后备内存随 DecodedArgs + // 一起带走,调用方持有 DecodedArgs 到回调返回即可保证有效。 + out.str_dict = std::move(state.dict); + return out; +} + +}// namespace fakelua::serialize diff --git a/src/native/serialize/wire_codec.h b/src/native/serialize/wire_codec.h new file mode 100644 index 0000000..c9a690e --- /dev/null +++ b/src/native/serialize/wire_codec.h @@ -0,0 +1,46 @@ +#pragma once + +// wire_codec.h — CVar 紧凑 wire 编解码(内部模块,非 Lua API)。 +// +// 单值格式与 Lua serialize.encode/decode 完全一致(见 native_serialize.cpp), +// 供两类使用者复用: +// 1. serialize 原生库(Lua API 层); +// 2. 异步原生回调(mysql):把"函数名 + 绑定参数"中的数据参数序列化成自有的 +// 字节串暂存,跨多次顶层 Call / State::Reset() 保存,结果回来时在派发帧内 +// decode 回临时 arena。字节串是普通堆内存,回调结束即释放,不占用 const arena。 + +#include "fakelua.h" + +#include +#include +#include + +namespace fakelua::serialize { + +// 值是否可被 wire 编码:nil/bool/int/float/string/stringid/table,其余(闭包/native 对象等)不行。 +bool WireIsSerializable(CVar v); + +// 单个 CVar → wire 字节。表内遇到不可序列化字段会跳过(与 serialize.encode 历史行为一致); +// 顶层值不可序列化时抛错。 +std::string WireEncode(CVar v); + +// wire 字节 → 单个 CVar(分配在 State 临时 arena 上,仅当前帧有效)。 +// 字节必须被完整消费,尾随字节抛错。 +CVar WireDecode(State *s, std::string_view in); + +// 回调绑定参数元组:[varint count][value]... +// 与 WireEncode 不同,这里对参数做【严格】校验:闭包/native 对象等不可序列化类型 +// 出现在任意嵌套位置(含表的键值)都抛 bad argument,不允许静默丢字段。 +// fname/argno_base 用于错误信息(bad argument #argno_base+i to 'fname')。 +std::string WireEncodeCallbackArgs(State *s, CVar *args, int first, int n, const char *fname, int argno_base); + +// 绑定参数元组解码。产物在临时 arena 上;DecodedArgs 同时持有字符串字典等后备内存, +// 调用方必须让它活到回调函数返回之后。 +struct DecodedArgs { + std::vector vars; + std::vector str_dict; +}; + +DecodedArgs WireDecodeCallbackArgs(State *s, std::string_view blob); + +}// namespace fakelua::serialize diff --git a/test/lua/mysql/test_mysql_contract.lua b/test/lua/mysql/test_mysql_contract.lua index c884322..dd8e165 100644 --- a/test/lua/mysql/test_mysql_contract.lua +++ b/test/lua/mysql/test_mysql_contract.lua @@ -1,63 +1,88 @@ package "MysqlContractTest" -- MySQL 回调契约测试(无需真实 MySQL 服务器:连到死端口验证回调行为) +-- 异步回调模型:函数名 + 绑定参数(纯数据),不支持内联闭包: +-- conn:query(sql, "on_result", arg1, arg2, ...) +-- 绑定参数在登记当下被序列化成独立字节串暂存,跨任意次顶层 Call 的 arena Reset +-- 都不丢;结果派发时在 tick 帧内反序列化,追加在固定参数 (conn, err, result) 之后。 +-- 回调结束字节串即释放,不占用 const arena。 +-- 跨帧可变状态挂在 conn 对象字段上(连接是 C++ 持有的 native 对象,Reset 不回收)。 -- 回归: --- P0-1 内联闭包回调曾被静默丢弃(CVarToString 转成空串,GetFunction("") 查不到) +-- P0-1 非法回调(闭包/数字/空串)曾被静默转成空名、回调永不触发,现在必须响亮报错 -- P0-2 连接未就绪时 query 的回调曾被静默吞掉(Query 提前 return 不设 pending_result_) -- P1-4 同连接飞行中再 query 曾直接报 "connection not ready";现在排队,回调恰好一次 --- 场景 1:连接未就绪时用闭包发起 query —— 连接失败后回调恰好收到一次错误 -function test_query_closure_exactly_once() - local state = { connect_calls = 0, query_calls = 0, query_err = nil } +-- 死端口配置:连接必然失败,正好驱动错误终态路径 +local dead_cfg = { + host = "127.0.0.1", + port = 1, + user = "root", + password = "x", + db = "test", + timeout_ms = 500 +} + +-- 观测状态全部挂在 conn 字段上;绑定参数用 tag 区分是哪一次登记。 +function contract_on_connect(c, err, success, tag) + if tag == "connect_tag" then + c.cc = (c.cc or 0) + 1 + end +end - local conn = mysql.connect({ - host = "127.0.0.1", - port = 1, - user = "root", - password = "x", - db = "test", - timeout_ms = 500 - }, function(c, err, success) - state.connect_calls = state.connect_calls + 1 - end) +function contract_on_query(c, err, result, tag, n, ctx) + if tag == "query_tag" then + c.qc = (c.qc or 0) + 1 + c.qerr = err + c.qtag = tag + c.qn = n + c.qctx = ctx + end +end + +-- 场景 1:连接未就绪时发起 query —— 连接失败后回调恰好收到一次错误; +-- 绑定的字符串/数字/表参数跨帧存活并按快照原样送达。 +function test_query_closure_exactly_once() + local conn = mysql.connect(dead_cfg, "contract_on_connect", "connect_tag") -- 连接尚未就绪:query 排队,连接进入错误终态后回调收到错误 - conn:query("SELECT 1", function(c, err, result) - state.query_calls = state.query_calls + 1 - state.query_err = err - end) + conn:query("SELECT 1", "contract_on_query", "query_tag", 7, { k = "v" }) for i = 1, 2000 do runtime.tick() - if state.query_calls > 0 then break end + if conn.qc then break end os.sleep(1) end - if state.connect_calls ~= 1 then - print("connect callback count = ", state.connect_calls) + if conn.cc ~= 1 then + print("connect callback count = ", conn.cc) + return 0 + end + if conn.qc ~= 1 then + print("query callback count = ", conn.qc) + return 0 + end + if type(conn.qerr) ~= "string" or #conn.qerr == 0 then + print("query callback missing error, got:", conn.qerr) return 0 end - if state.query_calls ~= 1 then - print("query callback count = ", state.query_calls) + if conn.qtag ~= "query_tag" then + print("bound string lost, got:", conn.qtag) return 0 end - if type(state.query_err) ~= "string" or #state.query_err == 0 then - print("query callback missing error, got:", state.query_err) + if conn.qn ~= 7 then + print("bound number lost, got:", conn.qn) + return 0 + end + if type(conn.qctx) ~= "table" or conn.qctx.k ~= "v" then + print("bound table lost, got:", type(conn.qctx)) return 0 end return 1 end --- 场景 2:回调参数传非函数/非字符串名 —— 必须响亮报错(曾静默丢弃) +-- 场景 2:回调名传非字符串(含内联闭包)、绑定参数含闭包 —— 都必须响亮报错 function test_bad_callback_type() - local conn = mysql.connect({ - host = "127.0.0.1", - port = 1, - user = "root", - password = "x", - db = "test", - timeout_ms = 500 - }, function(c, err, success) end) + local conn = mysql.connect(dead_cfg, "contract_on_connect", "connect_tag") local ok1 = pcall(function() conn:query("SELECT 1", 123) end) if ok1 then return 0 end @@ -65,12 +90,22 @@ function test_bad_callback_type() if ok2 then return 0 end local ok3 = pcall(function() conn:query("SELECT 1", {}) end) if ok3 then return 0 end + -- 内联闭包不再支持:异步回调无法安全持有临时 arena 上的闭包 + local ok4 = pcall(function() conn:query("SELECT 1", function() end) end) + if ok4 then return 0 end + -- 回调名合法,但绑定参数里夹带闭包:同样响亮报错,不能静默丢字段 + local ok5 = pcall(function() conn:query("SELECT 1", "contract_on_query", "x", function() end) end) + if ok5 then return 0 end + -- 连接回调同理 + local ok6 = pcall(function() mysql.connect(dead_cfg, function() end) end) + if ok6 then return 0 end conn:close() return 1 end --- 场景 3:pool:with —— 无健康连接时返回 nil(fn 不执行);非函数参数报错 +-- 场景 3:pool:with —— 无健康连接时返回 nil(fn 不执行);非函数参数报错。 +-- pool:with 的函数是【同帧同步调用】、不跨 tick 暂存,因此仍支持内联闭包。 function test_pool_with() local pool = mysql_pool.create({ host = "127.0.0.1", @@ -103,29 +138,27 @@ function make_thrower() end end --- 跨帧闭包(P0-1):闭包和它捕获的表必须活过下一次顶层 Call 的 arena Reset。 --- arm 在一次 Call 里登记回调后返回;pump 是下一次 Call(开头会 Reset 临时 arena), --- 再 tick 到回调。若裸指针悬空,读 ctx.n 会坏掉或回调根本跑不起来。 +-- 跨帧绑定参数(P0-1 的新形态):绑定参数序列化后必须活过下一次顶层 Call 的 +-- arena Reset。arm 在一次 Call 里登记回调后返回;宿主显式 Reset 临时 arena; +-- pump 是下一次 Call,再 tick 到回调。若字节串暂存失效,ctx 字段会坏掉或回调跑不起来。 +-- 本用例由 C++ 单引擎(TCC)驱动:具名回调固定派发到 TCC 编译产物,文件级 local +-- 是各引擎动态库各自的 static,不能像其他用例那样用 CallAll 三引擎混跑。 +-- cross_hits 只承载动态值(表字段 / nil),不会被推断成文件级数值常量。 local cross_hits = nil +function contract_on_cross(c, err, result, ctx) + if ctx.tag == "ok" and ctx.n == 7 and type(err) == "string" then + cross_hits = ctx.n + else + cross_hits = -1 + end +end + function arm_cross_frame() - local ctx = { n = 7, tag = "ok" } - local conn = mysql.connect({ - host = "127.0.0.1", - port = 1, - user = "root", - password = "x", - db = "test", - timeout_ms = 500 - }, function(c, err, success) end) - - conn:query("SELECT 1", function(c, err, result) - if ctx.tag == "ok" and ctx.n == 7 and type(err) == "string" then - cross_hits = ctx.n - else - cross_hits = -1 - end - end) + cross_hits = nil + local conn = mysql.connect(dead_cfg, "contract_on_connect", "connect_tag") + + conn:query("SELECT 1", "contract_on_cross", { tag = "ok", n = 7 }) return 1 end diff --git a/test/test_mysql.cpp b/test/test_mysql.cpp index dea187c..33d2405 100644 --- a/test/test_mysql.cpp +++ b/test/test_mysql.cpp @@ -245,7 +245,11 @@ TEST(test_mysql, pool_with_fn_throw_returns_connection) { FakeluaDeleteState(s); } -// 回归(P0-1):闭包回调跨过下一次顶层 Call 的临时 arena Reset 后仍然能读到捕获的表。 +// 回归(P0-1):异步回调的绑定参数(序列化字节串)跨过下一次顶层 Call 的临时 +// arena Reset 后仍然能被反序列化并原样送达回调。 +// 只在 TCC 单引擎下驱动:具名回调派发固定解析到 TCC 编译产物,而文件级 local 是各 +// 引擎动态库各自的 static;CallAll 混跑时 GCC/解释器会读到自己那份未写入的变量。 +// 序列化字节串本身是引擎无关的 C++ 逻辑,单引擎走通即完整覆盖跨 Reset 存活语义。 TEST(test_mysql, closure_callback_survives_frame_reset) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); @@ -253,13 +257,13 @@ TEST(test_mysql, closure_callback_survives_frame_reset) { CompileFile(s, "./mysql/test_mysql_contract.lua", config); int64_t armed = 0; - CallAll(s, "MysqlContractTest.arm_cross_frame", armed); + Call(s, JIT_TCC, "MysqlContractTest.arm_cross_frame", armed); EXPECT_EQ(armed, 1); // 顶层 Call 返回后显式再 Reset 一次,模拟宿主在帧末回收临时 arena。 inter::Reset(s); int64_t hits = 0; - CallAll(s, "MysqlContractTest.pump_cross_frame", hits); + Call(s, JIT_TCC, "MysqlContractTest.pump_cross_frame", hits); EXPECT_EQ(hits, 7); FakeluaDeleteState(s); } From 300bcf7b9567428846fa1b3892bdc88d545596e3 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 1 Oct 2026 21:10:50 +0800 Subject: [PATCH 6/8] fix: dispatch async callbacks in the engine that registered them, not pinned TCC Named mysql callbacks were always resolved through the TCC artifact, so a callback registered from GCC/interpreter code executed TCC machine code. With CallAll exercising all engines on one State, per-engine file-level statics written by callbacks were not visible to the registering engine. Track the currently executing engine on the State and propagate it through every Lua->C++ invocation boundary: - State holds current_jit_ with a C++ JitContextScope RAII and C-compatible getter/setter (TCC compiles generated code as C, cannot use C++ scopes) - CallByNameImpl (native function dispatch), DispatchCall (Lua function / closure dispatch from C++) and the FlCallClosure case ladder (direct closure calls from JIT C code, incl. the native object method bridge) push/restore the caller engine via FakeluaJitContextPush/Pop - ResultCallback records the engine at registration; InvokeCallback resolves and calls the function in that same engine (fallback to other engines only if its artifact is missing) - pool:with invokes its synchronous fn closure with the current engine This restores the same per-engine closure semantics as inline closures; the cross-frame contract test runs under CallAll again. All 1551 tests pass (excluding local-redis test_redis). --- src/compile/c_runtime_header.h | 13 ++++++++-- src/jit/vm.cpp | 15 ++++++++++++ src/native/README.md | 2 ++ src/native/README.zh.md | 1 + src/native/mysql/mysql_connection.cpp | 19 +++++++++++---- src/native/mysql/mysql_connection.h | 4 ++++ src/native/mysql/native_mysql.cpp | 3 ++- src/native/mysql/native_mysql_pool.cpp | 6 ++--- src/state/fakelua.cpp | 3 +++ src/state/state.h | 33 ++++++++++++++++++++++++++ test/lua/mysql/test_mysql_contract.lua | 3 +-- test/test_mysql.cpp | 10 ++++---- 12 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index eb7aa5f..a10c918 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -339,6 +339,10 @@ extern CVar FakeluaAllocMultiCVar(State *s, int count); extern void FakeluaSetMultiCVarElement(CVar *multi, int idx, CVar val); extern CVar FlEvalLoadClosure(State *state, VarClosure *cl, int arg_num, const CVar *args); extern CVar FakeluaInterpCall(State *state, VarClosure *cl, int arg_num, const CVar *args); +// 当前执行引擎上下文(供 JIT C 代码调用闭包前后维护):push 切到 jit 并返回上一个引擎, +// pop 恢复。native 对象方法等在闭包内执行时据此登记派回本引擎的异步回调。 +extern int FakeluaJitContextPush(State *s, int jit); +extern void FakeluaJitContextPop(State *s, int prev); #ifdef __cplusplus } #endif @@ -523,8 +527,13 @@ static inline CVar FlCallClosure(State *state, CVar cl_var, int arg_num, ...) { #define FCARG_31 FCARG_30, arg_arr[30] #define FCARG_32 FCARG_31, arg_arr[31] -#define FCCASE(N) \ - case N: return ((CVar (*)(VarClosure * FCCVAR_##N))(addr))(cl FCARG_##N); +#define FCCASE(N) \ + case N: { \ + int __fl_prev_jit = FakeluaJitContextPush(state, FAKELUA_JIT_TYPE); \ + CVar __fl_ret = ((CVar (*)(VarClosure * FCCVAR_##N))(addr))(cl FCARG_##N); \ + FakeluaJitContextPop(state, __fl_prev_jit); \ + return __fl_ret; \ + } switch (expected_arg_count) { FCCASE(0) FCCASE(1) FCCASE(2) FCCASE(3) FCCASE(4) FCCASE(5) diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index 781e824..a621781 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -40,6 +40,18 @@ extern "C" __attribute__((used)) void FakeluaSetMultiCVarElement(CVar *multi, in inter::SetMultiCVarElement(*multi, idx, val); } +// JIT 生成的 C 代码(TCC 是 C 编译器,用不了 C++ 的 State::JitContextScope)调用闭包 +// 前后经这对接口维护"当前执行引擎"。返回旧值供 Pop 恢复,单线程无需原子操作。 +extern "C" int FakeluaJitContextPush(State *s, int jit) { + const int prev = static_cast(s->CurrentJit()); + s->SetCurrentJit(static_cast(jit)); + return prev; +} + +extern "C" void FakeluaJitContextPop(State *s, int prev) { + s->SetCurrentJit(static_cast(prev)); +} + 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, ...) { @@ -70,6 +82,9 @@ extern "C" __attribute__((used)) CVar FakeluaCallByName(State *state, int jit_ty } static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg_num, const CVar *raw_arg_arr) { + // 记录"当前 Lua 调用方引擎":native 函数内登记的异步回调据此派回同一引擎。 + // 作用域覆盖整个函数(含嵌套的 Lua 回调),退出时恢复外层引擎。 + State::JitContextScope jit_scope(state, static_cast(jit_type)); // 查找函数:优先 JIT,其次 C++ 原生 // 用 string_view 查表,避免每次调用都堆分配 std::string const std::string_view func_name(name); diff --git a/src/native/README.md b/src/native/README.md index e72296b..4fcc883 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -606,6 +606,8 @@ used. - Bound arguments are a **registration-time snapshot** (copy by value): mutating the original table after registration is not visible to the callback. Put mutable cross-frame state on the connection object itself (e.g. `conn.query_done`). +- The callback runs in the **same engine that issued the call** (TCC/GCC/interpreter each close + the loop themselves); dispatch is not pinned to TCC. - `pool:with(fn)` invokes fn **synchronously in the same frame** and never stores it across ticks, so inline closures remain supported there. diff --git a/src/native/README.zh.md b/src/native/README.zh.md index 3432b03..f288c8b 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -653,6 +653,7 @@ PCG-32 算法:64-bit 状态,32-bit 输出,周期 2^64。每个 `random.new 不可序列化类型出现在任意嵌套位置都会直接报 "bad argument",不会静默丢字段。 - 绑定参数是**登记时刻的值快照**(按值复制),登记之后调用方再改原表,回调看不到; 需要跨帧共享的可变状态请挂在连接对象字段上(如 `conn.query_done`)。 +- 回调在**发起调用的同一引擎**里执行(TCC/GCC/解释器各自闭环),不固定派发到 TCC。 - `pool:with(fn)` 的 fn 是**同帧同步调用**、不跨 tick 暂存,因此仍然支持内联闭包。 **回调契约:** `conn:query` 一旦被调用,其回调**恰好被调用一次**——连接未就绪(握手中/重连中/ diff --git a/src/native/mysql/mysql_connection.cpp b/src/native/mysql/mysql_connection.cpp index 4306cfe..0357005 100644 --- a/src/native/mysql/mysql_connection.cpp +++ b/src/native/mysql/mysql_connection.cpp @@ -789,14 +789,23 @@ void MysqlConnection::SetError(MysqlErrorType type, uint16_t code, const std::st } void MysqlConnection::InvokeCallback(const ResultCallback &cb, CVar *args, int n) { - // 全局函数名:查 VM 注册表。 + // 派回【登记回调时的引擎】(与旧内联闭包自带 func_ptr 的语义一致)。 auto func = lua_state_->GetVM().GetFunction(cb.name); if (func.Empty()) return; - void *addr = func.GetAddr(JIT_TCC); - JITType jit_type = JIT_TCC; + JITType jit_type = cb.jit; + void *addr = func.GetAddr(jit_type); if (!addr) { - addr = func.GetAddr(JIT_GCC); - jit_type = JIT_GCC; + // 兜底:登记引擎没有该函数的编译产物(正常不应发生——同一份脚本三引擎都会编译)。 + // 按其余引擎依次找,避免回调静默丢失。 + static constexpr JITType kFallbacks[] = {JIT_TCC, JIT_GCC, JIT_INTERP}; + for (JITType t: kFallbacks) { + if (t == jit_type) continue; + addr = func.GetAddr(t); + if (addr) { + jit_type = t; + break; + } + } } if (!addr) return; inter::DispatchCall(lua_state_, addr, args, n, jit_type); diff --git a/src/native/mysql/mysql_connection.h b/src/native/mysql/mysql_connection.h index da8d2e5..9f2e526 100644 --- a/src/native/mysql/mysql_connection.h +++ b/src/native/mysql/mysql_connection.h @@ -3,6 +3,7 @@ // mysql_connection.h — async MySQL client using libmysqlclient (MariaDB Connector/C) // MYSQL_OPT_NONBLOCK + libevent wait on mysql_get_socket(). +#include "fakelua.h" #include "native/native_io_context.h" #ifdef __cplusplus @@ -51,6 +52,9 @@ struct StmtParam { struct ResultCallback { std::string name; std::string bound;// wire 编码的绑定参数元组;空串表示无绑定参数 + // 登记回调时所在的脚本引擎。结果回来后派回【同一引擎】:在哪个引擎发起的 IO, + // 回调就执行哪个引擎的编译产物,与内联闭包自带 func_ptr 的语义一致。 + JITType jit = JIT_TCC; [[nodiscard]] bool Empty() const { return name.empty(); } }; diff --git a/src/native/mysql/native_mysql.cpp b/src/native/mysql/native_mysql.cpp index ea99aea..92e93f2 100644 --- a/src/native/mysql/native_mysql.cpp +++ b/src/native/mysql/native_mysql.cpp @@ -55,7 +55,8 @@ static ResultCallback BuildCallback(State *s, CVar name_v, int name_argno, CVar } // 绑定参数严格校验 + 序列化(含闭包/native 对象等直接抛 bad argument)。 std::string bound = serialize::WireEncodeCallbackArgs(s, args, bound_first, n, fname, bound_first + 1); - return ResultCallback{std::move(name), std::move(bound)}; + // 记住登记时的引擎:回调派发时派回同一引擎,而不是固定 TCC。 + return ResultCallback{std::move(name), std::move(bound), s->CurrentJit()}; } // Retrieve MysqlConnection* from NativeObject diff --git a/src/native/mysql/native_mysql_pool.cpp b/src/native/mysql/native_mysql_pool.cpp index 30f3432..e59f6b6 100644 --- a/src/native/mysql/native_mysql_pool.cpp +++ b/src/native/mysql/native_mysql_pool.cpp @@ -271,9 +271,9 @@ static CVar PoolWith(NativeObject *self, State *s, CVar *args, int n) { NativeObject *nat; ~LeaseGuard() { DetachAcquiredWrapper(nat); } } lease{nat}; - // kNativeCallbackJit 只选择异常边界。fn 的地址在闭包的 func_ptr 里, - // GCC / 解释器闭包同样会走到自己的代码,不会因为标记是 TCC 而静默失败。 - return inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, kNativeCallbackJit); + // fn 是同帧同步调用,直接沿用当前 Lua 调用方的引擎(异常边界随之正确), + // 不固定 TCC 标记。 + return inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, s->CurrentJit()); } int TestPoolWithFnThrowReturnsConnection(State *s, CVar fn) { diff --git a/src/state/fakelua.cpp b/src/state/fakelua.cpp index 4584725..83ea662 100644 --- a/src/state/fakelua.cpp +++ b/src/state/fakelua.cpp @@ -503,6 +503,9 @@ void ThrowIfMultiCVar(const CVar &v) { static CVar DispatchCallRaw(void *addr, const CVar *arg_arr, int arg_count, VarClosure *cl); CVar DispatchCall(State *s, void *addr, const CVar *arg_arr, int arg_count, JITType type, VarClosure *cl) { + // 进入目标引擎代码:切换当前引擎上下文,使本次执行过程中调起的 native 函数 + // (含 native 对象方法桥接)能登记派回本引擎的异步回调。嵌套调用逐层保存恢复。 + State::JitContextScope jit_scope(s, type); // 解释器闭包用 code_str magic 识别,不能看函数指针最低位:GCC 的函数指针可以是奇数。 // type==JIT_INTERP 且 cl 为空:Call()/CallByName 直接调注册的解释器原型。 // cl 是 TCC/GCC 闭包时即使 type 是 JIT_INTERP 也必须走 C 函数指针(解释器回调 JIT 闭包)。 diff --git a/src/state/state.h b/src/state/state.h index 0ddf487..f2c409c 100644 --- a/src/state/state.h +++ b/src/state/state.h @@ -129,6 +129,37 @@ class State { bool prev_; }; + // 当前正在执行的脚本引擎(TCC/GCC/解释器)。 + // 每次 C++ 原生函数被 Lua 调起(FakeluaCallByName/CallByNameArgs 的汇聚点)都会压入 + // 调用方引擎,嵌套调用(JIT → native → Lua 回调 → native)逐层保存恢复。 + // 用途:异步回调(mysql 等)在登记时记住"是从哪个引擎发起的",结果回来后派回同一引擎, + // 与旧的内联闭包自带 func_ptr 的语义一致;不能固定派发到 TCC。 + // 栈空(C++ 宿主不经 Lua 直接调原生内部 API)时返回 JIT_TCC,与历史行为一致。 + [[nodiscard]] JITType CurrentJit() const { + return current_jit_; + } + + // C 编译单元(TCC 生成的 C 代码)不能用 C++ 的 JitContextScope,由 extern "C" + // 的 FakeluaJitContextPush/Pop 经这对 getter/setter 维护当前引擎。 + void SetCurrentJit(JITType jit) { + current_jit_ = jit; + } + + struct JitContextScope { + explicit JitContextScope(State *s, JITType jit) : s_(s), prev_(s->current_jit_) { + s_->current_jit_ = jit; + } + ~JitContextScope() { + s_->current_jit_ = prev_; + } + JitContextScope(const JitContextScope &) = delete; + JitContextScope &operator=(const JitContextScope &) = delete; + + private: + State *s_; + JITType prev_; + }; + // 本 State 的日志输出目标。为 nullptr 表示没指定日志文件,只打控制台。 LogSink *GetLogSink() const { return log_sink_.get(); @@ -184,6 +215,8 @@ class State { std::function var_interface_new_func_; JitErrorBoundary *jit_error_boundary_ = nullptr; bool interp_const_alloc_ = false; + // 当前执行引擎,初值 TCC(无 Lua 调用栈的边界场景,与 kNativeCallbackJit 一致)。 + JITType current_jit_ = JIT_TCC; int reentrant_count_ = 0; StateConfig config_; Compiler compiler_; diff --git a/test/lua/mysql/test_mysql_contract.lua b/test/lua/mysql/test_mysql_contract.lua index dd8e165..82302fe 100644 --- a/test/lua/mysql/test_mysql_contract.lua +++ b/test/lua/mysql/test_mysql_contract.lua @@ -141,8 +141,7 @@ end -- 跨帧绑定参数(P0-1 的新形态):绑定参数序列化后必须活过下一次顶层 Call 的 -- arena Reset。arm 在一次 Call 里登记回调后返回;宿主显式 Reset 临时 arena; -- pump 是下一次 Call,再 tick 到回调。若字节串暂存失效,ctx 字段会坏掉或回调跑不起来。 --- 本用例由 C++ 单引擎(TCC)驱动:具名回调固定派发到 TCC 编译产物,文件级 local --- 是各引擎动态库各自的 static,不能像其他用例那样用 CallAll 三引擎混跑。 +-- 回调派回登记时的引擎:三引擎各自 arm/pump 闭环,文件级 local 各自独立也没问题。 -- cross_hits 只承载动态值(表字段 / nil),不会被推断成文件级数值常量。 local cross_hits = nil diff --git a/test/test_mysql.cpp b/test/test_mysql.cpp index 33d2405..674b881 100644 --- a/test/test_mysql.cpp +++ b/test/test_mysql.cpp @@ -247,9 +247,9 @@ TEST(test_mysql, pool_with_fn_throw_returns_connection) { // 回归(P0-1):异步回调的绑定参数(序列化字节串)跨过下一次顶层 Call 的临时 // arena Reset 后仍然能被反序列化并原样送达回调。 -// 只在 TCC 单引擎下驱动:具名回调派发固定解析到 TCC 编译产物,而文件级 local 是各 -// 引擎动态库各自的 static;CallAll 混跑时 GCC/解释器会读到自己那份未写入的变量。 -// 序列化字节串本身是引擎无关的 C++ 逻辑,单引擎走通即完整覆盖跨 Reset 存活语义。 +// 回调派回【登记时的引擎】:CallAll 三引擎各自 arm 出自己的连接,pump 帧 tick +// 时该连接的具名回调执行同引擎编译产物、写同引擎的文件级 local,三引擎各自闭环, +// 与旧内联闭包模型行为一致。 TEST(test_mysql, closure_callback_survives_frame_reset) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); @@ -257,13 +257,13 @@ TEST(test_mysql, closure_callback_survives_frame_reset) { CompileFile(s, "./mysql/test_mysql_contract.lua", config); int64_t armed = 0; - Call(s, JIT_TCC, "MysqlContractTest.arm_cross_frame", armed); + CallAll(s, "MysqlContractTest.arm_cross_frame", armed); EXPECT_EQ(armed, 1); // 顶层 Call 返回后显式再 Reset 一次,模拟宿主在帧末回收临时 arena。 inter::Reset(s); int64_t hits = 0; - Call(s, JIT_TCC, "MysqlContractTest.pump_cross_frame", hits); + CallAll(s, "MysqlContractTest.pump_cross_frame", hits); EXPECT_EQ(hits, 7); FakeluaDeleteState(s); } From 7a7b385ef9021996f7b6a7c0ffe8a901c711821b Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 2 Oct 2026 07:52:46 +0800 Subject: [PATCH 7/8] fix(mysql,json): resolve pool:with async lease race, clean arena_pin dead code, and optimize json array check --- src/compile/c_runtime_header.h | 8 --- src/native/json/native_json.cpp | 46 ++++++--------- src/native/mysql/mysql_connection.cpp | 14 +++++ src/native/mysql/mysql_connection.h | 1 + src/native/mysql/native_mysql.cpp | 1 + src/native/mysql/native_mysql.h | 3 + src/native/mysql/native_mysql_pool.cpp | 81 ++++++++++++++++++++++++-- src/native/object/native_object.cpp | 2 - src/state/heap.cpp | 12 +--- src/state/heap.h | 7 --- src/var/var_table.h | 5 -- test/lua/mysql/test_mysql_contract.lua | 10 ++++ test/test_mysql.cpp | 20 +++++++ 13 files changed, 146 insertions(+), 64 deletions(-) diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index a10c918..ffb503a 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -82,10 +82,6 @@ struct VarTable { 需要重算——因此把 VarTable 整体清零的分配路径天然落在安全的重算分支上。 */ uint32_t seq_len_valid_; int64_t seq_len_; - /* spec 块字节数。0 = 无 spec。与 src/var/var_table.h 同布局。 */ - uint32_t spec_bytes; - /* >0:spec 是连续 CVar(表特化)。0:不透明块,按 spec_bytes 整体搬迁。 */ - uint32_t spec_cvars; }; typedef struct State State; @@ -660,8 +656,6 @@ static inline uint32_t FlHashString(const char *str, int len) { __t->spec_count = 0; \ __t->seq_len_valid_ = 1; \ __t->seq_len_ = 0; \ - __t->spec_bytes = 0; \ - __t->spec_cvars = 0; \ assert(sizeof(__t->quick_data_) == 8 * sizeof(VarEntry)); \ { int __i; for (__i = 0; __i < 8; ++__i) { \ __t->quick_data_[__i].key.type_ = VAR_NIL; \ @@ -682,8 +676,6 @@ static inline uint32_t FlHashString(const char *str, int len) { (v).data_.t->spec_keys = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \ (v).data_.t->spec_vals = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \ (v).data_.t->spec_count = (field_count); \ - (v).data_.t->spec_bytes = (uint32_t)sizeof(SpecType); \ - (v).data_.t->spec_cvars = (uint32_t)(field_count); \ assert(sizeof(SpecType) == (field_count) * sizeof(CVar)); \ } while(0) diff --git a/src/native/json/native_json.cpp b/src/native/json/native_json.cpp index 0b00ee7..cf6f725 100644 --- a/src/native/json/native_json.cpp +++ b/src/native/json/native_json.cpp @@ -85,6 +85,23 @@ static CVar JsonValueToLua(State *s, const bj::value &v) { return inter::NativeToFakeluaNil(s); } +// 纯数组 table 启发式:空 table 一律编码为 [](历史上空 table 产出 {}, +// 客户端按数组解析时直接崩);非空时要求 key 全部是从 1 起的连续整数。 +static bool CheckIsArray(const std::vector &kvs) { + int64_t max_idx = 0; + for (const auto &kv: kvs) { + if (kv.key.type_ != static_cast(VarType::Int)) { + return false; + } + int64_t key = kv.key.data_.i; + if (key < 1 || key > 1000000) { + return false; + } + if (key > max_idx) max_idx = key; + } + return kvs.empty() || static_cast(max_idx) == kvs.size(); +} + // Convert Lua CVar to boost::json::value static bj::value LuaToJsonValue(CVar v, int depth, std::unordered_set &visited) { if (depth > kMaxJsonDepth) { @@ -112,25 +129,7 @@ static bj::value LuaToJsonValue(CVar v, int depth, std::unordered_set(VarType::Int)) { - is_array = false; - break; - } - int64_t key = kv.key.data_.i; - if (key < 1 || key > 1000000) { - is_array = false; - break; - } - if (key > max_idx) max_idx = key; - } - if (is_array && !kvs.empty() && static_cast(max_idx) != kvs.size()) { - is_array = false; - } + bool is_array = CheckIsArray(kvs); if (is_array) { std::sort(kvs.begin(), kvs.end(), [](const table::TableKV &a, const table::TableKV &b) { return a.key.data_.i < b.key.data_.i; }); @@ -187,14 +186,7 @@ static CVar JsonEncode(State *s, CVar *args, int n) { static bool IsArrayLikeTable(CVar v) { if (v.type_ != static_cast(VarType::Table) || !v.data_.t) return false; auto kvs = table::TableHelper::CollectKVPairs(v); - int64_t max_idx = 0; - for (auto &kv: kvs) { - if (kv.key.type_ != static_cast(VarType::Int)) return false; - int64_t key = kv.key.data_.i; - if (key < 1 || key > 1000000) return false; - if (key > max_idx) max_idx = key; - } - return kvs.empty() || static_cast(max_idx) == kvs.size(); + return CheckIsArray(kvs); } // json.encode_array(value) → JSON string diff --git a/src/native/mysql/mysql_connection.cpp b/src/native/mysql/mysql_connection.cpp index 0357005..c03d92a 100644 --- a/src/native/mysql/mysql_connection.cpp +++ b/src/native/mysql/mysql_connection.cpp @@ -525,6 +525,7 @@ void MysqlConnection::Close() { pending_result_ = false; pending_results_.clear(); next_stmt_id_ = 1; + DrainQueryQueue(); } void MysqlConnection::Continue(int ready) { @@ -772,6 +773,10 @@ void MysqlConnection::SetState(::fakelua::State *state) { lua_state_ = state; } void MysqlConnection::SetNativeObject(::fakelua::NativeObject *obj) { native_obj_ = obj; } bool MysqlConnection::Connected() const { return ready_; } void MysqlConnection::MarkConnectedForTest() { + ClearWait(); + wait_op_ = WaitOp::None; + pending_connect_ = false; + pending_connect_err_.clear(); state_ = ConnState::Ready; ready_ = true; close_pending_ = false; @@ -780,6 +785,15 @@ bool MysqlConnection::Connecting() const { return state_ == ConnState::Connectin int MysqlConnection::TickDepth() const { return tick_depth_; } bool MysqlConnection::ClosePending() const { return close_pending_; } void MysqlConnection::RequestClose() { close_pending_ = true; } +bool MysqlConnection::HasPendingWork() const { + return state_ == ConnState::Querying || + state_ == ConnState::Connecting || + state_ == ConnState::Handshaking || + wait_op_ != WaitOp::None || + pending_result_ || + pending_connect_ || + !queued_queries_.empty(); +} void MysqlConnection::SetError(MysqlErrorType type, uint16_t code, const std::string &msg, const std::string &sql_state) { last_error_.type = type; diff --git a/src/native/mysql/mysql_connection.h b/src/native/mysql/mysql_connection.h index 9f2e526..8cfdd14 100644 --- a/src/native/mysql/mysql_connection.h +++ b/src/native/mysql/mysql_connection.h @@ -128,6 +128,7 @@ class MysqlConnection { int TickDepth() const; bool ClosePending() const; void RequestClose(); + bool HasPendingWork() const; private: native::IoContext &io_; diff --git a/src/native/mysql/native_mysql.cpp b/src/native/mysql/native_mysql.cpp index 92e93f2..1c3957f 100644 --- a/src/native/mysql/native_mysql.cpp +++ b/src/native/mysql/native_mysql.cpp @@ -390,6 +390,7 @@ void TickMysqlConnection(NativeObject *self, State *s) { conn->SetState(s); conn->Tick(); MaybeReleaseOwnedConn(self); + MaybeReleasePooledConn(self); MaybeReapPool(self); } diff --git a/src/native/mysql/native_mysql.h b/src/native/mysql/native_mysql.h index 2fdd513..99a55e6 100644 --- a/src/native/mysql/native_mysql.h +++ b/src/native/mysql/native_mysql.h @@ -31,6 +31,8 @@ MysqlConnection *UnwrapConnNative(NativeObject *self); // Per-State NativeObject registry so FakeluaDeleteState can close sockets. void RegisterMysqlNativeWrapper(State *s, NativeObject *nat, bool is_pool); void UnregisterMysqlNativeWrapper(NativeObject *nat); +void DetachAcquiredWrapper(NativeObject *nat); +void MaybeReleasePooledConn(NativeObject *self); void OnStateDeleted(State *s); // 驱动本 State 上所有连接池和连接。由 runtime.tick() 调用。 @@ -39,5 +41,6 @@ void TickAll(State *s); // 单测:pool:with 的 fn 抛错后连接必须回到可再次 Acquire 的状态。 // 归还成功返回 1;fn 没有抛错返回 -1;归还失败(仍被占用)返回 0。 int TestPoolWithFnThrowReturnsConnection(State *s, CVar fn); +int TestPoolWithAsyncQueryAutoRelease(State *s, CVar fn); }// namespace fakelua::mysql diff --git a/src/native/mysql/native_mysql_pool.cpp b/src/native/mysql/native_mysql_pool.cpp index e59f6b6..82c86cf 100644 --- a/src/native/mysql/native_mysql_pool.cpp +++ b/src/native/mysql/native_mysql_pool.cpp @@ -83,7 +83,7 @@ static void InvalidateAcquiredWrappers(PoolObject *po) { } } -static void DetachAcquiredWrapper(NativeObject *nat) { +void DetachAcquiredWrapper(NativeObject *nat) { if (!nat) return; auto *po = reinterpret_cast(nat->GetInt("__mysql_pool_obj__", 0)); auto *c = UnwrapConnNative(nat); @@ -97,6 +97,15 @@ static void DetachAcquiredWrapper(NativeObject *nat) { nat->SetInt("__mysql_conn__", 0); nat->SetInt("__mysql_pool_ptr__", 0); nat->SetInt("__mysql_pool_obj__", 0); + nat->SetInt("__mysql_auto_release__", 0); +} + +void MaybeReleasePooledConn(NativeObject *self) { + if (!self) return; + if (self->GetInt("__mysql_auto_release__", 0) == 0) return; + auto *conn = UnwrapConnNative(self); + if (!conn || conn->TickDepth() > 0 || conn->HasPendingWork()) return; + DetachAcquiredWrapper(self); } // mysql_pool.create(config) → pool object @@ -266,11 +275,22 @@ static CVar PoolWith(NativeObject *self, State *s, CVar *args, int n) { if (!nat) return inter::NativeToFakeluaNil(s); CVar conn_arg = inter::NativeToFakeluaNativeObject(s, nat); - // 析构时归还:fn 正常返回和抛错都走这里,避免 catch 漏掉某条路径。 + auto *conn = UnwrapConnNative(nat); + // 租约保护:离开作用域时检查。 + // 若无在途/排队的异步工作(如同步抛错或未发起 query),立刻归还连接; + // 若已有 query 在飞行或排队中,打上 auto_release 标记,由 Tick 在全部 query 结果派发完毕后自动归还。 struct LeaseGuard { NativeObject *nat; - ~LeaseGuard() { DetachAcquiredWrapper(nat); } - } lease{nat}; + MysqlConnection *conn; + ~LeaseGuard() { + if (!nat) return; + if (!conn || !conn->HasPendingWork()) { + DetachAcquiredWrapper(nat); + } else { + nat->SetInt("__mysql_auto_release__", 1); + } + } + } lease{nat, conn}; // fn 是同帧同步调用,直接沿用当前 Lua 调用方的引擎(异常边界随之正确), // 不固定 TCC 标记。 return inter::DispatchCallClosure(s, a0.data_.cl, &conn_arg, 1, s->CurrentJit()); @@ -324,6 +344,59 @@ int TestPoolWithFnThrowReturnsConnection(State *s, CVar fn) { return 1; } +int TestPoolWithAsyncQueryAutoRelease(State *s, CVar fn) { + PoolConfig config; + config.host = "127.0.0.1"; + config.port = 1; + config.user = "root"; + config.password = "x"; + config.database = "test"; + config.pool_size = 1; + config.connect_timeout_ms = 200; + config.read_timeout_ms = 200; + config.heartbeat_interval_ms = 0; + config.max_retries = 0; + + auto *pool_obj = new PoolObject(); + pool_obj->config = config; + pool_obj->pool = std::make_unique(config, s); + pool_obj->pool->Initialize(); + pool_obj->pool->MarkConnectedForTest(); + + int64_t gid = s->GetNativeObjectManager().CreateGroup(); + auto *nat = s->GetNativeObjectManager().Create(gid, "mysql_pool"); + nat->SetInt("__mysql_pool__", reinterpret_cast(pool_obj)); + RegisterMysqlNativeWrapper(s, nat, true); + nat->SetFinalizer([](NativeObject *self) { + UnregisterMysqlNativeWrapper(self); + auto *p = UnwrapPool(self); + if (p) { + InvalidateAcquiredWrappers(p); + delete p; + self->SetInt("__mysql_pool__", 0); + } + }); + + CVar args[1] = {fn}; + PoolWith(nat, s, args, 1); + + // fn 返回后,异步 query 仍在等待/飞行,连接不应被立刻归还 + MysqlConnection *in_use = pool_obj->pool->Acquire(); + if (in_use) { + pool_obj->pool->Release(in_use); + return 0; + } + + // 驱动 Tick:派发查询结果或错误回调 + TickAll(s); + + // 查询处理完毕后,连接应当被自动归还 + MysqlConnection *again = pool_obj->pool->Acquire(); + if (!again) return 0; + pool_obj->pool->Release(again); + return 1; +} + // pool:release(conn) static CVar PoolRelease(NativeObject *self, State *s, CVar *args, int n) { diff --git a/src/native/object/native_object.cpp b/src/native/object/native_object.cpp index 1e570ce..7e5e44a 100644 --- a/src/native/object/native_object.cpp +++ b/src/native/object/native_object.cpp @@ -492,8 +492,6 @@ CVar NativeObject::Wrap(State *s) const { vtbl->spec = spec; vtbl->spec_get = reinterpret_cast(NativeSpecGet); vtbl->spec_set = reinterpret_cast(NativeSpecSet); - vtbl->spec_bytes = static_cast(sizeof(NativeObjectSpec)); - vtbl->spec_cvars = 0; // 填充 spec_keys / spec_vals(供 pairs() 迭代) RefreshSpecKeys(vtbl, this, s); diff --git a/src/state/heap.cpp b/src/state/heap.cpp index 375651b..703eeab 100644 --- a/src/state/heap.cpp +++ b/src/state/heap.cpp @@ -70,14 +70,4 @@ size_t HeapAllocator::Size() const { return current_block_index_ * BLOCK_SIZE + current_block_offset_; } -bool HeapAllocator::Contains(const void *p) const { - if (!p) return false; - const auto *addr = static_cast(p); - for (const void *block: blocks_) { - const auto *base = static_cast(block); - if (addr >= base && addr < base + BLOCK_SIZE) return true; - } - return false; -} - -}// namespace fakelua +} // namespace fakelua diff --git a/src/state/heap.h b/src/state/heap.h index 76b7b9b..8515d16 100644 --- a/src/state/heap.h +++ b/src/state/heap.h @@ -29,9 +29,6 @@ class HeapAllocator { // 当前临时内存使用 [[nodiscard]] size_t Size() const; - // p 是否落在本分配器已经切出的块里。const arena 不 Reset,用来识别已经钉住的对象。 - [[nodiscard]] bool Contains(const void *p) const; - private: struct DestructorInfo { void (*destroyer)(void *); @@ -61,10 +58,6 @@ class Heap { // const_allocator_ 不重置,常量内存一直保留 } - [[nodiscard]] bool OwnsConst(const void *p) const { - return const_allocator_.Contains(p); - } - private: HeapAllocator temp_allocator_; // 临时内存分配器,编译过程中使用,编译结束后重置 HeapAllocator const_allocator_;// 常量内存分配器,编译过程中使用,编译结束后不重置 diff --git a/src/var/var_table.h b/src/var/var_table.h index 0a23d18..fb9e8c6 100644 --- a/src/var/var_table.h +++ b/src/var/var_table.h @@ -40,11 +40,6 @@ struct VarTable { // 需要重算——因此把 VarTable 整体清零的分配路径天然落在安全的重算分支上。 uint32_t seq_len_valid_; int64_t seq_len_; - // spec 块字节数。0 表示没有需要搬迁的 spec(spec 指针为空)。 - // 与 c_runtime_header.h 的 VarTable 保持同布局。 - uint32_t spec_bytes; - // >0 时 spec 块是连续 spec_cvars 个 CVar(JIT 表特化)。0 表示不透明块(如 NativeObjectSpec)。 - uint32_t spec_cvars; }; }// namespace fakelua diff --git a/test/lua/mysql/test_mysql_contract.lua b/test/lua/mysql/test_mysql_contract.lua index 82302fe..2907e78 100644 --- a/test/lua/mysql/test_mysql_contract.lua +++ b/test/lua/mysql/test_mysql_contract.lua @@ -138,6 +138,16 @@ function make_thrower() end end +-- 给 C++ 单测用:返回一个调用 c:query 的闭包,用于验证 pool:with 异步查询未完成时不提前归还连接 +function make_query_runner() + return function(c) + c:query("SELECT 1", "MysqlContractTest.on_dummy_result") + end +end + +function on_dummy_result(conn, err, result) +end + -- 跨帧绑定参数(P0-1 的新形态):绑定参数序列化后必须活过下一次顶层 Call 的 -- arena Reset。arm 在一次 Call 里登记回调后返回;宿主显式 Reset 临时 arena; -- pump 是下一次 Call,再 tick 到回调。若字节串暂存失效,ctx 字段会坏掉或回调跑不起来。 diff --git a/test/test_mysql.cpp b/test/test_mysql.cpp index 674b881..93983f7 100644 --- a/test/test_mysql.cpp +++ b/test/test_mysql.cpp @@ -245,6 +245,26 @@ TEST(test_mysql, pool_with_fn_throw_returns_connection) { FakeluaDeleteState(s); } +// 回归(P1-4):pool:with 内部发起异步 query 时,连接不能在 fn 返回瞬间立即归还, +// 必须在 query 结果派发完毕后才自动释放回池。 +TEST(test_mysql, pool_with_async_query_auto_release) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./mysql/test_mysql_contract.lua", config); + + int arg_count = 0; + bool is_vararg = false; + void *addr = inter::GetFuncAddr(s, JIT_TCC, "MysqlContractTest.make_query_runner", arg_count, is_vararg); + ASSERT_NE(addr, nullptr); + CVar fn = inter::DispatchCall(s, addr, nullptr, arg_count, JIT_TCC); + ASSERT_EQ(fn.type_, static_cast(VarType::Closure)); + + const int ok = mysql::TestPoolWithAsyncQueryAutoRelease(s, fn); + EXPECT_EQ(ok, 1); + FakeluaDeleteState(s); +} + // 回归(P0-1):异步回调的绑定参数(序列化字节串)跨过下一次顶层 Call 的临时 // arena Reset 后仍然能被反序列化并原样送达回调。 // 回调派回【登记时的引擎】:CallAll 三引擎各自 arm 出自己的连接,pump 帧 tick From 2232a7bf4ba4ecbd3912df64839b23bb0f80505d Mon Sep 17 00:00:00 2001 From: esrrhs Date: Fri, 2 Oct 2026 14:50:03 +0800 Subject: [PATCH 8/8] fix(jit): register FakeluaJitContextPush/Pop and MultiCVar symbols for Windows TCC --- src/jit/tcc_handle.cpp | 4 ++++ src/jit/vm.h | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/jit/tcc_handle.cpp b/src/jit/tcc_handle.cpp index a304dae..0ec204a 100644 --- a/src/jit/tcc_handle.cpp +++ b/src/jit/tcc_handle.cpp @@ -41,6 +41,10 @@ TCCHandle::TCCHandle(State *s, const CompileConfig &cfg) { tcc_add_symbol(tcc_state_, "FakeluaCallByName", reinterpret_cast(FakeluaCallByName)); tcc_add_symbol(tcc_state_, "FlEvalLoadClosure", reinterpret_cast(FlEvalLoadClosure)); tcc_add_symbol(tcc_state_, "FakeluaInterpCall", reinterpret_cast(FakeluaInterpCall)); + tcc_add_symbol(tcc_state_, "FakeluaAllocMultiCVar", reinterpret_cast(FakeluaAllocMultiCVar)); + tcc_add_symbol(tcc_state_, "FakeluaSetMultiCVarElement", reinterpret_cast(FakeluaSetMultiCVarElement)); + tcc_add_symbol(tcc_state_, "FakeluaJitContextPush", reinterpret_cast(FakeluaJitContextPush)); + tcc_add_symbol(tcc_state_, "FakeluaJitContextPop", reinterpret_cast(FakeluaJitContextPop)); tcc_add_symbol(tcc_state_, "FakeluaLogLua", reinterpret_cast(FakeluaLogLua)); tcc_add_symbol(tcc_state_, "GetLogLevel", reinterpret_cast(GetLogLevel)); tcc_define_symbol(tcc_state_, "FAKELUA_JIT_TYPE", std::to_string(static_cast(JIT_TCC)).c_str()); diff --git a/src/jit/vm.h b/src/jit/vm.h index 1a1baa9..bfc4639 100644 --- a/src/jit/vm.h +++ b/src/jit/vm.h @@ -97,6 +97,14 @@ extern "C" CVar FakeluaCallByName(State *state, int jit_type, const char *name, extern "C" CVar FlEvalLoadClosure(State *state, VarClosure *cl, int arg_num, const CVar *args); +extern "C" CVar FakeluaAllocMultiCVar(State *state, int count); + +extern "C" void FakeluaSetMultiCVarElement(CVar *multi, int idx, CVar val); + +extern "C" int FakeluaJitContextPush(State *s, int jit); + +extern "C" void FakeluaJitContextPop(State *s, int prev); + // 数组版 FakeluaCallByName,供解释器使用 CVar CallByNameArgs(State *state, int jit_type, const char *name, int arg_num, const CVar *args);