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;
}