Lua 中编写 C 函数的一些便捷技巧

零、前言

使用 Lua 时,在编写 C/C++ 函数经常需要对栈进行交互,而这中间更多的操作和数组、字符串相关。

一、数组操作的便捷方式

从之前分享的 “Lua 数据类型——表” 文章中知道 Lua 中的 “数组” 是以表的形式存在,只是他的 key 值是有序的数值。

对于 Lua 的数组存取,当然可以使用之前介绍的 lua_gettablelua_settable 的方法,只是较为繁琐。所以 Lua 针对数组提供了以下的方法进行快捷的存取:

// Lua 5.3 之后才有
// 会调用 table 元方法
LUA_API void  (lua_seti) (lua_State *L, int idx, lua_Integer n);

// 不调用 table 元方法
LUA_API void  (lua_rawseti) (lua_State *L, int idx, lua_Integer n);

// Lua 5.3 之后才有
// 会调用 table 的元方法
LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);

// 不调用 table 元方法
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);

接下来一个个讲解

1、lua_seti 和 lua_rawseti

LUA_API void  (lua_seti) (lua_State *L, int idx, lua_Integer n);
LUA_API void  (lua_rawseti) (lua_State *L, int idx, lua_Integer n);

描述:

用于将栈顶的元素(即索引为 -1 )作为 value ,用 n 作为 key ,设置到索引为 idx 的 table 中。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要操作的 Lua 表在栈中的索引位置。
  • n: 设置在 table 中的 key 值(必须是整型)。

返回值:

没有返回,栈顶的元素会被使用后出栈。

两个函数的区别:

如果操作的 table 没有元表,则效果是一样的,使用 lua_rawseti 会稍微快一些。

如果操作的 table 有元表,则 lua_seti 会使用到 table 的元方法。

lua_settable 相同的用法:

lua_seti 只是针对 key 为整型的 table 设置更加便捷,当然也可以用 lua_settable 实现一样的效果

lua_pushstring(L, value);
lua_seti(L, t, key);

// 当 t 为正整数时,等同于以下操作。
// 因为 t 为负数的话,lua_settable 的 t 要要深一个元素,lua_settable 的操作需要将 key 压入栈。

lua_pushnumber(L, key);
lua_pushstring(L, value);
lua_settable(L, t);

2、lua_geti 和 lua_rawgeti

LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);

描述:

用于从索引为 idx 位置的 table 中获取 key 为 n 的值,将其压入栈中,即栈顶。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要操作的 Lua 表在栈中的索引位置。
  • n: 要获取 table 的 key 值(必须是整型)。

返回值:

返回 value 数据类型,并且会将获取到的值压入栈顶,如果没有找到对应的值,则会将 nil 压入(因为在 table 中查询一个不存在的键时,则会返回 nil )。

返回的数据类型有以下类型,可以根据类型判断是否符合期望。

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

两个函数的区别:

如果操作的 table 没有元表,则效果是一样的,使用 lua_rawgeti 会稍微快一些。

如果操作的 table 有元表,则 lua_geti 会使用到 table 的元方法。

lua_settable 相同的用法:

lua_geti 只是针对 key 为整型这一类型 table 的设置更加便捷,当然也可以用 lua_gettable 实现一样的效果

lua_geti(L, t, key)

// 当 t 为正整数时,等同于以下操作。
// 因为 t 为负数的话,lua_gettable 的 t 要要深一个元素,lua_gettable 的操作需要将 key 压入栈。

lua_pushnumber(L, key);
lua_gettable(L, t);

3、举个例子

以下例子实现了这几个步骤:

  1. 创建一个 table ,并压入栈中。
  2. 使用两种方式向 table 中设置元素。
  3. 使用两种方式获取 table 中的元素。
  4. 打印 table 的长度。
lua_State *L = luaL_newstate();

// 创建并压入一个新表
lua_newtable(L);

