零、前言
在前面的文章中,C 函数操作的数据的生命周期都是在该函数执行期间。有时我们需要保存一些非局部数据,虽然在 C 语言中,我们可以使用全局变量或静态变量来满足非局部变量的持有,但是当我们需要使用 Lua 编写库函数时,就会遇到一些问题:
- C 语言中无法保存普通的 Lua 值。
- 如果 Lua 库函数中使用了全局变量或静态变量来保存一些数据,会导致该库在多个 lua_State 中使用受到约束。(因为每个 lua_State 间是相互独立的,而 C 函数中使用的全局变量和静态变量却是共用的,这里会出现数据混乱问题。)
因此 Lua 提供了 C-API ,让 C 语言函数有两个地方可以存储非局部数据:
- 注册表
- 上值
经过前面文章的学习,可以知道 Lua 内部存储 “非局部数据” ,则通过 “全局变量” 和 “非局部变量” 。
一、注册表
注册表是一张能被 C 代码访问的全局表。 注册表总是位于伪索引 LUA_REGISTRYINDEX
中。
Lua 中接收索引作为参数的 C-API 都可以接收这个伪索引 LUA_REGISTRYINDEX
。只是有一些需要除外,即对栈操作的 C-API 则不可以,例如:lua_remove
、lua_insert
等。
1、如何操作注册表
对注册表的操作,可以认为是对普通表 table 操作。
- 可以使用对 table 设置或获取 value 操作的 C-API。
- 和 table 的约束一样,不能用 nil 作为 key。但注册表更为严格,不能使用数值类型作为 key ,因为 Lua 将数值类型作为引用系统的保留字。
因为注册表是全局使用的一个 table ,不同模块均可以使用他,所以在 “注册表” 中使用一个独一无二的 key 较为关键,这样才不会被其他模块覆盖,导致数据问题。
有几种方式可以实现键的不同:
- 模块内使用的 key 值,均增加模块名作为前缀。例如一个 “3D-Graphics” 的模块,可以将他的 key 取为 “3D-Graphics-xxx” 。
- 使用引用系统辅助库为我们生成唯一 key ,这个 key 则为整数类型,这就是上面讲到不能自行创建数值类型 key 的原因。
- 使用 C 代码中静态变量的地址,这样也可以达到全局的唯一。
下面遍一一举例子进行分享
2、模块中使用自定义 key 值
假设我们要编写一个 “3D 渲染” 相关的库,将它命名为 “3D-Graphics” 。在这个模块中,按照约定所有存入注册表中的自定义 key 值都需要带上 “3D-Graphics-” 前缀。
这个约定只是笔者建议的一种 “尽可能” 避免 key 值冲突的解决方案,并非约束也不是避免 key 冲突的绝对方案。因为有可能存在相同名字的模块,或是有些模块不遵循导致恰巧用了相同的 key 。
假设我们需要将值存入到 key 为 “xxx” ,则在注册表的名为 “3D-Graphics-xxx” ,下面便是展示如何将值存入到注册表中。
lua_State *L = luaL_newstate();
const char *key = "3D-Graphics-xxx";
lua_pushstring(L, "江澎涌");
lua_setfield(L, LUA_REGISTRYINDEX, key);
lua_getfield(L, LUA_REGISTRYINDEX, key);
printf("content: %s\n", lua_tostring(L, -1));
lua_close(L);
可以发现,其实对注册表的操作和对 table 的操作并没有太大的区别,只是将索引替换为注册表的伪索引即可。
最后输出为:
content: 江澎涌
3、使用引用系统生成索引
为了避免冲突,Lua 提供了一套引用系统,为此 Lua 引入了两个 C-API 函数。
函数 | 描述 |
---|---|
int (luaL_ref) (lua_State *L, int t); | 会将栈顶的一个值弹出,作为 value,然后分配一个唯一的整型数作为 key ,以 “注册表[key]=value” 的形式保存,最后将整型数 key 返回,后续的更新、获取和移除都使用该返回的 key 进行操作。 |
void (luaL_unref) (lua_State *L, int t, int ref); | 释放 value 和 key ,会将 key 回收,后续 luaL_ref 可以重新利用回收的 key ,同时清空 value 。 |
举个例子
- 向注册表的申请一个索引同时存入值,然后获取该值,最后释放。
- 再次申请,会发现会分配同一个 key 值。
lua_State *L = luaL_newstate();
lua_pushstring(L, "jiang pengyong");
// 将栈顶弹出然后设置值
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
printf("引用系统创建唯一的键: %d\n", ref);
// 获取值
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
printf("name: %s\n", lua_tostring(L, -1));
// 释放引用
luaL_unref(L, LUA_REGISTRYINDEX, ref);
// 释放后在生成,又使用了同一个引用
lua_pushstring(L, "jiang pengyong!!!\n");
int ref1 = luaL_ref(L, LUA_REGISTRYINDEX);
printf("重新申请一个 key : %d\n", ref1);
lua_close(L);
运行后输出内容如下:
引用系统创建唯一的键: 4
name: jiang pengyong
重新申请一个 key : 4
值得注意
- 如果将 nil 作为值调用
luaL_ref
都不会创建新的引用,都会返回一个常量引用LUA_REFNIL
。
// 在 lauxlib.h 文件中
#define LUA_REFNIL (-1)
- 如果将
LUA_REFNIL
进行释放不会有什么作用,例如下面的代码不会有任何作用。
// 不会有任何的作用
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
- 如使用
LUA_REFNIL
进行获取注册表的值,会导致压入一个 nil 值到栈中。
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
- 引用系统定义了一个
LUA_NOREF
,表示无效的引用。
// 在 lauxlib.h
#define LUA_NOREF (-2)
- 创建 lua 状态时,注册表中有两个预定义的引用:
LUA_RIDX_MAINTHREAD
指向 Lua 状态本身,也就是主线程。LUA_RIDX_GLOBALS
指向全局变量。