C++ 调用 Lua 函数

零、前言

Lua 作为一门脚本语言,可以作为 "配置文件"、"动态逻辑脚本" 等角色作用于宿主程序。

因为他是一门语言,所以他有以下的好处:

1. Lua 会处理语法细节,后续维护简单,并且可以有注释。 2. 可以编写逻辑,达到复杂的配置。

如果我们的程序需要进行一些 "下发配置" 时,一般会考虑选择 "json"、"文件" 等形式。但是如果 "配置" 内容较为复杂,则可以考虑 Lua 了,具体可以查看以下分享。

一、运行 Lua 文件

在之前 "C++ 与 Lua 交互异常处理" 的文章中,已分享如何在 C/C++ 中使用 Lua 文件,这里复习一下。

可以通过 lua_calllua_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

接下来看宿主如何运行和调用,可以结合着注释理解。思路是:

  1. 使用 luaL_loadfile 加载 Lua 文件
  2. 使用 lua_pcall 运行 Lua 文件,此时 Lua 中的 luaFunction 是一个全局变量
  3. luaFunction 压入栈,同时将需要传递的参数压入栈,然后通过 lua_pcall 调用函数
  4. 最后使用出栈函数获取结果,因为这里为数值,所以使用 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吧,码字不易,请多多支持)

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

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

相关推荐
午言若34 分钟前
MYSQL 架构
c++·mysql
WPG大大通1 小时前
基于DIODES AP43781+PI3USB31531+PI3DPX1207C的USB-C PD& Video 之全功能显示器连接端口方案
c语言·开发语言·计算机外设·开发板·电源·大大通
1nullptr2 小时前
lua和C API库一些记录
开发语言·lua
Jerry Nan2 小时前
Lua元表
开发语言·lua
羑悻的小杀马特2 小时前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发
闻缺陷则喜何志丹2 小时前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
析木不会编程3 小时前
【C语言】动态内存管理:详解malloc和free函数
c语言·开发语言
达帮主3 小时前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
茶猫_3 小时前
力扣面试题 39 - 三步问题 C语言解法
c语言·数据结构·算法·leetcode·职场和发展
初学者丶一起加油3 小时前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio