总览与准备
本指南由 Kimi K2.6 编写生成。
仓库地址:https://github.com/qaqland/lua-c-api-guide
什么是 Lua C API
Lua 被设计为一个可嵌入的脚本语言。它提供了一组 C 语言接口,允许宿主程序(Host Program):
- 创建和销毁 Lua 状态机(
lua_State) - 在 C 和 Lua 之间传递数据
- 从 C 调用 Lua 函数
- 将 C 函数暴露给 Lua 脚本
- 操作 Lua 中的 table、userdata、metatable 等高级类型
关键头文件
只需关注两个头文件:
| 头文件 | 作用 |
|---|---|
lua.h | 核心 API。所有以 lua_ 开头的函数 |
lauxlib.h | 辅助库(auxiliary library)。以 luaL_ 开头的辅助函数,让代码更简洁安全 |
通常 C 文件顶部只需要:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h> // 如需打开标准库
编译环境
需要预先安装 Lua 5.4 开发包(如 lua5.4-dev)。下面介绍两种编译方式:手动用 gcc,以及本指南使用的
Meson。
手动编译
通过 pkg-config 获取头文件和库路径,手动调用 gcc:
gcc 00-overview.c $(pkg-config --cflags --libs lua5.4) -o 00-overview
./00-overview
Meson(本指南使用)
Meson 自动通过 pkg-config 获取 Lua 5.4 的编译参数,无需手动指定 -I、-L、-l:
meson setup build
meson compile -C build
./build/src/01_stack
最小可运行程序
#include <lua.h>
#include <stdio.h>
int main(void) {
printf("Lua version: %.0f\n", lua_version(NULL));
return 0;
}
这个程序只打印 Lua 版本号,验证开发环境是否正确链接了 Lua 库。后续章节会逐步介绍状态机的创建、使用和销毁。
参考资源
源码头文件(Lua 5.4 官方仓库):
致谢
感谢 st0nie 提供支持。
虚拟栈(The Stack)
Lua C API 的核心是虚拟栈。Lua 与 C 之间不直接共享变量,所有数据交换都通过栈完成。
状态机的创建与销毁
操作栈之前,需要先创建一个 Lua 状态机(lua_State):
lua_State *L = luaL_newstate(); // 创建状态机
// ... 使用栈 ...
lua_close(L); // 销毁状态机,释放资源
luaL_newstate 使用默认内存分配器创建一个全新的 Lua 状态机。后续第 12 章会介绍如何自定义分配器。
栈的基本特性
- Lua 管理栈的内存,栈会自动增长
- C 代码负责保持栈平衡:压入多少就要相应弹出多少
- 栈索引从 1 开始表示从底向上,从 -1 开始表示从顶向下
index value
top [5] [-1] "hello"
| [4] [-2] 3.14
| [3] [-3] 42
| [2] [-4] true
bottom [1] [-5] nil
常用栈操作
| 操作 | 函数示例 | 说明 |
|---|---|---|
| 压栈 | lua_pushnil, lua_pushboolean, … | 将值压入栈顶 |
| 查询 | lua_type, lua_isnumber, … | 获取栈中值的类型或内容 |
| 控制 | lua_pop, lua_remove, … | 调整栈的内容 |
| 大小 | lua_gettop, lua_settop | 获取/设置栈高度 |
索引规则
- 绝对索引:
1到lua_gettop(L),从栈底开始 - 相对索引:
-1到-lua_gettop(L),从栈顶开始 - 伪索引:如
LUA_REGISTRYINDEX,用于访问注册表,不在栈上
惯例:Lua C API 中绝大多数接受栈索引的函数同时支持正索引和负索引,
-1始终代表栈顶。
栈平衡
C 函数操作的是共享栈,返回前必须确保栈回到预期状态。推荐在进入 C 函数时记录栈底,返回前恢复:
static int myfunc(lua_State *L) {
int base = lua_gettop(L);
// ... 各种中间栈操作 ...
lua_settop(L, base); // 清理中间值
lua_pushinteger(L, result);
return 1;
}
栈检查
在 C 函数被 Lua 调用时,栈空间可能有限。如果你需要压入大量值,应先检查:
lua_checkstack(L, 20); // 成功返回 1,失败返回 0
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
lua_pushnil(L);
lua_pushboolean(L, 1);
lua_pushinteger(L, 42);
lua_pushnumber(L, 3.14);
lua_pushstring(L, "hello");
assert(lua_gettop(L) == 5);
assert(lua_type(L, -1) == LUA_TSTRING);
lua_pop(L, 2);
assert(lua_gettop(L) == 3);
assert(lua_isinteger(L, -1) == 1);
assert(strcmp(lua_tostring(L, -1), "42") == 0);
lua_pushvalue(L, 1);
lua_remove(L, 1);
lua_insert(L, 1);
lua_settop(L, 5);
assert(lua_gettop(L) == 5);
lua_settop(L, 2);
assert(lua_gettop(L) == 2);
lua_close(L);
puts("01-stack: ok");
return 0;
}
数据类型转换
Lua 是动态类型语言,C 是静态类型语言。交互时需要在两者之间转换。
Lua ↔ C 类型对照
| Lua 类型 | C API 检查 | C API 读取 | C API 写入 |
|---|---|---|---|
| nil | lua_isnil | — | lua_pushnil |
| boolean | lua_isboolean | lua_toboolean | lua_pushboolean |
| integer | lua_isinteger | lua_tointeger | lua_pushinteger |
| number (float) | lua_isnumber | lua_tonumber | lua_pushnumber |
| string | lua_isstring* | lua_tostring | lua_pushstring |
| table | lua_istable | — | lua_newtable |
| function | lua_isfunction | — | lua_pushcfunction |
| userdata | lua_isuserdata | lua_touserdata | lua_newuserdatauv |
| thread | lua_isthread | lua_tothread | — |
*注意:
lua_isstring对 number 也返回 true,因为 number 可以自动转换为 string。
类型检查与转换
安全读取(带默认值)
lua_Integer n = luaL_optinteger(L, 1, 10); // 第1参数不是整数则返回10
lua_Number x = luaL_optnumber(L, 2, 0.0);
const char *s = luaL_optstring(L, 3, "default");
严格检查(C 函数参数校验)
lua_Integer n = luaL_checkinteger(L, 1); // 类型不对则抛出 Lua 错误
lua_Number x = luaL_checknumber(L, 2);
const char *s = luaL_checkstring(L, 3);
字符串处理
lua_tostring 返回的指针指向 Lua 内部字符串,只要该字符串还在栈上或未被 GC,指针就有效。不要 free 它。
对于包含 \0 的二进制字符串,使用 lua_pushlstring / lua_tolstring:
size_t len;
const char *data = lua_tolstring(L, -1, &len); // 获取长度和内容
lua_pushlstring(L, data, len); // 压入二进制串
boolean 陷阱
lua_toboolean 对以下值返回 0(false):
nilfalse- 其余所有值(包括
0和空字符串)都返回1(true)
这与 C 的“0 为假”逻辑不同,需特别注意。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
lua_pushnil(L);
lua_pushboolean(L, 1);
lua_pushinteger(L, 42);
lua_pushnumber(L, 3.14);
lua_pushstring(L, "hello");
lua_pushlstring(L, "hi\x00world", 8);
assert(lua_type(L, -1) == LUA_TSTRING);
assert(lua_isinteger(L, -2) == 0);
size_t len;
const char *s = lua_tolstring(L, -1, &len);
assert(strcmp(s, "hi") == 0);
assert(len == 8);
assert(lua_tointeger(L, 3) == 42);
assert(lua_tonumber(L, 4) == 3.14);
lua_settop(L, 0);
lua_pushinteger(L, 100);
assert(luaL_checkinteger(L, 1) == 100);
assert(luaL_optinteger(L, 2, 99) == 99);
lua_close(L);
puts("02-types: ok");
return 0;
}
从 C 调用 Lua
C 代码可以加载并执行 Lua 代码,也可以直接调用 Lua 函数。
执行 Lua 代码
最简单的方式是使用辅助库的 luaL_dostring 和 luaL_dofile(第 14 章会讲更底层的加载机制):
// 执行字符串
if (luaL_dostring(L, "return 1 + 1") != LUA_OK) {
// 出错,错误信息在栈顶
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
它们实际上是两步的封装:先加载,再执行。
lua_pcall 用于在保护模式下调用栈上的函数,这是 C 与 Lua 交互的核心机制。第 6 章会深入讲解错误处理。
调用 Lua 函数
假设 Lua 全局环境中有一个函数 add:
-- config.lua
function add(a, b)
return a + b
end
在 C 中调用它:
// 1. 将函数压栈
// lua_getglobal 从全局表读取变量并压入栈(第 5 章会讲 table 操作)
lua_getglobal(L, "add");
// 2. 压入参数
lua_pushinteger(L, 10);
lua_pushinteger(L, 20);
// 3. 调用: 2 个参数, 1 个返回值
if (lua_pcall(L, 2, 1, 0) == LUA_OK) {
lua_Integer result = lua_tointeger(L, -1);
lua_pop(L, 1); // 弹出返回值
} else {
fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
pcall 参数详解
int lua_pcall(lua_State *L, int nargs, int nresults, int msgh);
| 参数 | 含义 |
|---|---|
nargs | 参数个数(必须在栈顶紧挨着函数) |
nresults | 期望的返回值个数,或 LUA_MULTRET(全部) |
msgh | 错误处理函数的栈索引,0 表示无 |
调用前栈布局:[ ... | function | arg1 | arg2 | ... | argN ](栈顶是最后一个参数)
调用成功后,函数和参数被弹出,返回值留在栈上。
读取 Lua 返回值
如果 Lua 函数返回多个值:
lua_getglobal(L, "multi_return");
lua_pcall(L, 0, LUA_MULTRET, 0);
int nrets = lua_gettop(L); // 计算返回了多少个值
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_dostring(L, "return 1 + 1");
assert(lua_tointeger(L, -1) == 2);
lua_pop(L, 1);
luaL_dostring(L, "function add(a, b) return a + b end");
lua_getglobal(L, "add");
lua_pushinteger(L, 10);
lua_pushinteger(L, 20);
lua_pcall(L, 2, 1, 0);
assert(lua_tointeger(L, -1) == 30);
lua_pop(L, 1);
luaL_dostring(L, "function pair() return 1, 2 end");
lua_getglobal(L, "pair");
lua_pcall(L, 0, LUA_MULTRET, 0);
int nrets = lua_gettop(L);
assert(nrets == 2);
assert(lua_tointeger(L, 1) == 1);
assert(lua_tointeger(L, 2) == 2);
lua_pop(L, nrets);
lua_close(L);
puts("03-call-lua: ok");
return 0;
}
从 Lua 调用 C
将 C 函数暴露给 Lua 是扩展 Lua 的主要方式。这允许你用 C/C++ 编写高性能模块,供 Lua 脚本调用。
C 函数原型
所有可被 Lua 调用的 C 函数必须符合以下签名:
int my_c_function(lua_State *L);
- 参数通过栈传递,
lua_gettop(L)可获取参数个数 - 返回值通过压栈实现,
return n表示返回 n 个值
最小示例
static int c_add(lua_State *L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);
lua_pushinteger(L, a + b);
return 1; // 1 个返回值
}
注册到 Lua
方式一:注册为全局函数
lua_pushcfunction(L, c_add);
lua_setglobal(L, "c_add"); // Lua 中可直接调用 c_add(1,2)
方式二:组织为模块(推荐)
使用 luaL_Reg 数组批量注册:
static const struct luaL_Reg mylib[] = {
{"add", c_add},
{"sub", c_sub},
{NULL, NULL} // 哨兵,必须以此结尾
};
// luaL_newlib 内部会创建 table 并把函数填入(第 5 章会讲 table)
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1; // 返回 table
}
在 Lua 中:
local mylib = require("mylib")
print(mylib.add(10, 20))
对于 require 能正确找到 luaopen_mylib,通常需要将 C 代码编译为共享库(.so / .dll),并放入 Lua 的
package.cpath 搜索路径。第 19 章会讲标准库加载和模块搜索机制。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int c_add(lua_State *L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);
lua_pushinteger(L, a + b);
return 1;
}
static int c_reverse(lua_State *L) {
size_t len;
const char *s = luaL_checklstring(L, 1, &len);
char *buf = malloc(len);
for (size_t i = 0; i < len; i++)
buf[i] = s[len - 1 - i];
lua_pushlstring(L, buf, len);
free(buf);
return 1;
}
static int c_sum(lua_State *L) {
int n = lua_gettop(L);
lua_Number sum = 0;
for (int i = 1; i <= n; i++) {
sum += luaL_checknumber(L, i);
}
lua_pushnumber(L, sum);
return 1;
}
static const struct luaL_Reg mylib[] = {
{"add", c_add}, {"reverse", c_reverse}, {"sum", c_sum}, {NULL, NULL}};
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, c_add);
lua_setglobal(L, "g_add");
luaopen_mylib(L);
lua_setglobal(L, "mylib");
const char *script =
"print('mylib.add(3,4) =', mylib.add(3, 4))\n"
"print('mylib.reverse(\"abc\") =', mylib.reverse('abc'))\n"
"print('mylib.sum(1,2,3,4) =', mylib.sum(1, 2, 3, 4))\n"
"print('g_add(10,20) =', g_add(10, 20))\n";
luaL_dostring(L, script);
lua_close(L);
puts("04-call-c: ok");
return 0;
}
操作 Table
Table 是 Lua 唯一的数据结构,掌握 C API 中的 table 操作至关重要。
创建 Table
lua_newtable(L); // 等价于 lua_createtable(L, 0, 0)
lua_createtable(L, narr, nrec); // 预分配数组部分 narr、哈希部分 nrec
读写字段
假设栈顶是一个 table:
// 读:t[key]
lua_pushstring(L, "name"); // 压入 key
lua_gettable(L, -2); // 查询,弹出 key,结果压栈
// 此时栈顶是 t["name"] 的值
// 写:t[key] = value
lua_pushstring(L, "name");
lua_pushstring(L, "Alice");
lua_settable(L, -3); // 弹出 key 和 value,设置到 table
lua_gettable和lua_settable会触发__index/__newindex元方法。
便捷 API(不触发元方法)
| 操作 | 函数 | 说明 |
|---|---|---|
| 获取整数键值 | lua_geti(L, idx, n) | t[n],结果压栈 |
| 设置整数键值 | lua_seti(L, idx, n) | t[n] = value |
| 获取字符串键值 | lua_getfield(L, idx, k) | t.k,结果压栈 |
| 设置字符串键值 | lua_setfield(L, idx, k, v) | t.k = v |
| 原始获取 | lua_rawget / lua_rawgeti / lua_rawgetp | 绕过元方法 |
| 原始设置 | lua_rawset / lua_rawseti / lua_rawsetp | 绕过元方法 |
遍历 Table
lua_pushnil(L); // 第一个 key
while (lua_next(L, table_index) != 0) {
// 此时栈顶:value (-1), key (-2)
// 处理 key-value...
lua_pop(L, 1); // 弹出 value,保留 key 供下一次迭代
}
lua_next 会触发 __pairs metamethod(如果存在)。如果需要原始遍历,用 lua_rawlen +
整数下标遍历数组部分,或使用
luaH_next的内部逻辑(不推荐)。
数组长度
lua_Integer len = luaL_len(L, -1); // 调用 #t,触发 __len
size_t rawlen = lua_rawlen(L, -1); // 不触发元方法
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_newtable(L);
lua_pushstring(L, "Alice");
lua_setfield(L, -2, "name");
lua_pushinteger(L, 30);
lua_setfield(L, -2, "age");
lua_pushstring(L, "lua");
lua_seti(L, -2, 1);
lua_pushstring(L, "c");
lua_seti(L, -2, 2);
lua_getfield(L, -1, "name");
assert(strcmp(lua_tostring(L, -1), "Alice") == 0);
lua_pop(L, 1);
lua_geti(L, -1, 1);
assert(strcmp(lua_tostring(L, -1), "lua") == 0);
lua_pop(L, 1);
assert(luaL_len(L, -1) == 2);
lua_rawgeti(L, -1, 1);
assert(strcmp(lua_tostring(L, -1), "lua") == 0);
lua_pop(L, 1);
lua_pop(L, 1);
lua_close(L);
puts("05-tables: ok");
return 0;
}
错误处理
Lua 使用异常机制(longjmp)处理错误。在 C 代码中调用 Lua 时,必须注意错误传播,否则会导致程序崩溃。
保护模式调用
lua_pcall
始终用 lua_pcall 代替 lua_call(除非你在一个被 Lua 调用的 C 函数内部,且确定不会出错)。
if (lua_pcall(L, nargs, nresults, msgh) != LUA_OK) {
// 出错
const char *errmsg = lua_tostring(L, -1);
fprintf(stderr, "Lua error: %s\n", errmsg);
lua_pop(L, 1); // 弹出错误信息
}
错误处理函数(msgh)
pcall 的第 4 个参数可以指定一个错误处理函数。当发生错误时,Lua
会先调用这个函数,将原始错误信息传给它,然后将处理函数的返回值作为最终错误信息。
常见用途是打印调用栈:
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_remove(L, -2); // 移除 debug table,保留 traceback 函数
// 此时 traceback 函数在栈顶,其索引就是 -1
if (lua_pcall(L, nargs, nresults, -2) != LUA_OK) { // 注意索引计算
...
}
从 C 报告错误
抛出 Lua 错误
在注册给 Lua 的 C 函数中,如果发现参数错误,应该使用 luaL_error 或 luaL_argerror:
static int myfunc(lua_State *L) {
if (!lua_isinteger(L, 1)) {
return luaL_argerror(L, 1, "expected integer"); // 不会返回
}
// ...
}
格式化错误信息
return luaL_error(L, "bad value at index %d: %s", idx, reason);
luaL_error使用lua_error抛出异常,不会返回到调用者。
C 函数中的保护模式
如果你想在 C 函数内部保护一段代码,使用 lua_pcall 即可。但如果需要在 C 层面做更细粒度的保护,可以使用:
int lua_cpcall(lua_State *L, lua_CFunction func, void *ud);
不过 Lua 5.2+ 更推荐的方式是在 C 函数内直接组织好栈,然后使用 lua_pcall。
常见错误场景
| 场景 | 结果 | 正确做法 |
|---|---|---|
lua_call 在保护模式外调用出错代码 | 程序 longjmp 到未知位置,崩溃 | 使用 lua_pcall |
lua_tostring 对非 string/number 使用 | 返回 NULL | 先用 lua_isstring 检查 |
| 栈索引越界 | 未定义行为 | 用 lua_gettop 确认栈大小 |
lua_tolstring 后修改字符串 | 未定义行为 | 复制到 C 缓冲区后再修改 |
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
static int c_divide(lua_State *L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);
if (b == 0)
return luaL_error(L, "division by zero");
lua_pushinteger(L, a / b);
return 1;
}
static int safe_divide(lua_State *L) {
lua_getglobal(L, "debug");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return luaL_error(L, "debug library not available");
}
lua_getfield(L, -1, "traceback");
lua_remove(L, -2);
lua_pushcfunction(L, c_divide);
lua_pushvalue(L, 1);
lua_pushvalue(L, 2);
if (lua_pcall(L, 2, 1, -4) != LUA_OK) {
return 1;
}
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, c_divide);
lua_setglobal(L, "c_divide");
lua_pushcfunction(L, safe_divide);
lua_setglobal(L, "safe_divide");
luaL_dostring(L, "print(c_divide(10, 3))");
assert(luaL_dostring(L, "print(c_divide('a', 3))") != LUA_OK);
lua_pop(L, 1);
assert(luaL_dostring(L, "print(c_divide(10, 0))") != LUA_OK);
lua_pop(L, 1);
luaL_dostring(L, "print(safe_divide(10, 0))");
lua_close(L);
puts("06-errors: ok");
return 0;
}
Userdata
Userdata 是 Lua 用来在脚本中保存任意 C 数据的类型。它是 C 扩展 Lua 的基石。
两种 Userdata
| 类型 | API | 特点 |
|---|---|---|
| Full userdata | lua_newuserdatauv(L, size, nuvalue) | Lua 分配内存,受 GC 管理 |
| Light userdata | lua_pushlightuserdata(L, p) | 仅保存 C 指针,不受 GC 管理 |
Full Userdata 详解
创建
// 分配一块大小为 sizeof(MyStruct) 的内存
MyStruct *obj = (MyStruct *)lua_newuserdatauv(L, sizeof(MyStruct), 0);
obj->x = 0;
obj->y = 0;
lua_newuserdatauv的第三个参数是关联的 uservalue 数量(Lua 5.4), 用于将 Lua 值与 userdata 关联。此处设为 0 表示不关联。
创建后,userdata 在 Lua 中是一个完整对象,可以赋给变量、放入 table、 作为参数传递。当不再被引用时,Lua GC 会自动回收它。
读写
C 代码通过返回的指针直接读写内存:
// 从 Lua 获取 userdata
MyStruct *obj = (MyStruct *)lua_touserdata(L, 1);
obj->x += 10;
Lua 代码只能持有 userdata 引用,不能直接访问其字段。
通常通过 C 函数来读写(第 8 章会讲 metatable 如何让 userdata 支持 . 访问)。
Light Userdata
用于传递已存在的 C 指针:
void *some_c_ptr = ...;
lua_pushlightuserdata(L, some_c_ptr);
lua_setglobal(L, "c_handle");
注意:Light userdata 不受 GC 管理,生命周期由 C 代码保证。 它也没有独立的元表(所有 light userdata 共享同一个元表)。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
typedef struct {
int count;
} Counter;
static int get_counter(lua_State *L) {
Counter *c = (Counter *)lua_newuserdatauv(L, sizeof(Counter), 0);
c->count = 0;
return 1;
}
static int inc_counter(lua_State *L) {
Counter *c = (Counter *)lua_touserdata(L, 1);
int n = (int)luaL_optinteger(L, 2, 1);
c->count += n;
lua_pushinteger(L, c->count);
return 1;
}
static int get_count(lua_State *L) {
Counter *c = (Counter *)lua_touserdata(L, 1);
lua_pushinteger(L, c->count);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, get_counter);
lua_setglobal(L, "get_counter");
lua_pushcfunction(L, inc_counter);
lua_setglobal(L, "inc_counter");
lua_pushcfunction(L, get_count);
lua_setglobal(L, "get_count");
const char *script = "local c = get_counter()\n"
"print('initial:', get_count(c))\n"
"print('inc 5:', inc_counter(c, 5))\n"
"print('inc 1:', inc_counter(c))\n"
"c = nil\n"
"collectgarbage('collect')\n"
"print('done')\n";
luaL_dostring(L, script);
lua_close(L);
puts("07-userdata: ok");
return 0;
}
Metatable
Metatable 让 C 可以为 userdata 和 table 定义自定义行为(运算符重载、面向对象等)。
常用元方法
| 元方法 | 触发时机 |
|---|---|
__index | 访问不存在的字段 |
__newindex | 给不存在的字段赋值 |
__gc | GC 回收对象时 |
__len | 使用 # 运算符 |
__tostring | tostring() 或 print() |
__eq, __lt, __le | 比较运算符 |
__add, __sub, __mul, __div 等 | 算术运算符 |
__call | 将对象当作函数调用 |
在 C 中设置 Metatable
为 Userdata 设置(最常见)
// 注册 metatable 到注册表
void register_my_meta(lua_State *L) {
luaL_newmetatable(L, "MyType"); // 若不存在则创建
lua_pushcfunction(L, my_tostring);
lua_setfield(L, -2, "__tostring");
lua_pushcfunction(L, my_add);
lua_setfield(L, -2, "__add");
lua_pop(L, 1); // 弹出 metatable
}
// 创建对象时
MyType *obj = lua_newuserdatauv(L, sizeof(MyType), 0);
luaL_getmetatable(L, "MyType");
lua_setmetatable(L, -2);
__index 的两种模式
模式 A:__index 是一个 table
lua_newtable(L); // 方法表
lua_pushcfunction(L, method1);
lua_setfield(L, -2, "method1");
lua_pushcfunction(L, method2);
lua_setfield(L, -2, "method2");
luaL_newmetatable(L, "MyType");
lua_pushvalue(L, -2); // 复制方法表
lua_setfield(L, -2, "__index"); // mt.__index = methods
lua_pop(L, 2);
这是实现面向对象风格 obj:method() 最简洁的方式。
模式 B:__index 是一个函数
适合需要动态计算字段,或字段名不确定的情况。
__gc 元方法(析构)
当 userdata 被 GC 回收时,如果其 metatable 有 __gc 字段,Lua 会调用它。
这是释放 C 资源(如文件句柄、网络连接)的正确位置:
static int mystruct_gc(lua_State *L) {
MyStruct *obj = (MyStruct *)luaL_checkudata(L, 1, "MyStructMeta");
printf("MyStruct %p collected\n", (void *)obj);
return 0;
}
类型安全检查
用 luaL_checkudata 确保参数是指定类型的 userdata:
MyStruct *obj = (MyStruct *)luaL_checkudata(L, 1, "MyStructMeta");
如果类型不匹配,会自动抛出 Lua 错误。
注册表(Registry)
luaL_newmetatable 实际上是在 Lua Registry 中创建一个以字符串为 key 的 table。
Registry 是一个全局的、对 C 代码可见的 table,Lua 脚本无法直接访问
(除非用 debug 库)。它是 C 模块间共享数据的常用机制。
// 手动读写 registry
lua_pushstring(L, "my_key");
lua_gettable(L, LUA_REGISTRYINDEX); // 读取 registry["my_key"]
lua_pushstring(L, "my_key");
lua_pushinteger(L, 42);
lua_settable(L, LUA_REGISTRYINDEX); // registry["my_key"] = 42
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
#define VEC2_MT "Vec2Meta"
typedef struct {
double x, y;
} Vec2;
static Vec2 *check_vec2(lua_State *L, int idx) {
return (Vec2 *)luaL_checkudata(L, idx, VEC2_MT);
}
static int vec2_tostring(lua_State *L) {
Vec2 *v = check_vec2(L, 1);
lua_pushfstring(L, "Vec2(%f, %f)", v->x, v->y);
return 1;
}
static int vec2_add(lua_State *L) {
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
Vec2 *res = (Vec2 *)lua_newuserdatauv(L, sizeof(Vec2), 0);
res->x = a->x + b->x;
res->y = a->y + b->y;
luaL_getmetatable(L, VEC2_MT);
lua_setmetatable(L, -2);
return 1;
}
static int vec2_len(lua_State *L) {
Vec2 *v = check_vec2(L, 1);
lua_pushnumber(L, v->x * v->x + v->y * v->y);
return 1;
}
static int vec2_eq(lua_State *L) {
Vec2 *a = check_vec2(L, 1);
Vec2 *b = check_vec2(L, 2);
lua_pushboolean(L, a->x == b->x && a->y == b->y);
return 1;
}
static int vec2_new(lua_State *L) {
double x = luaL_optnumber(L, 1, 0);
double y = luaL_optnumber(L, 2, 0);
Vec2 *v = (Vec2 *)lua_newuserdatauv(L, sizeof(Vec2), 0);
v->x = x;
v->y = y;
luaL_getmetatable(L, VEC2_MT);
lua_setmetatable(L, -2);
return 1;
}
static int vec2_index(lua_State *L) {
Vec2 *v = check_vec2(L, 1);
const char *key = luaL_checkstring(L, 2);
if (strcmp(key, "x") == 0) {
lua_pushnumber(L, v->x);
return 1;
}
if (strcmp(key, "y") == 0) {
lua_pushnumber(L, v->y);
return 1;
}
luaL_getmetatable(L, VEC2_MT);
lua_pushvalue(L, 2);
lua_rawget(L, -2);
return 1;
}
int luaopen_vec2(lua_State *L) {
luaL_newmetatable(L, VEC2_MT);
lua_pushcfunction(L, vec2_tostring);
lua_setfield(L, -2, "__tostring");
lua_pushcfunction(L, vec2_add);
lua_setfield(L, -2, "__add");
lua_pushcfunction(L, vec2_len);
lua_setfield(L, -2, "__len");
lua_pushcfunction(L, vec2_eq);
lua_setfield(L, -2, "__eq");
lua_pushcfunction(L, vec2_index);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
lua_newtable(L);
lua_pushcfunction(L, vec2_new);
lua_setfield(L, -2, "new");
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaopen_vec2(L);
lua_setglobal(L, "vec2");
const char *script = "local vec2 = vec2\n"
"local a = vec2.new(1, 2)\n"
"local b = vec2.new(3, 4)\n"
"local c = a + b\n"
"print('a =', a)\n"
"print('b =', b)\n"
"print('a + b =', c)\n"
"print('a.x =', a.x)\n"
"print('#a (len_sq) =', #a)\n"
"print('a == b ?', a == b)\n"
"local a2 = vec2.new(1, 2)\n"
"print('a == a2 ?', a == a2)\n";
luaL_dostring(L, script);
lua_close(L);
puts("08-metatable: ok");
return 0;
}
协程(Coroutine)
Lua 的协程是非对称协程(asymmetric coroutine),支持 yield/resume。C API 允许 C
代码创建、恢复和中断 Lua 协程。
核心概念
- Thread:在 Lua C API 中,协程的类型是
LUA_TTHREAD,用lua_State*表示 - 主线程也是一个
lua_State*,由luaL_newstate创建 - 协程有自己的调用栈和全局环境,但共享同一个全局表
创建协程
lua_State *co = lua_newthread(L); // 在当前栈压入 thread,返回其状态机指针
创建后,协程的栈是空的。需要把要运行的函数压入它的栈:
lua_State *co = lua_newthread(L);
lua_getglobal(co, "my_coroutine_func"); // 在 co 的栈上压入函数
恢复(Resume)与让出(Yield)
从 C 恢复协程
int status = lua_resume(co, L, nargs, &nresults);
| 返回值 | 含义 |
|---|---|
LUA_OK | 协程正常结束 |
LUA_YIELD | 协程执行了 coroutine.yield(),可再次 resume |
| 其他 | 出错,错误信息在 co 的栈顶 |
在 C 函数中让出
被 Lua 协程调用的 C 函数也可以让出执行权,但需要使用 C 延续点(C continuation):
static int my_c_func(lua_State *L) {
// ... 做一些工作 ...
return lua_yieldk(L, nresults, ctx, my_continuation);
}
static int my_continuation(lua_State *L, int status, lua_KContext ctx) {
// 协程被 resume 后,从这里继续执行
return 0;
}
在标准 Lua 5.4 中,C 函数中直接调用
lua_yield是允许的(在协程内)。但如果是在主线程中调用,会报错。
典型使用模式
lua_State *co = lua_newthread(L);
lua_getglobal(co, "producer"); // 协程入口函数
int nresults;
while ((status = lua_resume(co, L, 0, &nresults)) == LUA_YIELD) {
// 协程 yield 了一个值,在 co 栈上
printf("yielded: %s\n", lua_tostring(co, -1));
lua_pop(co, nresults); // 清理返回值
// 可以再次 resume
}
if (status != LUA_OK) {
fprintf(stderr, "coroutine error: %s\n", lua_tostring(co, -1));
}
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
const char *script = "function counter(max)\n"
" for i = 1, max do\n"
" coroutine.yield(i)\n"
" end\n"
" return 'done'\n"
"end\n";
luaL_dostring(L, script);
lua_State *co = lua_newthread(L);
lua_getglobal(co, "counter");
lua_pushinteger(co, 5);
int status;
int nresults;
int narg = 1;
int expected = 1;
while ((status = lua_resume(co, L, narg, &nresults)) == LUA_YIELD) {
assert(lua_tointeger(co, -nresults) == expected);
lua_pop(co, nresults);
narg = 0;
expected++;
}
assert(status == LUA_OK);
assert(nresults == 1);
assert(strcmp(lua_tostring(co, 1), "done") == 0);
lua_pop(co, nresults);
lua_pop(L, 1);
lua_close(L);
puts("09-coroutine: ok");
return 0;
}
垃圾回收(GC)
Lua 使用增量式标记-清除垃圾回收器。C 代码可以通过 API 与 GC 交互。
控制 GC
lua_gc(L, LUA_GCCOLLECT, 0); // 执行完整 GC 周期
lua_gc(L, LUA_GCSTOP, 0); // 停止 GC
lua_gc(L, LUA_GCRESTART, 0); // 重启 GC
lua_gc(L, LUA_GCSETSTEPMUL, 200); // 设置步进倍率
lua_gc(L, LUA_GCSETPAUSE, 100); // 设置暂停比率
查询内存使用
int kbytes = lua_gc(L, LUA_GCCOUNT, 0); // 已用内存(KB)
int bytes = lua_gc(L, LUA_GCCOUNTB, 0); // 已用内存的余数(字节)
总字节数 ≈ kbytes * 1024 + bytes
弱引用与 GC
通过 metatable 的 __mode 字段可以创建弱引用 table:
lua_newtable(L); // 创建 table
lua_newtable(L); // 创建其 metatable
lua_pushstring(L, "kv"); // key 和 value 都是弱引用
lua_setfield(L, -2, "__mode");
lua_setmetatable(L, -2); // 设置 metatable
当 key 或 value 不再被其他地方引用时,它们会从弱引用 table 中自动移除。
__gc 与终结器
Full userdata 的 __gc 是 Lua GC 最重要的 C 交互点。注意以下几点:
- resurrection 循环:如果
__gc中将 userdata 重新放入全局表,它会被复活,但下次 GC 不会再调用__gc - 顺序不确定:多个 userdata 的
__gc调用顺序没有保证 - 不要在
__gc中做复杂操作:避免在__gc中抛出错误或创建新对象
C 层面的对象生命周期管理
如果你有一个 C 库管理的外部资源池,可以将 Lua userdata 作为句柄:
typedef struct {
int handle;
} Resource;
static int resource_gc(lua_State *L) {
Resource *r = (Resource *)luaL_checkudata(L, 1, "ResourceMT");
release_handle(r->handle);
return 0;
}
这样 Lua 代码可以像普通对象一样使用资源,而 C 库保证资源最终被释放。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#define BOX_MT "BoxMeta"
typedef struct {
int id;
} Box;
static int box_gc(lua_State *L) {
Box *b = (Box *)luaL_checkudata(L, 1, BOX_MT);
printf("[gc] Box #%d collected\n", b->id);
return 0;
}
static int box_new(lua_State *L) {
int id = (int)luaL_checkinteger(L, 1);
Box *b = (Box *)lua_newuserdatauv(L, sizeof(Box), 0);
b->id = id;
luaL_getmetatable(L, BOX_MT);
lua_setmetatable(L, -2);
return 1;
}
int luaopen_box(lua_State *L) {
luaL_newmetatable(L, BOX_MT);
lua_pushcfunction(L, box_gc);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
lua_newtable(L);
lua_pushcfunction(L, box_new);
lua_setfield(L, -2, "new");
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaopen_box(L);
lua_setglobal(L, "box");
int kbytes = lua_gc(L, LUA_GCCOUNT, 0);
assert(kbytes > 0);
const char *script = "local t = {}\n"
"for i = 1, 5 do\n"
" t[i] = box.new(i)\n"
"end\n"
"print('created 5 boxes')\n"
"for i = 1, 5 do\n"
" t[i] = nil\n"
"end\n"
"collectgarbage('collect')\n"
"print('after gc')\n";
luaL_dostring(L, script);
kbytes = lua_gc(L, LUA_GCCOUNT, 0);
assert(kbytes > 0);
lua_close(L);
puts("10-gc: ok");
return 0;
}
Upvalue 与 C 闭包
Lua C API 中的 lua_CFunction 本身是无状态的。如果希望 C 函数携带私有数据(类似 Lua 闭包中的 upvalue),需要使用
C 闭包(C closure)。
核心 API
void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n);
- 将栈顶的
n个值捕获为 upvalue - 然后压入一个 C 闭包(类型为
LUA_TFUNCTION)
#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i))
- 通过伪索引访问闭包的 upvalue
lua_upvalueindex(1)访问第 1 个 upvalue,lua_upvalueindex(2)访问第 2 个,以此类推
lua_pushcfunction(L, f)只是lua_pushcclosure(L, f, 0)的宏,表示没有 upvalue。
最简单的示例:带前缀的日志函数
static int logger(lua_State *L) {
const char *prefix = lua_tostring(L, lua_upvalueindex(1));
const char *msg = luaL_checkstring(L, 1);
printf("[%s] %s\n", prefix, msg);
return 0;
}
// 注册时
lua_pushstring(L, "ERROR");
lua_pushcclosure(L, logger, 1); // 捕获 1 个 upvalue
lua_setglobal(L, "log_error");
Lua 中调用:
log_error("disk full") -- 输出: [ERROR] disk full
Upvalue 是引用还是拷贝?
Upvalue 在创建闭包时按值捕获。如果 upvalue 是 table,捕获的是 table 引用;如果 upvalue 是
string/number,捕获的是当时的值。后续通过 lua_upvalueindex 读写的是闭包内部保存的那份值。
修改 Upvalue
可以通过 lua_pushvalue + lua_setupvalue 或直接在函数内通过栈操作修改:
static int counter_next(lua_State *L) {
int n = lua_tointeger(L, lua_upvalueindex(1));
n = n + 1;
lua_pushinteger(L, n);
lua_copy(L, -1, lua_upvalueindex(1)); // 写回 upvalue
lua_pop(L, 1);
return 1;
}
lua_copy(L, fromidx, toidx)将fromidx的值复制到toidx,不修改栈高度。
用 Upvalue 实现迭代器
这是 C 闭包最经典的用法之一:
static int iter_next(lua_State *L) {
int i = lua_tointeger(L, lua_upvalueindex(1));
int max = lua_tointeger(L, lua_upvalueindex(2));
if (i > max) return 0; // 结束迭代
lua_pushinteger(L, i);
lua_pushinteger(L, i + 1);
lua_copy(L, -1, lua_upvalueindex(1)); // 更新计数器
lua_pop(L, 1);
return 1;
}
static int make_iter(lua_State *L) {
int max = luaL_checkinteger(L, 1);
lua_pushinteger(L, 1); // upvalue 1: 当前值
lua_pushinteger(L, max); // upvalue 2: 上限
lua_pushcclosure(L, iter_next, 2);
return 1;
}
Lua 中使用:
for x in make_iter(5) do
print(x)
end
与 Lua 闭包的区别
| 特性 | Lua 闭包 | C 闭包 |
|---|---|---|
| upvalue 数量 | 无限制 | 最多 255 个(通常够用) |
| upvalue 类型 | 任意 Lua 值 | 任意 Lua 值 |
| 共享 upvalue | 支持(同一局部变量的多个闭包共享) | 不共享(每个 C 闭包独立拷贝) |
C 闭包的 upvalue 在闭包创建时被复制到闭包内部。即使后续修改了原始栈上的值,已创建的闭包不受影响。
常见模式:模块级别的配置表
// 创建模块时,把整个配置 table 作为 upvalue
lua_newtable(L);
// ... 填充配置 ...
lua_pushcclosure(L, my_function, 1); // 配置表作为 upvalue 1
这样 my_function 内部无需通过全局变量或 registry 查找配置,直接通过 lua_upvalueindex(1)
访问,性能更好,也更封装。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
static int logger(lua_State *L) {
const char *prefix = lua_tostring(L, lua_upvalueindex(1));
const char *msg = luaL_checkstring(L, 1);
printf("[%s] %s\n", prefix, msg);
return 0;
}
static int counter_next(lua_State *L) {
int step = (int)lua_tointeger(L, lua_upvalueindex(1));
int cur = (int)lua_tointeger(L, lua_upvalueindex(2));
int max = (int)lua_tointeger(L, lua_upvalueindex(3));
if (cur > max)
return 0;
lua_pushinteger(L, cur);
lua_pushinteger(L, cur + step);
lua_copy(L, -1, lua_upvalueindex(2));
lua_pop(L, 1);
return 1;
}
static int make_counter(lua_State *L) {
int start = (int)luaL_optinteger(L, 1, 1);
int step = (int)luaL_optinteger(L, 2, 1);
int max = (int)luaL_optinteger(L, 3, 10);
lua_pushinteger(L, step);
lua_pushinteger(L, start);
lua_pushinteger(L, max);
lua_pushcclosure(L, counter_next, 3);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushstring(L, "INFO");
lua_pushcclosure(L, logger, 1);
lua_setglobal(L, "log_info");
lua_pushstring(L, "WARN");
lua_pushcclosure(L, logger, 1);
lua_setglobal(L, "log_warn");
lua_pushcfunction(L, make_counter);
lua_setglobal(L, "make_counter");
const char *script = "log_info('system started')\n"
"log_warn('low memory')\n"
"print('counter 1 to 5:')\n"
"for v in make_counter(1, 1, 5) do\n"
" print(' ', v)\n"
"end\n"
"print('counter 10 to 30 step 5:')\n"
"for v in make_counter(10, 5, 30) do\n"
" print(' ', v)\n"
"end\n";
luaL_dostring(L, script);
lua_close(L);
puts("11-upvalue: ok");
return 0;
}
状态机与内存管理
除了 luaL_newstate() 这种开箱即用的方式,Lua C API 还提供了更底层的状态机控制接口。
自定义内存分配器
luaL_newstate() 内部使用标准 malloc/free。如果你需要自定义内存管理(如使用内存池、追踪泄漏、限制总量),可以使用
lua_newstate:
lua_State *lua_newstate(lua_Alloc f, void *ud);
lua_Alloc 的签名:
typedef void * (*lua_Alloc)(void *ud, void *ptr, size_t osize, size_t nsize);
| 场景 | 参数值 | 行为 |
|---|---|---|
| 分配新块 | ptr=NULL, osize=0, nsize>0 | 分配 nsize 字节 |
| 重新分配 | ptr!=NULL, osize=原大小, nsize>0 | 调整为 nsize 字节 |
| 释放 | ptr!=NULL, nsize=0 | 释放 ptr |
osize 的值还暗示了分配用途(Lua 内部用此做统计):
osize == LUA_TSTRING等:为特定类型分配- 其他值:原始字节数
Extra Space
每个 lua_State 在头部预留了一块额外空间(大小由 LUAI_EXTRASPACE 决定),可通过宏访问:
void *extra = lua_getextraspace(L);
常用于存储与状态机关联的自定义上下文指针,避免全局变量。
查询与替换分配器
lua_Alloc old = lua_getallocf(L, &old_ud);
lua_setallocf(L, my_alloc, my_ud);
栈转移
当有两个 lua_State*(如主线程和协程)时,可以用 lua_xmove 在它们之间转移栈值:
lua_xmove(from, to, n); // 从 'from' 栈顶弹出 n 个值,压入 'to'
lua_xmove 要求两个状态机共享同一个全局表(即由 lua_newthread 创建的协程)。任意两个独立状态机之间不能使用
lua_xmove。
版本查询
lua_Number ver = lua_version(L);
返回 LUA_VERSION_NUM(如 504),可用于运行时版本检查。
Panic 函数
当 Lua 错误发生在没有任何保护帧的情况下(极其严重的编程错误),Lua 会调用 panic 函数:
lua_CFunction old = lua_atpanic(L, my_panic);
默认 panic 函数打印错误信息并调用 abort()。嵌入到游戏引擎或 GUI 程序时,通常需要替换为自定义实现以优雅退出。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static size_t total_allocated = 0;
static size_t total_freed = 0;
static size_t current_used = 0;
static void *my_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
(void)ud;
(void)osize;
if (nsize == 0) {
if (ptr) {
total_freed += osize;
current_used -= osize;
free(ptr);
}
return NULL;
} else {
void *newp = realloc(ptr, nsize);
if (newp && ptr == NULL) {
total_allocated += nsize;
current_used += nsize;
} else if (newp && ptr != NULL) {
current_used = current_used - osize + nsize;
total_allocated += (nsize > osize) ? (nsize - osize) : 0;
}
return newp;
}
}
static int my_panic(lua_State *L) {
const char *msg = lua_tostring(L, -1);
fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n",
msg ? msg : "?");
return 0;
}
int main(void) {
lua_State *L = lua_newstate(my_alloc, NULL);
if (!L) {
fputs("cannot create state\n", stderr);
return 1;
}
lua_atpanic(L, my_panic);
assert(lua_version(L) == 504);
luaL_openlibs(L);
{
int *ctx = (int *)lua_getextraspace(L);
*ctx = 42;
assert(*(int *)lua_getextraspace(L) == 42);
}
size_t before = current_used;
(void)luaL_dostring(L, "local t = {} for i = 1, 1000 do t[i] = i end");
assert(current_used > before);
lua_State *co = lua_newthread(L);
lua_pushstring(L, "hello from main");
lua_xmove(L, co, 1);
assert(strcmp(lua_tostring(co, -1), "hello from main") == 0);
lua_pop(co, 1);
void *ud;
lua_Alloc allocf = lua_getallocf(L, &ud);
assert(allocf == my_alloc);
assert(ud == NULL);
lua_close(L);
assert(total_allocated > 0);
assert(total_freed > 0);
puts("12-state: ok");
return 0;
}
算术、比较与字符串操作
Lua C API 提供了直接在栈上执行算术运算、比较和字符串操作的函数,无需在 Lua 代码中完成。
栈上算术运算
void lua_arith(lua_State *L, int op);
操作数必须从栈顶弹出:
- 一元运算(
UNM、BNOT):弹出 1 个,压入结果 - 二元运算(
ADD、SUB等):弹出 2 个,压入结果
支持的操作码:
| 宏 | 含义 |
|---|---|
LUA_OPADD | 加法 + |
LUA_OPSUB | 减法 - |
LUA_OPMUL | 乘法 * |
LUA_OPMOD | 取模 % |
LUA_OPPOW | 幂 ^ |
LUA_OPDIV | 除法 / |
LUA_OPIDIV | 整除 // |
LUA_OPBAND | 按位与 & |
LUA_OPBOR | 按位或 | |
LUA_OPBXOR | 按位异或 ~ |
LUA_OPSHL | 左移 << |
LUA_OPSHR | 右移 >> |
LUA_OPUNM | 取负 - |
LUA_OPBNOT | 按位非 ~ |
操作数类型必须兼容(整数或浮点),否则会触发错误。在
pcall保护外使用需谨慎。
比较
int lua_compare(lua_State *L, int idx1, int idx2, int op);
int lua_rawequal(lua_State *L, int idx1, int idx2);
| 宏 | 含义 |
|---|---|
LUA_OPEQ | 等于 == |
LUA_OPLT | 小于 < |
LUA_OPLE | 小于等于 <= |
lua_compare触发元方法(__eq、__lt、__le)lua_rawequal不触发元方法,直接比较原始值
返回值:1 表示比较成立,0 表示不成立。
连接
void lua_concat(lua_State *L, int n);
将栈顶 n 个值弹出,连接成一个字符串压回栈顶。如果值是 number,会自动转换为 string。会触发 __concat 元方法。
长度
void lua_len(lua_State *L, int idx);
将 #idx 的结果压入栈顶。对 table 会触发 __len 元方法。对 string 返回字节长度。
字符串转数字
size_t lua_stringtonumber(lua_State *L, const char *s);
将字符串 s 转换为 number/integer 并压入栈。返回转换消耗的字节数(0 表示失败)。
辅助库字符串函数
luaL_tolstring
const char *luaL_tolstring(lua_State *L, int idx, size_t *len);
调用 tostring(idx),将结果压入栈顶并返回字符串指针。与 lua_tolstring 不同,它会触发 __tostring 元方法。
luaL_gsub
const char *luaL_gsub(lua_State *L, const char *s,
const char *p, const char *r);
将字符串 s 中的所有 p 替换为 r,结果压入栈顶并返回指针。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushinteger(L, 10);
lua_pushinteger(L, 3);
lua_arith(L, LUA_OPADD);
assert(lua_tointeger(L, -1) == 13);
lua_pop(L, 1);
lua_pushinteger(L, 10);
lua_pushinteger(L, 3);
lua_arith(L, LUA_OPIDIV);
assert(lua_tointeger(L, -1) == 3);
lua_pop(L, 1);
lua_pushinteger(L, 7);
lua_arith(L, LUA_OPBNOT);
assert(lua_tointeger(L, -1) == -8);
lua_pop(L, 1);
lua_pushinteger(L, 5);
lua_pushinteger(L, 10);
assert(lua_compare(L, -2, -1, LUA_OPLT) == 1);
assert(lua_rawequal(L, -2, -1) == 0);
lua_pop(L, 2);
lua_pushstring(L, "Hello");
lua_pushstring(L, " ");
lua_pushstring(L, "Lua");
lua_concat(L, 3);
assert(strcmp(lua_tostring(L, -1), "Hello Lua") == 0);
lua_pop(L, 1);
lua_pushinteger(L, 42);
lua_pushstring(L, " apples");
lua_concat(L, 2);
assert(strcmp(lua_tostring(L, -1), "42 apples") == 0);
lua_pop(L, 1);
lua_pushstring(L, "hello");
lua_len(L, -1);
assert(lua_tointeger(L, -1) == 5);
lua_pop(L, 2);
lua_newtable(L);
lua_pushinteger(L, 1);
lua_seti(L, -2, 1);
lua_pushinteger(L, 2);
lua_seti(L, -2, 2);
lua_pushinteger(L, 3);
lua_seti(L, -2, 3);
lua_len(L, -1);
assert(lua_tointeger(L, -1) == 3);
lua_pop(L, 2);
size_t n = lua_stringtonumber(L, "3.14159");
assert(n == 8);
assert(lua_tonumber(L, -1) == 3.14159);
lua_pop(L, 1);
n = lua_stringtonumber(L, "not a number");
assert(n == 0);
lua_pushinteger(L, 12345);
size_t len;
const char *s = luaL_tolstring(L, -1, &len);
assert(strcmp(s, "12345") == 0);
assert(len == 5);
lua_pop(L, 2);
s = luaL_gsub(L, "hello world", "world", "lua");
assert(strcmp(s, "hello lua") == 0);
lua_pop(L, 1);
lua_close(L);
puts("13-arith: ok");
return 0;
}
加载与字节码
除了 luaL_loadstring 和 luaL_loadfile,Lua C API
提供了更底层的加载接口,支持从任意来源读取代码、加载预编译字节码、以及将 Lua 函数序列化为字节码。
底层加载:lua_load
int lua_load(lua_State *L, lua_Reader reader, void *data,
const char *chunkname, const char *mode);
| 参数 | 说明 |
|---|---|
reader | 回调函数,每次提供一块源代码/字节码 |
data | 用户数据,传给 reader |
chunkname | 用于错误信息和调试信息的名字 |
mode | "t"(文本)、"b"(字节码)、"bt"(两者) |
返回值:LUA_OK、LUA_ERRSYNTAX、LUA_ERRMEM。
Reader 回调
typedef const char * (*lua_Reader)(lua_State *L, void *ud, size_t *sz);
- 返回指向数据块的指针,并通过
*sz设置大小 - 没有更多数据时返回
NULL
辅助库加载函数
int luaL_loadfilex(lua_State *L, const char *filename, const char *mode);
int luaL_loadbufferx(lua_State *L, const char *buff, size_t sz,
const char *name, const char *mode);
luaL_loadfilex是luaL_loadfile的可指定模式版本luaL_loadbufferx从内存缓冲区加载,可指定模式
出于安全考虑,默认模式通常是
"bt"。如果你只信任文本源码,可以强制"t"。
序列化:lua_dump
int lua_dump(lua_State *L, lua_Writer writer, void *data, int strip);
将栈顶的 Lua 函数序列化为字节码。strip 为真时移除调试信息(减小体积但错误信息会变差)。
Writer 回调
typedef int (*lua_Writer)(lua_State *L, const void *p, size_t sz, void *ud);
返回 0 表示成功,非 0 表示出错(会触发 lua_dump 返回错误码)。
典型应用场景
- 自定义加载器:从加密文件、网络流、内存映射文件加载 Lua 代码
- 字节码缓存:将常用脚本预编译为字节码,运行时直接加载二进制 buffer
- 沙箱:限制只允许加载文本源码(
"t"),防止加载不可信字节码
字节码兼容性
Lua 字节码不保证跨版本兼容。Lua 5.4 的字节码不能直接在 5.3 运行,反之亦然。即使是 5.4 的小版本之间,也建议重新编译。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
static char bytecode[4096];
static size_t bytecode_len = 0;
static int writer(lua_State *L, const void *p, size_t sz, void *ud) {
(void)L;
(void)ud;
if (bytecode_len + sz > sizeof(bytecode)) {
return 1;
}
memcpy(bytecode + bytecode_len, p, sz);
bytecode_len += sz;
return 0;
}
static const char *reader(lua_State *L, void *ud, size_t *sz) {
(void)L;
(void)ud;
if (bytecode_len == 0) {
*sz = 0;
return NULL;
}
*sz = bytecode_len;
bytecode_len = 0;
return bytecode;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
const char *code = "return function(x) return x * 2 end";
luaL_loadbufferx(L, code, strlen(code), "inline", "t");
lua_pcall(L, 0, 1, 0);
lua_pushinteger(L, 21);
lua_pcall(L, 1, 1, 0);
assert(lua_tointeger(L, -1) == 42);
lua_pop(L, 1);
luaL_loadstring(L, "return function(a,b) return a+b end");
lua_pcall(L, 0, 1, 0);
bytecode_len = 0;
lua_dump(L, writer, NULL, 0);
assert(bytecode_len > 0);
lua_pop(L, 1);
lua_load(L, reader, NULL, "from_mem", "b");
lua_pushinteger(L, 10);
lua_pushinteger(L, 32);
lua_pcall(L, 2, 1, 0);
assert(lua_tointeger(L, -1) == 42);
lua_pop(L, 1);
lua_close(L);
puts("14-load: ok");
return 0;
}
引用系统
Lua C API 提供了一套轻量的引用机制,用于在 C 代码中安全地引用 Lua 值,防止其被 GC 回收。
核心概念
Registry 是一个全局 table(只能通过伪索引 LUA_REGISTRYINDEX 访问)。引用系统利用 Registry 中的某个子 table
来保存被引用的值:
int luaL_ref(lua_State *L, int t);
void luaL_unref(lua_State *L, int t, int ref);
| 常量 | 值 | 含义 |
|---|---|---|
LUA_NOREF | -2 | 无效引用 |
LUA_REFNIL | -1 | 引用 nil(不存入 table) |
使用流程
// 1. 把要引用的值压入栈
lua_pushstring(L, "important data");
// 2. 创建引用(值会被弹出或移入 registry)
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
// ... 之后任意时刻 ...
// 3. 通过引用取回值
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
// 栈顶是 "important data"
// 4. 释放引用,允许 GC 回收
luaL_unref(L, LUA_REGISTRYINDEX, ref);
引用表的自动复用
luaL_ref 使用一种简单但高效的策略:
- 引用表的第一个元素(整数 key
0)保存下一个可用的空闲槽位 - 当释放引用时,该槽位被链入空闲列表
- 因此引用值不会无限增长
你可以自己创建任意 table 作为引用表(不一定是 Registry),只要遵守同样的约定即可。
典型应用场景
- C 对象持有 Lua 回调:C 结构体中保存一个
int ref,指向 Lua 函数。需要调用时通过lua_rawgeti取回。 - 反向映射:从 C 指针查找对应的 Lua userdata/table。
- 延迟处理:将事件相关的 Lua 值暂存,等事件触发时再取出。
与全局变量的区别
- 全局变量(
lua_setglobal)用字符串 key,容易被 Lua 脚本意外覆盖 - 引用用整数 key,C 代码独占管理,更安全、更快
直接使用 Registry
除了 luaL_ref,也可以用 light userdata 作为 key 直接操作 Registry,适合存储少量 C 指针或私有数据:
static char MY_KEY; // 静态变量地址在进程内唯一
// 存入
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_pushlightuserdata(L, (void *)my_ptr);
lua_rawset(L, LUA_REGISTRYINDEX);
// 取出
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_rawget(L, LUA_REGISTRYINDEX);
void *my_ptr = lua_touserdata(L, -1);
lua_pop(L, 1);
// 删除
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_pushnil(L);
lua_rawset(L, LUA_REGISTRYINDEX);
这种方式不需要管理整数 ref,key 天然唯一且不会被 Lua 脚本伪造。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
typedef struct {
int ref;
char name[32];
} Callback;
static Callback cb = {LUA_NOREF, "my_callback"};
static int set_callback(lua_State *L) {
if (cb.ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, cb.ref);
}
luaL_checktype(L, 1, LUA_TFUNCTION);
cb.ref = luaL_ref(L, LUA_REGISTRYINDEX);
return 0;
}
static int fire_callback(lua_State *L) {
if (cb.ref == LUA_NOREF || cb.ref == LUA_REFNIL) {
return luaL_error(L, "no callback registered");
}
lua_rawgeti(L, LUA_REGISTRYINDEX, cb.ref);
lua_pushstring(L, "event data");
if (lua_pcall(L, 1, 1, 0) == LUA_OK) {
lua_pop(L, 1);
return 0;
} else {
return lua_error(L);
}
}
static int clear_callback(lua_State *L) {
(void)L;
if (cb.ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, cb.ref);
cb.ref = LUA_NOREF;
}
return 0;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, set_callback);
lua_setglobal(L, "set_callback");
lua_pushcfunction(L, fire_callback);
lua_setglobal(L, "fire_callback");
lua_pushcfunction(L, clear_callback);
lua_setglobal(L, "clear_callback");
const char *script = "set_callback(function(msg)\n"
" print('Lua callback got:', msg)\n"
" return 'ack'\n"
"end)\n"
"fire_callback()\n"
"clear_callback()\n"
"-- 再次注册然后 fire\n"
"set_callback(function(msg) return 'second' end)\n"
"fire_callback()\n"
"clear_callback()\n";
luaL_dostring(L, script);
lua_close(L);
puts("15-ref: ok");
return 0;
}
辅助缓冲区(luaL_Buffer)
当需要在 C 代码中逐步构建一个长字符串时,反复使用 lua_pushstring + lua_concat 效率很低。Lua 辅助库提供了专门的
Buffer 机制。
基本用法
luaL_Buffer b;
luaL_buffinit(L, &b); // 初始化
luaL_addstring(&b, "Hello"); // 追加
luaL_addchar(&b, ' ');
luaL_addstring(&b, "Lua");
luaL_pushresult(&b); // 将结果压入栈
Buffer 内部使用一块局部数组作为初始空间,超出后自动通过 Lua 分配器扩展。
常用 API
| 函数/宏 | 说明 |
|---|---|
luaL_buffinit(L, B) | 初始化 Buffer |
luaL_prepbuffsize(B, sz) | 确保剩余空间 ≥ sz,返回可写入的 char* |
luaL_prepbuffer(B) | 同上,使用默认大小 LUAL_BUFFERSIZE |
luaL_addchar(B, c) | 追加单个字符 |
luaL_addstring(B, s) | 追加 C 字符串 |
luaL_addlstring(B, s, l) | 追加指定长度的字符串 |
luaL_addvalue(B) | 将栈顶值弹出并追加到 buffer |
luaL_pushresult(B) | 完成,将最终字符串压入栈 |
luaL_pushresultsize(B, sz) | 完成并指定结果大小 |
luaL_buffinitsize(L, B, sz) | 初始化并预分配 sz 空间 |
luaL_bufflen(B) | 当前已写入长度 |
luaL_buffaddr(B) | 当前缓冲区的 char* 地址 |
高级用法:直接写入缓冲区
luaL_Buffer b;
char *p = luaL_prepbuffsize(&b, 256);
// 直接向 p 写入最多 256 字节
size_t written = sprintf(p, "value=%d", 42);
luaL_addsize(&b, written); // 通知 buffer 实际写入量
luaL_pushresult(&b);
将栈值追加到 buffer
luaL_addvalue 非常有用,可以将 Lua 值(如 tostring 后的数字)追加:
luaL_Buffer b;
luaL_buffinit(L, &b);
lua_pushinteger(L, 42);
luaL_tolstring(L, -1, NULL); // 转换为 string 压栈
luaL_addvalue(&b); // 弹出并追加
luaL_pushresult(&b);
注意:
luaL_addvalue会弹出栈顶值。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_Buffer b;
luaL_buffinit(L, &b);
luaL_addstring(&b, "Hello");
luaL_addchar(&b, ' ');
luaL_addstring(&b, "World");
luaL_pushresult(&b);
assert(strcmp(lua_tostring(L, -1), "Hello World") == 0);
lua_pop(L, 1);
luaL_buffinit(L, &b);
for (int i = 1; i <= 5; i++) {
char *p = luaL_prepbuffsize(&b, 32);
int n = snprintf(p, 32, "[%d] ", i);
luaL_addsize(&b, (size_t)n);
}
luaL_pushresult(&b);
assert(strcmp(lua_tostring(L, -1), "[1] [2] [3] [4] [5] ") == 0);
lua_pop(L, 1);
luaL_buffinit(L, &b);
luaL_addstring(&b, "values: ");
for (int i = 1; i <= 3; i++) {
lua_pushinteger(L, i * 10);
luaL_tolstring(L, -1, NULL);
luaL_addvalue(&b);
if (i < 3)
luaL_addstring(&b, ", ");
}
luaL_pushresult(&b);
assert(strcmp(lua_tostring(L, -1), "values: 10, 20, 30") == 0);
lua_pop(L, 1);
luaL_buffinit(L, &b);
for (int i = 0; i < 1000; i++) {
luaL_addstring(&b, "x");
}
luaL_pushresult(&b);
size_t len;
lua_tolstring(L, -1, &len);
assert(len == 1000);
lua_pop(L, 1);
lua_close(L);
puts("16-buffer: ok");
return 0;
}
调试接口
Lua 提供了一套完整的 C 级别调试 API,允许宿主程序检查调用栈、设置钩子(hook)、操作局部变量和 upvalue。
Hook 机制
通过 lua_sethook 可以在特定事件发生时执行回调:
void lua_sethook(lua_State *L, lua_Hook f, int mask, int count);
| 事件掩码 | 触发时机 |
|---|---|
LUA_MASKCALL | 调用函数时 |
LUA_MASKRET | 函数返回时 |
LUA_MASKLINE | 执行新行时 |
LUA_MASKCOUNT | 每执行 count 条指令时 |
Hook 函数签名:
typedef void (*lua_Hook)(lua_State *L, lua_Debug *ar);
ar->event
表示当前事件类型:LUA_HOOKCALL、LUA_HOOKRET、LUA_HOOKLINE、
LUA_HOOKCOUNT、LUA_HOOKTAILCALL。
在 hook 中通常只能做安全检查、计数或轻量信息收集,不应执行复杂 Lua 操作。
获取调用栈信息
int lua_getstack(lua_State *L, int level, lua_Debug *ar);
int lua_getinfo(lua_State *L, const char *what, lua_Debug *ar);
lua_getstack填充ar中的i_ci(CallInfo 指针)lua_getinfo根据what字符串查询详细信息
what 字符含义:
| 字符 | 获取信息 |
|---|---|
n | name 和 namewhat(函数名) |
S | source、srclen、linedefined、lastlinedefined、what、short_src |
l | currentline |
u | nups、nparams、isvararg |
t | istailcall |
r | ftransfer、ntransfer(尾调用传值信息) |
L | 将活跃行号压入栈(table) |
f | 将正在运行的函数压入栈 |
Lua 函数的 Upvalue 操作
这与第 11 章的 C 闭包 upvalue 不同,这里操作的是 Lua 函数的 upvalue:
const char *lua_getupvalue(lua_State *L, int funcindex, int n);
const char *lua_setupvalue(lua_State *L, int funcindex, int n);
funcindex指向栈上的 Lua 函数n是 upvalue 索引(从 1 开始)- 返回 upvalue 名称
辅助库:luaL_traceback
void luaL_traceback(lua_State *L, lua_State *L1,
const char *msg, int level);
将 L1 的调用栈回溯信息压入 L 的栈。这是生成错误堆栈最便捷的方式。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
static int hook_calls = 0;
static int hook_lines = 0;
static void my_hook(lua_State *L, lua_Debug *ar) {
(void)L;
if (ar->event == LUA_HOOKCALL)
hook_calls++;
else if (ar->event == LUA_HOOKLINE)
hook_lines++;
}
static int traceback_msgh(lua_State *L) {
luaL_traceback(L, L, lua_tostring(L, 1), 1);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_sethook(L, my_hook, LUA_MASKCALL | LUA_MASKLINE, 0);
luaL_dostring(L, "function fact(n)\n"
" if n <= 1 then return 1 end\n"
" return n * fact(n - 1)\n"
"end\n"
"return fact(5)\n");
assert(hook_calls > 0);
assert(hook_lines > 0);
lua_pop(L, 1);
lua_sethook(L, NULL, 0, 0);
lua_getglobal(L, "fact");
lua_Debug ar;
assert(lua_getinfo(L, ">nuS", &ar) == 1);
assert(ar.linedefined == 1);
assert(ar.nups == 1);
assert(ar.nparams == 1);
assert(ar.isvararg == 0);
lua_pushcfunction(L, traceback_msgh);
luaL_loadstring(L, "function a() b() end\n"
"function b() c() end\n"
"function c() error('boom') end\n"
"a()\n");
assert(lua_pcall(L, 0, 0, -2) != LUA_OK);
assert(strstr(lua_tostring(L, -1), "boom") != NULL);
lua_pop(L, 1);
lua_pop(L, 1);
luaL_dostring(L, "local x = 10\n"
"local f = function() return x end\n"
"return f\n");
const char *name = lua_getupvalue(L, -1, 1);
assert(name != NULL);
assert(strcmp(name, "x") == 0);
assert(lua_tointeger(L, -1) == 10);
lua_pop(L, 1);
lua_pushinteger(L, 99);
name = lua_setupvalue(L, -2, 1);
assert(name != NULL);
lua_pcall(L, 0, 1, 0);
assert(lua_tointeger(L, -1) == 99);
lua_pop(L, 1);
if (lua_gettop(L) > 0 && lua_isfunction(L, -1))
lua_pop(L, 1);
lua_close(L);
puts("17-debug: ok");
return 0;
}
警告系统与杂项
Lua 5.4 引入了警告系统,允许 Lua 核心和 C 代码发出非致命警告信息。此外还有一些零散的实用 API。
警告系统
设置警告处理函数
void lua_setwarnf(lua_State *L, lua_WarnFunction f, void *ud);
lua_WarnFunction 签名:
typedef void (*lua_WarnFunction)(void *ud, const char *msg, int tocont);
tocont == 0:这是一条完整警告的结束tocont != 0:后续还有片段,应继续拼接
发出警告
void lua_warning(lua_State *L, const char *msg, int tocont);
C 代码或绑定库可以用此向宿主程序报告非致命问题。
默认行为(未设置警告函数时)警告被忽略。嵌入到 IDE 或游戏引擎时,建议设置自定义函数将警告路由到日志系统。
字符串转数字(栈操作)
size_t lua_stringtonumber(lua_State *L, const char *s);
将字符串 s 解析为 Lua number/integer 并压入栈。返回成功解析的字符数(0 表示失败)。
关闭槽位(to-be-closed)
Lua 5.4 支持 <close> 变量。C API 中对应:
void lua_toclose(lua_State *L, int idx);
void lua_closeslot(lua_State *L, int idx);
lua_toclose将栈位置idx标记为 to-be-closed。当该位置被弹出(如作用域结束)时,如果值有__close元方法,Lua 会调用它。lua_closeslot强制立即关闭指定槽位。
典型用途:在 C 函数中创建需要确保清理的资源对象(如临时锁、事务句柄),即使后续发生错误也能正确释放。
分配器查询与替换
lua_Alloc lua_getallocf(lua_State *L, void **ud);
void lua_setallocf(lua_State *L, lua_Alloc f, void *ud);
用于在运行时替换内存分配器。例如,某些场景下需要临时切换为带统计的分配器。
C 栈深度限制
int lua_setcstacklimit(lua_State *L, unsigned int limit);
设置 C 调用栈的使用上限(以字节计)。防止 C 代码递归过深导致栈溢出。返回之前的限制值。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>
static char warn_buffer[256];
static int warn_len = 0;
static void my_warn(void *ud, const char *msg, int tocont) {
(void)ud;
size_t n = strlen(msg);
if ((size_t)warn_len + n < sizeof(warn_buffer)) {
memcpy(warn_buffer + warn_len, msg, n);
warn_len += (int)n;
}
if (!tocont) {
warn_buffer[warn_len] = '\0';
printf("[WARN] %s\n", warn_buffer);
warn_len = 0;
}
}
static int close_resource(lua_State *L) {
const char *name = lua_tostring(L, 1);
printf("[close] resource '%s' is being closed\n", name);
return 0;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_setwarnf(L, my_warn, NULL);
lua_warning(L, "something", 1);
lua_warning(L, " odd happened", 0);
(void)luaL_dostring(L, "@on");
(void)luaL_dostring(L, "warn('deprecated API used')");
(void)luaL_dostring(L, "@off");
size_t n = lua_stringtonumber(L, "42");
assert(n == 3);
assert(lua_tointeger(L, -1) == 42);
lua_pop(L, 1);
n = lua_stringtonumber(L, "3.14extra");
assert(n == 0);
lua_newtable(L);
lua_pushstring(L, "my_resource");
lua_setfield(L, -2, "name");
lua_newtable(L);
lua_pushcfunction(L, close_resource);
lua_setfield(L, -2, "__close");
lua_setmetatable(L, -2);
lua_toclose(L, -1);
lua_pop(L, 1);
void *ud;
lua_Alloc allocf = lua_getallocf(L, &ud);
assert(allocf != NULL);
lua_close(L);
puts("18-warn: ok");
return 0;
}
标准库加载
Lua 的核心解释器本身只提供虚拟机、垃圾回收和 C API。所有内置函数(print、math.sin、string.format
等)都来自标准库,由独立的 C 模块实现。
lualib.h
头文件 lualib.h 声明了所有标准库的打开函数:
| 函数 | 提供的 Lua 模块/全局 |
|---|---|
luaopen_base(L) | _G、assert、print、type、pairs 等基础函数 |
luaopen_coroutine(L) | coroutine |
luaopen_table(L) | table |
luaopen_io(L) | io |
luaopen_os(L) | os |
luaopen_string(L) | string |
luaopen_utf8(L) | utf8 |
luaopen_math(L) | math |
luaopen_debug(L) | debug |
luaopen_package(L) | package、require |
打开全部标准库
#include <lualib.h>
luaL_openlibs(L); // 打开所有标准库
这等价于依次调用上述所有 luaopen_* 函数。
按需加载
在沙箱或内存受限环境中,通常只加载必要的库:
luaopen_base(L);
luaopen_math(L);
luaopen_string(L);
// 不加载 io/os/debug,限制文件系统和环境访问
自定义 package 搜索器
luaopen_package 注册了 require 机制和默认搜索路径。你可以:
- 修改
package.path/package.cpath指向自定义目录 - 插入自定义搜索器到
package.searchers中 - 完全替换
require的实现(手动注册同名全局函数)
预加载模块
lauxlib.h 中定义了 Registry key:
#define LUA_PRELOAD_TABLE "_PRELOAD"
可以通过在预加载表中注册函数,让 require 在未找到文件时调用你的加载器:
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
lua_pushcfunction(L, my_custom_loader);
lua_setfield(L, -2, "mymodule");
lua_pop(L, 1);
之后在 Lua 中 require("mymodule") 就会调用 my_custom_loader。
完整代码
#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
static int open_mymodule(lua_State *L) {
lua_newtable(L);
lua_pushstring(L, "hello from preload");
lua_setfield(L, -2, "msg");
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaopen_base(L);
luaopen_math(L);
lua_setglobal(L, "math");
luaopen_string(L);
lua_setglobal(L, "string");
luaL_dostring(L, "print('math.pi =', math.pi)");
luaL_dostring(L, "print('string.upper =', string.upper('hi'))");
luaL_dostring(L, "print(io)");
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
lua_pushcfunction(L, open_mymodule);
lua_setfield(L, -2, "mymodule");
lua_pop(L, 1);
luaopen_package(L);
luaL_dostring(L, "local m = require('mymodule')\n"
"print('mymodule.msg =', m.msg)\n");
luaL_openlibs(L);
luaL_dostring(L, "print('os.time =', os.time())");
lua_close(L);
puts("19-stdlib: ok");
return 0;
}