Lua 5.4 C API:栈、值转换与保护调用
先理解 C 指针、函数返回值与对象生存期,以及 Lua table 和函数。宿主是链接 Lua 库的 C 程序;Lua 脚本只在它提供的状态与能力里执行。本组用 C99,把 Lua 作为库,不要求独立解释器参与。示例未执行;本篇片段用于解释完整工程中的操作,不是独立程序。
教学版本与接口兼容
Lua宿主应该固定哪个版本?本组固定 Lua 5.4.9,头文件与库必须匹配。2026-10-17 核对官方版本历史:5.4.9 是 5.4 最终发布,5.5.1 属于另一版本;选定 5.4 是为了学习现有嵌入接口,不声称使用最新系列或承诺维护更新。现有两篇课程以 5.5 独立解释器为前提,保留其原版本说明,不将它们的运行记录借作本工程证据。
数字配置、目标架构和链接方式也是契约的一部分。Lua 默认整数通常为 64 位,定制构建可不同,不能在宿主中把 lua_Integer 写死成 int。本工程数值域仅为正负一百万,使用官方默认构建;CMake 固定版本,源码头部再检查 release 宏。动态链接还要核对实际装载的库,编译成功不证明运行时 ABI 一致。
Windows 使用与 C 工具链匹配的头文件及导入库或静态库,Linux/macOS 使用相应开发库;本轮都未安装、构建或启动。LuaJIT、Lua 5.1 和引擎自定义接口需要单独适配,不能因为函数名相似就混用。官方版本历史与下载区是固定版本的依据。
记录栈顶并理解索引
Lua C API怎样保持栈平衡?先用 lua_gettop 记录入口栈顶,再给每个调用写清栈变化,最后仅清理本层拥有的值。栈是 C 和 Lua 交换值的协议,不是 C 的调用栈,也不是“所有对象的储存位置”。从 1 开始的正索引指向本次 C 函数参数和临时值,-1 表示当前栈顶,-2 是其下一项;push 后负索引含义会移动。
/* 插在已有 lua_State *L 的受保护 C 函数中;不含 close 槽。 */
int base = lua_gettop(L);
lua_newtable(L); /* base + 1 */
int table_index = lua_absindex(L, -1);
lua_pushinteger(L, 7); /* base + 2 */
lua_setfield(L, table_index, "count"); /* 弹出 value,base + 1 */
lua_getfield(L, table_index, "count"); /* 推入结果,base + 2 */
lua_pop(L, 1); /* base + 1 */
lua_settop(L, base); /* 删除本层临时表 */绝对索引适合跨多次 push 保留表位置;它不是跨调用永久引用,缩栈后对应槽仍会失效。registry 的伪索引不属于普通栈槽,不能传给要求普通栈位置的消息处理器。C 函数返回整数表示留给 Lua 的结果数量,返回 0 不等于返回数值零;需要零值时应先 push 再 return 1。
Lua 5.4 存在 to-be-closed 槽,lua_settop/lua_pop 在清除这些槽时可能触发关闭动作。本工程没有调用 lua_toclose;上述“恢复栈顶”不能被泛化为任意业务中绝不会执行用户代码的操作。栈增长前应理解 API 所需空间,需要批量 push 时用 lua_checkstack 并处理失败;本工程单次推入数量处于 C 调用的保证空间内。
push 与 get 的值协议
Lua宿主怎样传入并取回值?push 把一个值放上栈,get 取得字段或全局后也会把结果推入栈;它们不是简单的 C 指针赋值。lua_pushinteger/lua_pushboolean 传标量,lua_pushlstring 复制指定长度的字节,lua_pushvalue 复制栈上的引用,后者不深拷贝 table 或 userdata。
lua_getfield 可能触发 __index,lua_setfield 可能触发 __newindex,因此不能在持有未托管外部资源时当作无错误操作。读取不需要元方法的内部表时,可使用 lua_rawgeti 等 raw 操作;raw 只绕过元方法,不自动校验对象类型、引用归属或边界。
| API | 本层应记住的变化 | 典型用途 |
|---|---|---|
lua_pushinteger(L, n) | 增加一个整数 | 请求参数与返回值 |
lua_getfield(L, i, key) | 保留表并增加一个结果 | 读取插件字段 |
lua_setfield(L, i, key) | 消耗栈顶值 | 建立元表方法 |
lua_rawgeti(L, LUA_REGISTRYINDEX, ref) | 增加引用对象 | 取回持久回调 |
luaL_ref(L, LUA_REGISTRYINDEX) | 消耗栈顶对象 | 由 registry 保活 |
例如 counter:add(delta) 在 C 中收到参数 1 为 userdata、参数 2 为 delta;若误写 counter.add(delta),第一个参数就变成数值,luaL_checkudata 应拒绝它。调用约定属于接口设计,不能用跳过校验来“兼容”错误的 self。
整数校验、转换与溢出
Lua C回调怎样严格接收整数?本工程先用 lua_isinteger 要求整数子类型,再用 luaL_checkinteger 取值,最后进行业务范围检查。luaL_checkinteger 本身允许能转换成整数的值,因此只调用它并不表达“拒绝字符串及 1.0”的协议。读取结果同理,先验证类型再用 lua_tointeger;仅看转换所得的 0 无法区分真实零和转换失败。
C 的有符号整数溢出可能是未定义行为。不能先算 value + delta 再检查结果。先约束 delta 到 [-LIMIT, LIMIT],对于正 delta 检查 value > LIMIT - delta,对于负 delta 检查 value < -LIMIT - delta;通过才加。由于 value 与 delta 均已受限,这两个差值也可表示。不要先对任意输入取负或绝对值,最小有符号整数的取负本身可能溢出。
完整工程使用 LIMIT=1000000,拒绝“超出业务域”,不模拟 Lua 整数回绕行为。浮点结果若要支持,应另行规定有限数、NaN、无穷、取整与精度策略,不能悄悄截断。命令行通过十进制词法检查和 strtoll 的 ERANGE 检查后再缩到业务范围,避免将宿主解析问题推给 Lua。
字节串与借用指针
Lua字符串指针能保存多久?lua_tolstring 返回借用指针,必须保留对象的有效引用,并在其有效期内使用;字符串长度需要单独读取。Lua 字符串能包含 NUL,不可把 strlen 或 %s 当作完整长度协议。本工程输出错误时用 fwrite(message, 1, length, stderr),不向请求结构保存该指针。
lua_tolstring 可把数字转成字符串;本工程先判断错误对象类型为 LUA_TSTRING,再取字符串,避免为错误报告引入额外数值转换。错误对象可能是 table、userdata 等,不能假定所有错误都是文本。若要跨状态关闭保存消息,应在有效期内复制到由宿主拥有的缓冲区,并定义长度上限与释放方。
UTF-8 是应用层约定,字节串本身不验证编码。文件路径、日志和插件返回文本需要分别约定编码与平台处理。本例业务值仅为整数,错误信息按现有字节输出,不声称实现 Windows Unicode 路径支持。
加载与保护调用
luaL_loadfile和lua_pcall有什么区别?加载负责把文本文件编译成函数,保护调用负责执行函数;每一步都要检查状态码。工程采用 luaL_loadfilex(...,"t") 限定文本 chunk。加载失败时栈顶为错误对象;成功时栈顶为待调用函数,不是 chunk 返回结果。
lua_pcall(L, nargs, nresults, 0) 消耗函数与参数。成功后按约定留下结果,失败后留下一个错误对象;调用者应先看状态码,再决定栈顶的含义。工程分两次调用:先以 0 参数执行 chunk 得到回调,再以 2 参数执行回调得到一个整数。参数数量、结果类型以及错误对象不能混作同一协议。
外围 main 另以一个 lua_pcall 保护整个 setup/加载/引用创建过程。这样注册元表或创建 userdata 时的内存错误也有保护边界;只把业务函数放进 pcall,不能保护它之前的分配操作。内部失败用 lua_error 把现有错误对象继续抛给外层,外层统一打印、清栈、关闭状态。这里没有 traceback 处理器,生产诊断的扩展见错误边界。
练习
- 在栈顶为 table 时 push 一个整数,原来的 -1 现在指向什么?如何保持原表索引?
- 为什么只调用
luaL_checkinteger不能拒绝所有数值字符串和浮点整数? - 已有 value=1000000、delta=1,写出不进行加法的拒绝条件。
参考解答
- -1 指向新整数;push 前用
lua_absindex保存原表普通栈索引,或根据已知栈变化改为 -2。绝对索引只在该槽仍存在时有效。 - 该辅助函数允许可转换值。严格接口先检查
lua_isinteger,再提取并检查范围;不以“转换成功”替代“类型符合协议”。 - delta 为正且
value > LIMIT - delta,这里是 1000000 > 999999,直接报错,不计算相加结果。
资料核对 2026-10-17:官方 5.4 手册 C API、官方版本规则。正文按宿主工程独立组织,代码与三平台行为未执行验证。
继续阅读
- 排错
- 开发实践
- 开发实践