// --------- 添加至数组的操作方式 ---------
// 第一种:
// table[1] = "江澎涌!(快捷方式插入)"
lua_pushstring(L, "江澎涌!(快捷方式插入)");
lua_seti(L, -2, 1);

// 第二种:
// 栈底 ----> 栈顶
// table - key - value
// 使用完后,key - value 会被弹出
// table[2] = "江澎涌!(常规方式插入)"
lua_pushnumber(L, 2);
lua_pushstring(L, "江澎涌!(常规方式插入)");
lua_settable(L, -3);

// --------- 从数组中获取值方式 ---------
// 第一种
// 获取 table 中索引为 1 的值压入栈
printf("lua_geti(L, -1, 1) 类型:%d\n", lua_geti(L, -1, 1));
printf("table[1] = %s\n", lua_tostring(L, -1));
// 将获取的值弹出
lua_pop(L, 1);

// 第二种
// 压入 key 值
lua_pushnumber(L, 2);
// 获取 table 中索引为 2 的值压入栈
printf("lua_gettable(L, -2) 类型:%d\n", lua_gettable(L, -2));
printf("table[2] = %s\n", lua_tostring(L, -1));
// 将获取的值弹出
lua_pop(L, 1);

long long n = luaL_len(L, 1);
printf("lua table length: %lld\n", n);

lua_close(L);

输出以下内容:

lua_geti(L, -1, 1) 类型:4
table[1] = 江澎涌!(快捷方式插入)
lua_gettable(L, -2) 类型:4
table[2] = 江澎涌!(常规方式插入)
lua table length: 2

4、luaL_len

LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx);

描述:

用于获取给定索引 idx 位置的 Lua 值的长度(length),可以用于获取字符串、表和用户数据等类型的长度。

参数:

  • L: Lua 状态机(Lua state)的指针。
  • idx: 要获取长度的值在栈中的索引位置。

返回值:

会对该索引位置的元素执行长度运算符,然后将其长度值(整数)返回。

但由于元方法的存在,该运算符有可能会返回任意类型的值,所以如果不是整型,则会发生错误。

二、字符串操作

1、字符串普通操作的 C-API

在 C/C++ 和 Lua 的交互中,字符串的使用也是比较频繁的,但由于内存回收的问题,使用过程中需要以下几点:

  1. 当 C/C++ 接收到一个 Lua 字符串为参数时,需要遵循在使用该字符串期间不能从栈中将其弹出,而且不修改字符串。
  2. 当需要将 C/C++ 的字符串传递给 Lua 时,Lua 提供了很多关于字符串的 C-API 给我们使用,Lua 内部会进行相应的内存分配和管理。

Lua 提供操作字符串的 C-API

C API 函数 描述
const char *(lua_pushstring) (lua_State *L, const char *s); 将 字符串(以 “\0” 结尾) 压栈。
const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len); 将 字符串(会结合 “\0” 和长度的参数决定字符串的长度) 压栈。
const char *(lua_pushvfstring) (lua_State *L, const char *fmt,va_list argp); 将 字符串(格式化字符串,接收可变参数) 压栈。
const char *(lua_pushfstring) (lua_State *L, const char *fmt, …); 将 字符串(格式化字符串) 压栈。
void (lua_concat) (lua_State *L, int n); 会将栈顶的 n 个值,弹出,然后进行连接,最后将结果压入栈中。

在之前分享的 《C++ 与 Lua 数据交互载体——栈》 中,已经展示了如何使用这些 API ,这里就不再一一罗列如何使用。

lua_pushfstring 可以接受以下的指示符

指示符 描述
%s 插入一个以 \0 结尾的字符串
%d 插入一个 int
%f 插入一个 Lua 语言的浮点数
%p 插入一个浮点数
%I 插入一个 Lua 语言的整型数
%c 插入一个以 int 表示的单字节字符
%U 插入一个以 int 表示的 UTF-8 字节序列
%% 插入一个百分号

lua_pushfstringlua_concat 都可以简便的将多个字符串进行连接,但是如果如果字符串数量较大时,效率会比较低,可以考虑使用缓冲机制(下面的小节会进行分享)。

