Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

引用系统

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 3(即 LUA_RIDX_LAST + 1,内部称为 freelist)处保存下一个可用的空闲槽位
  • 当释放引用时,该槽位被链入空闲列表
  • 因此引用值不会无限增长

你可以自己创建任意 table 作为引用表(不一定是 Registry),只要遵守同样的约定即可。

典型应用场景

  1. C 对象持有 Lua 回调:C 结构体中保存一个 int ref,指向 Lua 函数。需要调用时通过 lua_rawgeti 取回。
  2. 反向映射:从 C 指针查找对应的 Lua userdata/table。
  3. 延迟处理:将事件相关的 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;
}