零、前言
在前面的文章中,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" ,下面便是展示如何将值存入到注册表中。
cpp
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 的操作并没有太大的区别,只是将索引替换为注册表的伪索引即可。
最后输出为:
cpp
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 值。
cpp
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);
运行后输出内容如下:
makefile
引用系统创建唯一的键: 4
name: jiang pengyong
重新申请一个 key : 4
值得注意
- 如果将 nil 作为值调用
luaL_ref
都不会创建新的引用,都会返回一个常量引用LUA_REFNIL
。
cpp
// 在 lauxlib.h 文件中
#define LUA_REFNIL (-1)
- 如果将
LUA_REFNIL
进行释放不会有什么作用,例如下面的代码不会有任何作用。
cpp
// 不会有任何的作用
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
- 如使用
LUA_REFNIL
进行获取注册表的值,会导致压入一个 nil 值到栈中。
cpp
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
- 引用系统定义了一个
LUA_NOREF
,表示无效的引用。
cpp
// 在 lauxlib.h
#define LUA_NOREF (-2)
- 创建 lua 状态时,注册表中有两个预定义的引用:
LUA_RIDX_MAINTHREAD
指向 Lua 状态本身,也就是主线程。LUA_RIDX_GLOBALS
指向全局变量。
cpp
#define LUA_RIDX_MAINTHREAD 1
#define LUA_RIDX_GLOBALS 2
通过以下代码可以更加具体的感受到。
cpp
printf("获取 LUA_RIDX_GLOBALS 值\n");
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
stackDump(L);
lua_pop(L, 1);
printf("获取 LUA_RIDX_MAINTHREAD 值\n");
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
stackDump(L);
lua_pop(L, 1);
输出内容如下
arduino
获取 LUA_RIDX_GLOBALS 值
栈顶
^ typename: table, value: table
栈底
获取 LUA_RIDX_MAINTHREAD 值
栈顶
^ typename: thread, value: thread
栈底
4、使用 C 语言静态变量做唯一 key
这种方法也可以实现在注册表中创建唯一的 key 。只是这种方式需要借助 lua_pushlightuserdata
函数将一个 C 语言指针压入到栈中。
以下的代码进行展示如何使用。
cpp
lua_State *L = luaL_newstate();
static char Key = 'k';
// 保存字符串
// 注册表[(void *) &Key] = "江澎涌"
lua_pushlightuserdata(L, (void *) &Key);
lua_pushstring(L, "江澎涌");
lua_settable(L, LUA_REGISTRYINDEX);
// 获取字符串
lua_pushlightuserdata(L, (void *) &Key);
lua_gettable(L, LUA_REGISTRYINDEX);
printf("注册表[(void *) &Key] = %s", lua_tostring(L, -1));
lua_close(L);
可以看到,将通过 lua_pushlightuserdata
将静态变量指针压入栈作为 key ,然后设置 value ,最后对注册表的设置、获取操作和普通表的操作是一样的。
输出如下所示。
arduino
注册表[(void *) &Key] = 江澎涌
Lua 为了简化这种操作,提供了两个 C-API :
cpp
void (lua_rawsetp) (lua_State *L, int idx, const void *p);
int (lua_rawgetp) (lua_State *L, int idx, const void *p);
参数 p 是需要压入的静态变量指针,上面代码的操作就可以减少一步,具体代码如下,运行后的效果是一致的
cpp
lua_State *L = luaL_newstate();
static char Key = 'k';
// 保存字符串
// 注册表[(void *) &Key] = "江澎涌"
lua_pushstring(L, "江澎涌");
lua_rawsetp(L, LUA_REGISTRYINDEX, (void *) &Key);
// 获取字符串
lua_rawgetp(L, LUA_REGISTRYINDEX, (void *) &Key);
printf("注册表[(void *) &Key] = %s", lua_tostring(L, -1)); --> 注册表[(void *) &Key] = 江澎涌
lua_close(L);
值得一提
注册表没有元表 ,lua_rawsetp
和 lua_rawgetp
都是原始访问,效率会比普通访问快一些。
二、上值
每一次在 Lua 中创建新的 C 函数时,可以将任意数量的上值和这个函数相关联,而每个上值都可以保存一个 Lua 值。后面调用该函数时,可以通过伪索引自由的访问这些上值。这种 C 函数与其上值的关联称为闭包。
上值的个数有限制, C 语言函数中最多可以有 255 个上值,lua_upvalueindex 的最大索引值是 256 。
1、如何为闭包添加上值
这里结合一个例子讲解,我们创建一个 C++ 函数给 Lua 调用,这个 C++ 函数是一个累加的闭包,每次调用都会产生新的值,而且每个闭包的值互不影响。
第一步,创建累加的 C 函数。
- 函数
newCounter
:创建闭包的函数,每次 Lua 中调用newCounter
函数,都会返回一个新的闭包,会携带新的上值。 - 函数
counter
:真正的执行函数,每次 Lua 执行newCounter
创建后的闭包都会运行该函数,该函数会 "更新上值以便下次使用" 和 "返回更新后的上值"。
cpp
static int counter(lua_State *L) {
// 通过 lua_upvalueindex 获取第一个上值的伪索引,并通过 lua_tointeger 获取到对应的值
int value = lua_tointeger(L, lua_upvalueindex(1));
// 将数值累加后压栈,后续可以用作 "更新上值" 和 "返回"
lua_pushinteger(L, ++value);
// 通过 lua_upvalueindex 获取第一个上值的伪索引
// 并通过 lua_copy 将索引为 -1 的值拷贝到第一个上值
lua_copy(L, -1, lua_upvalueindex(1));
// 返回累加后的数值
return 1;
}
int newCounter(lua_State *L) {
// 压入初始上值,即整型数值 0
lua_pushinteger(L, 0);
// 压入闭包函数 counter , 并携带一个上值
// counter 函数则可以获取到该上值
lua_pushcclosure(L, &counter, 1);
// 返回一个闭包,该闭包的执行体是 counter ,上值为一个整型数值
return 1;
}
static const struct luaL_Reg luaLReg[] = {
{"newCounter", newCounter},
{nullptr, nullptr}
};
第二步:将上述函数以库的形式添加到 Lua 中
在之前的 《Lua 调用 C 模块中的函数》 文章中已经详细的分享了如何编写和调用自己编写的库,这里就不再赘述。
cpp
lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 创建一个 lib
luaL_newlib(L, luaLReg);
// Counter = lib
lua_setglobal(L, "Counter");
// ... 运行 Lua 脚本
第三步:编写 Lua 文件,并运行
Lua 中创建两个累加器,a 累加器累加三次,b 累加器只累加一次,可以从运行结果看出,两个累加器互不影响。
lua
local a = Counter.newCounter()
local b = Counter.newCounter()
print("a", a(), a(), a())
print("b", b())
运行代码
cpp
std::string fileName = PROJECT_PATH + "/9、C函数中保存状态/上值/上值.lua";
if (luaL_loadfile(L, fileName.c_str()) || lua_pcall(L, 0, 0, 0)) {
error(L, "can't run config. file: %s", lua_tostring(L, -1));
}
lua_close(L);
运行结果
css
a 1 2 3
b 1
2、lua_pushcclosure
cpp
void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
描述:
将一个 C 函数作为闭包压入 Lua 栈中。
参数:
- 参数 L: Lua 状态机指针。
- 参数 fn: 一个 C 函数指针,表示要作为闭包压入 Lua 栈的函数。
- 参数 n: 一个整数,表示闭包所使用的上值数量。
3、lua_upvalueindex
cpp
#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i))
描述:
他是一个宏,用于获取闭包的上值在 Lua 栈中的索引。这个索引是一个伪索引,他和其他的栈索引一样,唯一的区别在于它不存在于栈中。
参数:
- 参数 i : 表示要获取的上值的索引,第一个上值的索引为 1 。
返回值:
返回值是一个整数,表示该上值在 Lua 栈中的索引。
如果传入的索引超出了范围,则会返回 LUA_TNONE
。可以使用 lua_isnone
函数进行判断上值的伪索引是否合法,例如下面代码:
cpp
lua_isnone(L, lua_upvalueindex(i))
三、共享上值
在封装一个库给 Lua 进行调用的时候,有时需要库函数间共享一些数值或变量。虽然注册表也可以达到这一目的,但不够优雅,毕竟将库的过多细节暴露,并且存在 key 被覆盖的风险,所以更好的做法应该是使用上值。
Lua 为这种场景提供了一种解决方案,简化在库函数间共享上值的做法。
之前我们创建一个 Lua 库时,一般都使用 luaL_newlib
函数,他其实是个宏,具体实现如下所示。
cpp
#define luaL_newlib(L,l) \
(luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
要达到共享上值的关键点在于 luaL_setfuncs
的三个参数,用 luaL_newlib
创建库,默认情况下传递的都是 0 ,即没有共享上值,所以我们只需要改造下就可以支持了。
1、举个例子
定义一个库,这个库中有两个函数,共享三个上值,分别为整数型、字符串、table,两个函数均可对共享上值获取和修改
第一步,定义库函数和设置库的共享上值。
showInfo
和 showCode
是两个库函数,会获取各自的参数和共享上值,进行操作后返回给 Lua
共享上值的关键点在 luaopen_user
函数,函数中会压入三个值,然后调用 luaL_setfuncs
将这三个值作为库的共享上值。
cpp
int showInfo(lua_State *L) {
// 获取 lua 传过来的第一个参数
std::string tip = lua_tostring(L, 1);
// 获取 lua 传过来的第二个参数
long long value = lua_tointeger(L, 2);
printf("【showInfo】C++ 函数获取到 Lua 传递的参数:%s, %lld\n", tip.c_str(), value);
// 获取第一个上值,整数型
long long age = lua_tointeger(L, lua_upvalueindex(1));
// 获取第二个上值,字符串
std::string name = lua_tostring(L, lua_upvalueindex(2));
printf("【showInfo】C++ 函数获取到上值:%lld, %s\n", age, name.c_str());
// 给第三个上值 table 设置值 table["up"] = "up value jiang"
lua_pushstring(L, "up value jiang");
lua_setfield(L, lua_upvalueindex(3), "up");
lua_pushstring(L, name.c_str());
lua_pushinteger(L, age);
// 将参数和上值拼凑后作为返回值
lua_concat(L, 4);
return 1;
}
int showCode(lua_State *L) {
// 获取 lua 传过来的第一个参数
std::string value = lua_tostring(L, 1);
printf("【showCode】C++ 函数获取到 Lua 传递的参数:%s\n", value.c_str());
// 获取第一个上值,整数型
long long age = lua_tointeger(L, lua_upvalueindex(1));
// 获取第二个上值,字符串
std::string name = lua_tostring(L, lua_upvalueindex(2));
// 获取第三个上值 table 中 key 为 "up" 的 value
// table["up"]
lua_getfield(L, lua_upvalueindex(3), "up");
std::string up = lua_tostring(L, 1);
printf("【showInfo】C++ 函数获取到上值:%lld, %s, %s\n", age, name.c_str(), up.c_str());
lua_pushstring(L, name.c_str());
lua_pushinteger(L, age);
lua_pushstring(L, up.c_str());
// 将参数和上值拼凑后作为返回值
lua_concat(L, 4);
return 1;
}
static const struct luaL_Reg user[] = {
{"showInfo", showInfo},
{"showCode", showCode},
{nullptr, nullptr}
};
int luaopen_user(lua_State *L) {
// 创建一个 lib
luaL_newlibtable(L, user);
// 压入上值
lua_pushnumber(L, 28);
lua_pushstring(L, "江澎涌");
lua_newtable(L);
// 设置函数
luaL_setfuncs(L, user, 3);
lua_setglobal(L, "User");
return 1;
}
第二步,编写 lua 文件。
只是简单的调用库函数,然后打印返回值。
lua
print(User.showInfo("hi", 80000))
print(User.showCode("hello"))
第三步,将库函数添加到 Lua 中,并运行 Lua 文件。
cpp
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaopen_user(L);
std::string fileName = PROJECT_PATH + "/9、C函数中保存状态/共享上值/共享上值.lua";
if (luaL_loadfile(L, fileName.c_str()) || lua_pcall(L, 0, 0, 0)) {
error(L, "can't run config. file: %s", lua_tostring(L, -1));
}
lua_close(L);
运行结果如下。
【showInfo】C++ 函数获取到 Lua 传递的参数:hi, 80000
【showInfo】C++ 函数获取到上值:28, 江澎涌
hi80000江澎涌28
【showCode】C++ 函数获取到 Lua 传递的参数:hello
【showInfo】C++ 函数获取到上值:28, 江澎涌, hello
up value jiang江澎涌28hello
四、注册表和上值的区别
注册表提供了全局变量的概念,只要获取到相同的 lua_State ,就能通过 lua_State 访问到相同的注册表,通过对应的 key 获取需要的 value 。
上值则实现了一种只能在特定的函数中可见的机制,相较于注册表能做到更小粒度的共享值。
五、写在最后
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。