2、举个例子——字符串截取

这个例子的功能:用 C++ 函数实现一个分割字符串的功能,给到 Lua 进行调用

第一步,定义分割字符串的 C++ 函数。

  1. 接收和检查 “需要被分割的字符串” 和 “分割符” 。
  2. 创建一个 table ,并且压入到栈中,后续的分割结果会压入到该 table 中。
  3. 使用 C-API strchr 对字符串进行检索被分割的位置,然后使用 lua_pushlstring 进行对字符串裁剪后压栈。
  4. 使用 lua_rawseti 进行设置到 “第二步创建的 table ” 中。
  5. 循环结束后,将剩余的字符串也设置到 table 中。
  6. 返回 1 ,表示只有一个返回值。
int lua_split(lua_State *L) {
    // 被分割的内容
    const char *s = luaL_checkstring(L, 1);
    // 分割符
    const char *sep = luaL_checkstring(L, 2);

    // 创建一个 table ,并且压入到栈中,后续的分割结果会压入到该 table 中
    lua_newtable(L);

    const char *e;
    int i = 1;
    // char *strchr(const char *str, int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置
    // 该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL
    while ((e = strchr(s, *sep)) != nullptr) {
        // 将 s 中 ( e - s ) 个字符压入栈顶
        lua_pushlstring(L, s, e - s);
        // 设置到 -2 ( table )中
        lua_rawseti(L, -2, i++);
        s = e + 1;
    }

    // 把最后的字符也压入
    lua_pushstring(L, s);
    lua_rawseti(L, -2, i);

    return 1;
}

第二步,编写 Lua 脚本。

Lua 脚本很简单,只是调用一下第三步注入的 C 函数(函数内容为第一步编写的函数),然后打印 table 长度和内容

local result = split("江_澎_涌", "_");

print("length", #result);

for i, v in ipairs(result) do
    print(i, " --- ", v)
end

第三步,将函数注入到 Lua 中,并运行 Lua 的脚本。

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

lua_pushcfunction(L, lua_split);
lua_setglobal(L, "split");

std::string fname = PROJECT_PATH + "/8、编写C函数技巧/字符串便捷操作/字符串截取.lua";
if (luaL_loadfile(L, fname.c_str()) || lua_pcall(L, 0, 0, 0)) {
    printf("can't run config. file: %s\n", lua_tostring(L, -1));
}

lua_close(L);

最后输出的内容如下

length	3
1	 --- 	江
2	 --- 	澎
3	 --- 	涌

3、lua_concat 的使用

lua_concat 会将栈顶的 n 个值,弹出,然后进行连接,最后将结果压入栈中。

举个例子,我们压入 3 个字符串,然后使用 lua_concat 进行弹出拼接并压入栈中,最后将这个结果打印。

lua_State *L = luaL_newstate();

// 压入三个字符串
lua_pushstring(L, "江!");
lua_pushstring(L, "澎!");
lua_pushstring(L, "涌!");
// 将三个字符串进行弹出并拼接
lua_concat(L, 3);

printf("stack top: %d\n", lua_gettop(L));
printf("concat: %s\n", lua_tostring(L, -1));

lua_close(L);

最后输出为

stack top: 1
concat: 江!澎!涌!

4、缓冲机制

如果操作的字符串数量较大,可以考虑使用缓冲机制。

4-1、已知长度的缓冲区

对于长度已知的情况,一般分为以下几个步骤:

  1. 声明一个 luaL_Buffer 类型的变量。
  2. 使用 luaL_buffinitsize 进行初始化,获取一个指向指定大小缓冲区的指针,后续就可以自由地使用该缓冲区来创建字符串。
  3. 最后调用 luaL_pushresultsize 将缓冲区中的内容转换为一个新的 Lua 字符串,并将该字符串压栈。
int str_upper(lua_State *L) {
    size_t l;
    // 检测参数是否为字符串,并且获取长度
    const char *s = luaL_checklstring(L, 1, &l);

    size_t i;
    // 声明缓冲区
    luaL_Buffer b;
    // 初始化缓冲区
    char *p = luaL_buffinitsize(L, &b, l);
    for (i = 0; i < l; i++) {
        p[i] = toupper(s[i]);
    }
    // 将缓冲区中的内容转换为一个新的 Lua 字符串,并将字符串压栈
    luaL_pushresultsize(&b, l);
}

调用的代码如下,这里为了方便,就省去了从 Lua 进行调用,直接通过 C 压入字符串,给到上述的函数进行使用。

lua_State *L = luaL_newstate();

lua_pushstring(L, "jiang peng yong");
str_upper(L);
printf("to upper: %s\n", lua_tostring(L, -1));

lua_close(L);

输出的内容如下

to upper: JIANG PENG YONG

str_upper 其实是 Lua 中 lstrlib.c 的源代码,感兴趣可以自行查阅。

luaL_pushresultsize 的调用并未传入 lua_State 类型的参数,是因为初始化之后,缓冲区保存了对 lua_State 状态的引用。

4-2、未知长度的缓冲区

如果不知道最终需要多大的缓冲区,可以通过逐步增加内容的方式来使用缓冲区。Lua 为此提供了以下 C-API

函数 描述
void (luaL_buffinit) (lua_State *L, luaL_Buffer *B); 初始化缓冲区,但不设置大小
char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); 为缓冲区分配内存空间,以容纳指定大小的数据。sz 即预分配的大小(以字节为单位),返回指向缓冲区的指针
void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); 增加一个长度明确的字符串
void (luaL_addstring) (luaL_Buffer *B, const char *s); 用于增加一个以 \0 结尾的字符串
void (luaL_addvalue) (luaL_Buffer *B); 用于将栈顶的字符串放入缓冲区
void (luaL_pushresult) (luaL_Buffer *B); 刷新缓冲区并在栈顶留下最终的结果字符串
luaL_addchar(B,c) 是一个宏定义,用于增加单个字符

