C 函数中如何保存 Lua 的数据

零、前言

在前面的文章中,C 函数操作的数据的生命周期都是在该函数执行期间。有时我们需要保存一些非局部数据,虽然在 C 语言中,我们可以使用全局变量或静态变量来满足非局部变量的持有,但是当我们需要使用 Lua 编写库函数时,就会遇到一些问题:

  1. C 语言中无法保存普通的 Lua 值。
  2. 如果 Lua 库函数中使用了全局变量或静态变量来保存一些数据,会导致该库在多个 lua_State 中使用受到约束。(因为每个 lua_State 间是相互独立的,而 C 函数中使用的全局变量和静态变量却是共用的,这里会出现数据混乱问题。)

因此 Lua 提供了 C-API ,让 C 语言函数有两个地方可以存储非局部数据:

  1. 注册表
  2. 上值

经过前面文章的学习,可以知道 Lua 内部存储 “非局部数据” ,则通过 “全局变量” 和 “非局部变量” 。

一、注册表

注册表是一张能被 C 代码访问的全局表。 注册表总是位于伪索引 LUA_REGISTRYINDEX 中。

Lua 中接收索引作为参数的 C-API 都可以接收这个伪索引 LUA_REGISTRYINDEX 。只是有一些需要除外,即对栈操作的 C-API 则不可以,例如:lua_removelua_insert 等。

1、如何操作注册表

对注册表的操作,可以认为是对普通表 table 操作。

  • 可以使用对 table 设置或获取 value 操作的 C-API。
  • 和 table 的约束一样,不能用 nil 作为 key。但注册表更为严格,不能使用数值类型作为 key ,因为 Lua 将数值类型作为引用系统的保留字。

因为注册表是全局使用的一个 table ,不同模块均可以使用他,所以在 “注册表” 中使用一个独一无二的 key 较为关键,这样才不会被其他模块覆盖,导致数据问题。

有几种方式可以实现键的不同:

  1. 模块内使用的 key 值,均增加模块名作为前缀。例如一个 “3D-Graphics” 的模块,可以将他的 key 取为 “3D-Graphics-xxx” 。
  2. 使用引用系统辅助库为我们生成唯一 key ,这个 key 则为整数类型,这就是上面讲到不能自行创建数值类型 key 的原因。
  3. 使用 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 。

举个例子

  1. 向注册表的申请一个索引同时存入值,然后获取该值,最后释放。
  2. 再次申请,会发现会分配同一个 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

值得注意

  1. 如果将 nil 作为值调用 luaL_ref 都不会创建新的引用,都会返回一个常量引用 LUA_REFNIL
// 在 lauxlib.h 文件中
#define LUA_REFNIL      (-1)
  1. 如果将 LUA_REFNIL 进行释放不会有什么作用,例如下面的代码不会有任何作用。
// 不会有任何的作用
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
  1. 如使用 LUA_REFNIL 进行获取注册表的值,会导致压入一个 nil 值到栈中。
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
  1. 引用系统定义了一个 LUA_NOREF ,表示无效的引用。
// 在 lauxlib.h
#define LUA_NOREF       (-2)
  1. 创建 lua 状态时,注册表中有两个预定义的引用:
  • LUA_RIDX_MAINTHREAD 指向 Lua 状态本身,也就是主线程。
  • LUA_RIDX_GLOBALS 指向全局变量。
#define LUA_RIDX_MAINTHREAD	1
#define LUA_RIDX_GLOBALS	2

通过以下代码可以更加具体的感受到。

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

输出内容如下

获取 LUA_RIDX_GLOBALS 值
栈顶
^ typename: table, value: table    
栈底

获取 LUA_RIDX_MAINTHREAD 值
栈顶
^ typename: thread, value: thread    
栈底

4、使用 C 语言静态变量做唯一 key

这种方法也可以实现在注册表中创建唯一的 key 。只是这种方式需要借助 lua_pushlightuserdata 函数将一个 C 语言指针压入到栈中。

以下的代码进行展示如何使用。

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 ,最后对注册表的设置、获取操作和普通表的操作是一样的。

输出如下所示。

注册表[(void *) &Key] = 江澎涌

Lua 为了简化这种操作,提供了两个 C-API :

void  (lua_rawsetp) (lua_State *L, int idx, const void *p);

int (lua_rawgetp) (lua_State *L, int idx, const void *p);

