零、前言
Lua 作为一门脚本语言,可以作为 "配置文件"、"动态逻辑脚本" 等角色作用于宿主程序。
因为他是一门语言,所以他有以下的好处:
1. Lua 会处理语法细节,后续维护简单,并且可以有注释。 2. 可以编写逻辑,达到复杂的配置。
如果我们的程序需要进行一些 "下发配置" 时,一般会考虑选择 "json"、"文件" 等形式。但是如果 "配置" 内容较为复杂,则可以考虑 Lua 了,具体可以查看以下分享。
一、运行 Lua 文件
在之前 "C++ 与 Lua 交互异常处理" 的文章中,已分享如何在 C/C++ 中使用 Lua 文件,这里复习一下。
可以通过 lua_call
和 lua_pcall
两个函数调用 Lua 代码。
cpp
int lua_call(lua_State *L, int nargs, int nresults);
int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);
两者均用于在 C/C++ 代码中调用 Lua 函数,不同点在于:
lua_call
会将代码中的异常直接抛出,导致程序中断。lua_pcall
提供一个保护模式运行 Lua 代码,即使发生异常,也会被捕获,并可以通过第四个参数的错误处理函数处理错误,程序不会因此而中断。
参数:
- 参数 L: Lua State 的指针。
- 参数 nargs: 传递给被调用函数的参数个数。
- 参数 nresults: 期望的返回值个数。
- 参数 errfunc: 错误处理函数的索引,用于处理发生的错误。如果为 0,则错误信息会被压入栈顶。
返回值:
- 函数调用成功,返回 0,并将返回值压入栈中。
- 如果函数调用发生错误,返回一个非零值,并将错误信息压入栈中。
错误处理的细节可以翻阅之前的 "C++ 与 Lua 交互异常处理" 文章。
举个例子
我们通过 lua_pcall
加载一个 Lua 文件,然后在调用 Lua 中的一个函数计算数值,最后获取返回结果。
Lua 文件的内容
lua
function luaFunction(x, y)
return (x ^ 2 * math.sin(y)) / (1 - x)
end
接下来看宿主如何运行和调用,可以结合着注释理解。思路是:
- 使用
luaL_loadfile
加载 Lua 文件 - 使用
lua_pcall
运行 Lua 文件,此时 Lua 中的luaFunction
是一个全局变量 - 将
luaFunction
压入栈,同时将需要传递的参数压入栈,然后通过lua_pcall
调用函数 - 最后使用出栈函数获取结果,因为这里为数值,所以使用
lua_tonumberx
出栈函数
cpp
// C++ 入口
void cppCallLuaFunction() {
std::string fname = PROJECT_PATH + "/5、C++调用Lua代码/调用Lua函数/调用Lua函数.lua";
lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 加载 Lua 文件、运行 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);
return;
}
double calResult = 0;
bool isSuccess = false;
// 调用 Lua 文件中的函数
isSuccess = callLuaFunction(L, 2, 34, &calResult);
if (isSuccess) {
printf("调用 Lua 成功 luaFunction: %f\n", calResult);
} else {
printf("调用 Lua 失败\n");
}
lua_close(L);
}
// 调用 Lua 函数
bool callLuaFunction(lua_State *L, double x, double y, double *result) {
// 获取全局中的 luaFunction 变量,将其压入栈中
int getResult = lua_getglobal(L, "luaFunction");
if (getResult == LUA_TNIL) {
printf("lua_getglobal get failure\n");
return false;
}
// 将 x 和 y 入栈,会作为 luaFunction 函数的两个参数
lua_pushnumber(L, x);
lua_pushnumber(L, y);
// 运行 luaFunction 函数
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
printf("error running function 'f': %s", lua_tostring(L, -1));
return false;
}
int isNum;
// 获取 luaFunction 的返回值
*result = lua_tonumberx(L, -1, &isNum);
if (!isNum) {
printf("function 'luaFunction' should return a number.");
return false;
}
// 需要将返回值弹出
lua_pop(L, 1);
return true;
}
// --> 调用 Lua 成功 luaFunction: -2.116331
二、lua_getglobal
在上面一小节中使用到了 lua_getglobal
,这里详细阐述下这一函数的作用
cpp
int (lua_getglobal) (lua_State *L, const char *name);
作用:
用于获取全局变量的值,并将其压入 Lua 栈中。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 name: 要获取的全局变量的名称,以字符串形式表示。
返回值:
如果成功获取到全局变量,则返回该变量在栈中的索引(索引是从 1 开始的整数)。
如果未找到指定的全局变量,则返回 LUA_TNIL。
三、配置文件
在前言一节中,分享用 Lua 文件作为 "配置文件",这一小节则围绕这一用法展开分享他的好处和讲解如何在 C++ 中调用 Lua 文件。
1、根据环境执行不同逻辑
有时我们需要根据不同的环境,配置一些不同的参数。可以在 Lua 文件中通过 os.getenv
获取系统配置的环境变量,进行返回不同的配置信息。
首先,需要在运行的机器中配置环境变量,我的电脑是 Mac ,配置在 ~/.zshrc
中。
在 ~/.zshrc
配置内容如下所示
ini
export DISPLAY_ENV="Mac"
然后在 Lua 中进行获取使用,Lua 的内容如下
lua
-- DISPLAY_ENV 在环境变量中配置,open ~/.zshrc 可以查看
local displayEnv = os.getenv("DISPLAY_ENV");
print("displayEnv", displayEnv)
if displayEnv == "Mac" then
width = 3072
height = 1920
else
width = 1920
height = 1080
end
最后通过 C++ 加载和运行该 Lua 文件
cpp
void loadConfigUseEnv() {
std::string filename = PROJECT_PATH + "/5、C++调用Lua代码/Lua作为配置文件/根据环境变量获取值/config.lua";
lua_State *L = luaL_newstate();
luaL_openlibs(L);
if (luaL_loadfile(L, filename.c_str()) || lua_pcall(L, 0, 0, 0)) {
printf("can't run config. file: %s\n", lua_tostring(L, -1));
return;
}
int width;
if (!getGlobInt(L, "width", &width)) {
printf("Get width failure.");
return;
}
int height;
if (!getGlobInt(L, "height", &height)) {
printf("Get height failure.");
return;
}
printf("size: %d x %d\n", width, height);
lua_close(L);
}
bool getGlobInt(lua_State *L, const char *var, int *result) {
int isNum;
// 将 var 对应的值压入栈中
lua_getglobal(L, var);
*result = (int) lua_tointegerx(L, -1, &isNum);
if (!isNum) {
printf("'%s' should be a number\n", var);
return false;
}
// 将 var 对应值压入栈中的值弹出
lua_pop(L, 1);
return true;
}
会输出以下内容
yaml
displayEnv Mac
size: 3072 x 1920
可以看到,Lua 脚本会获取我们电脑的环境变量,根据值执行不同的逻辑,这一点在 json、文本配置文件是无法做到的。
2、预设配置
宿主可以初始化一些配置选项给到 Lua ,Lua 根据所需进行使用,当然也可以自行生成配置项,这一过程只是业务逻辑上的设计。
下面举个例子,假设需要给一个 App 配置一个主题色,颜色用 rgb 进行表示,在 Lua 中可以用 table 进行装载颜色,让数据联系更加紧凑,可以内置一些颜色给到 Lua 进行使用。
话不多说,下面开始演示这一过程是如何使用。
第一步,我们需要先将内置的颜色值设置到 Lua 中。 需要先创建一个表 table ,然后将 key - value 放置到 table 中。
在下面的 setColor
方法中,会先创建一个 table ,然后通过封装的 setColorField 方法将 red、green、blue 放置到 table 中。
setColorField 中会将 key、value 按顺序压栈,然后通过 lua_settable 设置到对应的索引表中。
lua_newtable、lua_createtable、lua_settable、lua_setfield 这些 api 会在接下来的小节进行详细讲解
cpp
void setColor(lua_State *L, struct ColorTable *ct) {
// 创建一个空表,并压入栈顶
// lua_newtable 这是一个宏,真是定义是 lua_createtable(L, 0, 0)
// lua_newtable 和 lua_createtable 会压入一个 table 到栈中
lua_newtable(L);
// lua_createtable(L, 0, 3);
setColorField(L, "red", ct->red);
setColorField(L, "green", ct->green);
setColorField(L, "blue", ct->blue);
// 'name' = table
// 弹出表,并且将其设置为指定名称的全局变量的值
lua_setglobal(L, ct->name);
}
void setColorField(lua_State *L, const char *index, int value) {
// 第一种
// 键
lua_pushstring(L, index);
// 值
lua_pushnumber(L, (double) value / MAX_COLOR);
// 将键和值弹出,然后设置到 table (索引为 -3) 中,table[stack[-2]] = stack[-1]
lua_settable(L, -3);
// 第二种
// lua_pushnumber(L, (double) value / MAX_COLOR);
// lua_setfield(L, -2, index);
}
经过这一步的设置,在 Lua 中就可以查看到对应的值了。
第二步,加载和运行 Lua 文件,在 Lua 文件中使用第一步设置的值,将其返回给宿主
Lua 文件内容如下
lua
print(PINK, PINK.red, PINK.green, PINK.blue)
-- c++ 获取到的为 table
background = PINK
-- c++ 获取到的为 string
--background = "PINK"
加载和运行的代码如下
cpp
#define MAX_COLOR 255
struct ColorTable {
char *name;
unsigned char red, green, blue;
} colorTable[] = {
{"WHITE", MAX_COLOR, MAX_COLOR, MAX_COLOR},
{"RED", MAX_COLOR, 0, 0},
{"GREEN", 0, MAX_COLOR, 0},
{"BLUE", 0, 0, MAX_COLOR},
{"PINK", 255, 192, 203},
{nullptr, 0, 0, 0}
};
void load(lua_State *L, const char *fname) {
int i = 0;
// 设置颜色值
while (colorTable[i].name != nullptr) {
setColor(L, &colorTable[i++]);
}
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) {
printf("can't run config. file: %s\n", lua_tostring(L, -1));
return;
}
// ... 获取值的操作,下面分享
}
void loadConfigUseTable() {
std::string filename = PROJECT_PATH + "/5、C++调用Lua代码/Lua作为配置文件/配置中使用表/config.lua";
lua_State *L = luaL_newstate();
luaL_openlibs(L);
load(L, filename.c_str());
lua_close(L);
}
第三步,获取 Lua 返回的值
将 Lua 中对应的 "background" 变量压入栈,然后将 key 压入栈,通过 lua_gettable 获取对应的值。
cpp
bool getColorField(lua_State *L, const char *key, int *result) {
int isNum;
// 第一种做法
// 将 key 压入栈
lua_pushstring(L, key);
// 将栈顶(key)弹出,然后将读取的值压入
lua_gettable(L, -2);
// 第二种做法(lua_getfield 和 lua_gettable 都会返回类型)
// if (lua_getfield(L, -1, key) == LUA_TNUMBER) {
// printf("invalid component in background color");
// return false;
// }
*result = (int) (lua_tonumberx(L, -1, &isNum) * MAX_COLOR);
if (!isNum) {
printf("invalid component '%s' in color", key);
return false;
}
lua_pop(L, 1);
return true;
}
void load(lua_State *L, const char *fname) {
// ... 加载和运行 Lua 文件
// 将 background 压入栈
lua_getglobal(L, "background");
if (lua_istable(L, -1)) {
int red;
if (!getColorField(L, "red", &red)) {
printf("Get red failure.\n");
return;
}
int green;
if (!getColorField(L, "green", &green)) {
printf("Get green failure.\n");
return;
}
int blue;
if (!getColorField(L, "blue", &blue)) {
printf("Get blue failure.\n");
return;
}
printf("table color: (%d, %d, %d)\n", red, green, blue);
} else if (lua_isstring(L, -1)) {
const char *name = lua_tostring(L, -1);
int i;
for (i = 0; colorTable[i].name != nullptr; i++) {
if (strcmp(name, colorTable[i].name) == 0) {
break;
}
}
if (colorTable[i].name == nullptr) {
printf("invalid color name (%s)", name);
return;
}
int red = colorTable[i].red;
int green = colorTable[i].green;
int blue = colorTable[i].blue;
printf("string color: (%d, %d, %d)\n", red, green, blue);
} else {
printf("'background' is not a table.");
}
}
最后会输出
lua
table: 0x600001afc880 1.0 0.75294117647059 0.79607843137255
table color: (255, 192, 203)
四、lua_newtable
cpp
void lua_newtable(lua_State *L);
作用:
用于创建一个新的空表,并将其压入 Lua 栈中。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
返回值:
没有返回值,但会压入一个 table 在栈顶。
五、lua_createtable
lua_newtable 其实是一个宏定义,真正实现是 lua_createtable
cpp
#define lua_newtable(L) lua_createtable(L, 0, 0)
cpp
void (lua_createtable) (lua_State *L, int narr, int nrec);
作用:
用于创建一个新的表,并将其压入 Lua 栈中。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 narr: 表的数组部分初始容量。数组部分是用于存储连续整数键(索引)的区域。当按顺序插入整数键时,它们会被存储在数组部分中,以实现高效的数组访问。narr 的值可以是正整数,用于指定表初始化时预分配的数组部分的大小。如果 narr 为 0,则表示不分配数组部分。
- 参数 nrec: 表的哈希部分初始容量。哈希部分是用于存储非整数键(如字符串键)的区域。当我们插入非整数键时,它们会被存储在哈希部分中,并使用哈希表来实现高效的键值对查找。nrec 的值可以是正整数,用于指定表初始化时预分配的哈希部分的大小。如果 nrec 为 0,则表示不分配哈希部分。
一般情况下,如果事先知道表的大致大小,设置适当的 narr 和 nrec 值可以提高表操作的效率。但如果不确定表的大小,或者表的大小会动态变化,设置为 0 会让 Lua 在需要时自动调整表的大小。Lua 在内部会根据需要动态调整表的大小,以适应实际的键值对数量。
返回值:
没有返回值,但会压入一个 table 在栈顶。
六、lua_gettable
cpp
int (lua_gettable) (lua_State *L, int idx);
作用:
用于从 Lua 栈中获取表中指定键的值,并将该值压入栈顶
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 index: 表示要获取表的索引位置。如果索引为正数,则表示从栈底向上数的位置获取表;如果索引为负数,则表示从栈顶向下数的位置获取表。
返回值:
返回数据类型,并且会将获取到的值压入栈顶,如果没有找到对应的值,则会将 nil 压入(因为在 table 中查询一个不存在的键时,则会返回 nil )。
返回的数据类型有以下类型,可以根据类型判断是否符合期望。
cpp
#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
一图胜千言:
七、lua_getfield
和 lua_gettable 的作用一样,只是更加便捷,不用额外的将 key 压入栈顶。
cpp
int (lua_getfield) (lua_State *L, int idx, const char *k);
作用:
用于获取指定表(索引为 idx )中字段名为 k 的值。
参数:
- 参数 L: Lua 状态机的指针。
- 参数 index: 表在堆栈中的索引。如果索引为正数,则表示从栈底向上数的位置获取表;如果索引为负数,则表示从栈顶向下数的位置获取表。
- 参数 k: 字段的名称,以 C 字符串的形式传递。
返回值:
返回数据类型,并且会将获取到的值压入栈顶,如果没有找到对应的值,则会将 nil 压入(因为在 table 中查询一个不存在的键时,则会返回 nil )。
返回的数据类型有以下类型,可以根据类型判断是否符合期望。
cpp
#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
八、lua_settable
cpp
void lua_settable(lua_State *L, int index);
作用:
用于将键(索引为 -2 )、值(索引为 -1 )对存储到表(索引为 index )中。
可以理解为 table[stack[-2]] = stack[-1]
,table 为 stack[index]
。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 index: 表示要获取表的索引位置。如果索引为正数,则表示从栈底向上数的位置获取表;如果索引为负数,则表示从栈顶向下数的位置获取表。
返回值:
没有返回值,会将键和值出栈。
九、lua_setfield
和 lua_settable 的作用一样,只是更加便捷,不用将 key 值压入到栈中,但还是需要将 value 压至栈顶。
cpp
void lua_setfield(lua_State *L, int index, const char *key);
作用:
用于将值(索引为 -1 )存储到指定表(索引为 index )中的指定键名(key)。
可以理解为 table[index] = stack[-1]
,table 为 stack[index]
。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 index: 表在栈中的索引位置。如果索引为正数,则表示从栈底向上数的位置获取表;如果索引为负数,则表示从栈顶向下数的位置获取表。
- 参数 key: 作为键的字符串。是一个 C 字符串,表示要在表中使用的键名。
返回值:
没有返回值,会将栈顶的 value 出栈。
十、lua_setglobal
cpp
void lua_setglobal(lua_State *L, const char *name);
作用:
用于将值(索引为 -1 )以 name 作为属性名存储到全局变量中。
可以理解为 'name' = value
。
参数:
- 参数 L: Lua 状态机(Lua state)指针。
- 参数 name: 是一个 C 字符串,表示要存储值的全局变量名。
返回值:
没有返回值,会将栈顶值弹出。
十一、写在最后
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。