举个例子

实现一个简易的将 table 内容连接为字符串功能。

第一步,实现拼接的 C++ 函数。

  1. 检测第一个参数是否为 table ,不是则会抛异常到 Lua 中
  2. 获取 table 的长度
  3. 初始化缓冲区,不进行大小的设置
  4. 循环 table ,将内容添加到缓冲区中
  5. 将缓冲区内容组装为字符串后压入栈,返回 Lua
int bufferConcat(lua_State *L) {
    luaL_Buffer b;
    long long i, n;
    // 检测是否为 table
    luaL_checktype(L, 1, LUA_TTABLE);
    // 获取 table 长度
    n = luaL_len(L, 1);
    // 初始化缓冲区
    luaL_buffinit(L, &b);
    // 循环取出 table 的值,添加到缓冲区中
    for (i = 1; i <= n; i++) {
        lua_geti(L, 1, i);
        luaL_addvalue(&b);
    }
    // 将缓冲区内容组装为字符串,压入栈
    luaL_pushresult(&b);
    return 1;
}

第二步,实现 Lua 脚本内容。

Lua 脚本中的内容如下,只是简单的调用 C++ 暴露的函数,然后将其打印。

print('concat({"江", "澎", "涌", 29}', concat({"江", "澎", "涌", 29}))

第三步,注入第一步函数到 Lua 中,然后运行第二步的脚本。

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

lua_pushcfunction(L, bufferConcat);
lua_setglobal(L, "concat");

std::string fname = PROJECT_PATH + "/8、编写C函数技巧/字符串便捷操作/字符串连接(缓冲区).lua";
if (luaL_loadfile(L, fname.c_str()) || lua_pcall(L, 0, 0, 0)) {
    printf("can't run config. file: %s\n", lua_tostring(L, -1));
}

lua_close(L);

输出的内容如下

concat({"江", "澎", "涌", 29}	江澎涌29

三、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

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

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

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