使用 Lua 时,在编写 C/C++ 函数经常需要对栈进行交互,而这中间更多的操作和数组、字符串相关。
从之前分享的 “Lua 数据类型——表” 文章中知道 Lua 中的 “数组” 是以表的形式存在,只是他的 key 值是有序的数值。
对于 Lua 的数组存取,当然可以使用之前介绍的 lua_gettable
和 lua_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);
接下来一个个讲解
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 中。
参数:
返回值:
没有返回,栈顶的元素会被使用后出栈。
两个函数的区别:
如果操作的 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);
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 的值,将其压入栈中,即栈顶。
参数:
返回值:
返回 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);
以下例子实现了这几个步骤:
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
LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx);
描述:
用于获取给定索引 idx 位置的 Lua 值的长度(length),可以用于获取字符串、表和用户数据等类型的长度。
参数:
返回值:
会对该索引位置的元素执行长度运算符,然后将其长度值(整数)返回。
但由于元方法的存在,该运算符有可能会返回任意类型的值,所以如果不是整型,则会发生错误。
在 C/C++ 和 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_pushfstring
和lua_concat
都可以简便的将多个字符串进行连接,但是如果如果字符串数量较大时,效率会比较低,可以考虑使用缓冲机制(下面的小节会进行分享)。
这个例子的功能:用 C++ 函数实现一个分割字符串的功能,给到 Lua 进行调用
第一步,定义分割字符串的 C++ 函数。
strchr
对字符串进行检索被分割的位置,然后使用 lua_pushlstring
进行对字符串裁剪后压栈。lua_rawseti
进行设置到 “第二步创建的 table ” 中。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 --- 涌
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: 江!澎!涌!
如果操作的字符串数量较大,可以考虑使用缓冲机制。
对于长度已知的情况,一般分为以下几个步骤:
luaL_Buffer
类型的变量。luaL_buffinitsize
进行初始化,获取一个指向指定大小缓冲区的指针,后续就可以自由地使用该缓冲区来创建字符串。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 状态的引用。
如果不知道最终需要多大的缓冲区,可以通过逐步增加内容的方式来使用缓冲区。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++ 函数。
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吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。