参数 p 是需要压入的静态变量指针,上面代码的操作就可以减少一步,具体代码如下,运行后的效果是一致的

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_rawsetplua_rawgetp 都是原始访问,效率会比普通访问快一些。

二、上值

每一次在 Lua 中创建新的 C 函数时,可以将任意数量的上值和这个函数相关联,而每个上值都可以保存一个 Lua 值。后面调用该函数时,可以通过伪索引自由的访问这些上值。这种 C 函数与其上值的关联称为闭包

上值的个数有限制, C 语言函数中最多可以有 255 个上值,lua_upvalueindex 的最大索引值是 256 。

1、如何为闭包添加上值

这里结合一个例子讲解,我们创建一个 C++ 函数给 Lua 调用,这个 C++ 函数是一个累加的闭包,每次调用都会产生新的值,而且每个闭包的值互不影响。

第一步,创建累加的 C 函数。

  • 函数 newCounter:创建闭包的函数,每次 Lua 中调用 newCounter 函数,都会返回一个新的闭包,会携带新的上值。
  • 函数 counter :真正的执行函数,每次 Lua 执行 newCounter 创建后的闭包都会运行该函数,该函数会 “更新上值以便下次使用” 和 “返回更新后的上值”。
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 模块中的函数》 文章中已经详细的分享了如何编写和调用自己编写的库,这里就不再赘述。

lua_State *L = luaL_newstate();
luaL_openlibs(L);

// 创建一个 lib
luaL_newlib(L, luaLReg);
// Counter = lib
lua_setglobal(L, "Counter");

// ... 运行 Lua 脚本

第三步:编写 Lua 文件,并运行

Lua 中创建两个累加器,a 累加器累加三次,b 累加器只累加一次,可以从运行结果看出,两个累加器互不影响。

local a = Counter.newCounter()
local b = Counter.newCounter()

print("a", a(), a(), a())
print("b", b())

运行代码

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

运行结果

a	1	2	3
b	1

2、lua_pushcclosure

void  (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);

描述:

将一个 C 函数作为闭包压入 Lua 栈中。

参数:

  • 参数 L: Lua 状态机指针。
  • 参数 fn: 一个 C 函数指针,表示要作为闭包压入 Lua 栈的函数。
  • 参数 n: 一个整数,表示闭包所使用的上值数量。

3、lua_upvalueindex

#define lua_upvalueindex(i)	(LUA_REGISTRYINDEX - (i))

描述:

他是一个宏,用于获取闭包的上值在 Lua 栈中的索引。这个索引是一个伪索引,他和其他的栈索引一样,唯一的区别在于它不存在于栈中。

参数:

  • 参数 i : 表示要获取的上值的索引,第一个上值的索引为 1 。

返回值:

返回值是一个整数,表示该上值在 Lua 栈中的索引。

如果传入的索引超出了范围,则会返回 LUA_TNONE 。可以使用 lua_isnone 函数进行判断上值的伪索引是否合法,例如下面代码:

lua_isnone(L, lua_upvalueindex(i))

三、共享上值

在封装一个库给 Lua 进行调用的时候,有时需要库函数间共享一些数值或变量。虽然注册表也可以达到这一目的,但不够优雅,毕竟将库的过多细节暴露,并且存在 key 被覆盖的风险,所以更好的做法应该是使用上值。

Lua 为这种场景提供了一种解决方案,简化在库函数间共享上值的做法。

之前我们创建一个 Lua 库时,一般都使用 luaL_newlib 函数,他其实是个宏,具体实现如下所示。

#define luaL_newlib(L,l)  \
  (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))

要达到共享上值的关键点在于 luaL_setfuncs 的三个参数,用 luaL_newlib 创建库,默认情况下传递的都是 0 ,即没有共享上值,所以我们只需要改造下就可以支持了。

1、举个例子

定义一个库,这个库中有两个函数,共享三个上值,分别为整数型、字符串、table,两个函数均可对共享上值获取和修改

第一步,定义库函数和设置库的共享上值。

showInfoshowCode 是两个库函数,会获取各自的参数和共享上值,进行操作后返回给 Lua

共享上值的关键点在 luaopen_user 函数,函数中会压入三个值,然后调用 luaL_setfuncs 将这三个值作为库的共享上值。

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 文件。

只是简单的调用库函数,然后打印返回值。

print(User.showInfo("hi", 80000))
print(User.showCode("hello"))

第三步,将库函数添加到 Lua 中,并运行 Lua 文件。

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吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

你可能感兴趣的:(Lua,c语言,lua,android,开发语言,c++)