C++ 与 Lua 交互异常处理

一、异常处理

Lua 使用了 C 语言的 setjmp 机制,setjmp 营造了一个类似异常处理的机制。因此大多数 API 函数都可以抛出异常(即调用函数 longjmp)而不是直接返回。

这里关注的视角是 C/C++ 提供一个运行环境,Lua 作为一个脚本在其中运行,所以最为重要的是脚本的异常不影响宿主的正常运行。 要做到这一环节最重要的是考虑如何处理好其中的异常流程。归纳起来有四种交互方式:

  • C++ 调用 C++ 代码,这只是纯粹的 C++ 编码,不在 C++ 和 Lua 交互的涉猎范围,需要的话可以查阅 C++ 异常捕获的相关文章。
  • C++ 调用 Lua 代码,这一过程发生在宿主 C++ 驱动 Lua 脚本代码,Lua 代码中可能会发生异常,这一异常需要让 C++ 能够感知到并处理,这一过程会用到 lua_pcall (下面会详细分享) 。
  • Lua 调用 Lua 代码,这一过程发生在脚本代码调用脚本代码,则使用 pcall 函数(详细使用看前面分享的 《Lua 编译执行和错误处理》 文章)。或是发生在脚本代码调用自己封装的 Lua 库代码(详细可以看前面分享的 《Lua 模块与包》)。这两种调用方式都有可能发生异常,两者都能在返回值接收到异常信息,所以这里更多是业务层面需要的如何处理,即按需求是中断流程还是继续流程。
  • Lua 调用 C++ 代码,这一过程发生在 Lua 调用宿主的能力,或是 Lua 调用 C 标准库的能力,这里的标准库可能会有异常的发生,C/C++ 代码异常则需要自行捕获(就和 "第一点" 一样),或是自身流程需要抛出异常,这过程需要让 Lua 脚本感知,则需要通过 lua_error (下面会详细分享)进行抛回给 Lua 脚本 ,而不是直接 throw 之类的抛出异常。

第一小点不在我们这系列文章的涉猎范围,也就不详细分享了。

第三小点在前面的文章已经详细分享,需要的话可以翻阅之前的文章,也就不赘述了。

我们接下来基于第二点和第四点进行详细的分享。

Lua 编译执行和错误处理 mp.weixin.qq.com/s?__biz=Mzg...
Lua 模块与包 mp.weixin.qq.com/s?__biz=Mzg...

二、C++ 处理 Lua 异常

1、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,并将返回值压入栈中。
  • 如果函数调用发生错误,返回一个非零值,并将错误信息压入栈中。

2、lua_pcall 的错误处理函数

错误处理函数的格式为一个返回 int 类型,入参为 lua_State 类型的函数

cpp 复制代码
int functionName(lua_State *L);

传递给 "错误处理函数" 的 lua_State 中的栈是一个新的栈 ,和调用 lua_pcall 的函数栈并非同一个。栈中有一个元素,即 Lua 代码中抛出的异常。

可以在 "错误处理函数" 内部通过这个元素得知 Lua 代码中的异常信息,然后可以对错误信息进行加工后再压入栈,最后返回给 C/C++ 宿主程序,C/C++ 宿主程序通过 lua_State 的栈中栈顶元素即为错误信息。

Lua 的异常信息可以是 Lua 自行抛出的,例如对一个 nil 变量调用了一个方法,或是程序员因为业务流程的需要使用 error 函数(具体使用可以查看前面分享的 《Lua 编译执行和错误处理》 文章)抛出异常。所以这一异常并不一定是字符串,可以是 table 类型等 Lua 的数据类型。

3、举个例子

举一个例子捋顺这一小节的使用流程,下面例子的流程为:

  1. 通过 C++ 调用 Lua 的脚本代码
  2. 在 Lua 的脚本代码中抛出异常
  3. 然后 C++ 的错误处理函数中增加 Lua 抛出异常的堆栈信息,压入栈中返回给调用点
  4. 最后调用点,即第一点的调用处获取 Lua 脚本的运行结果,如果失败则打印错误信息

先看 C++ 调用的代码,其中 safeCallLua 是对 lua_pcall 进行了封装,可以看最后面的代码段

cpp 复制代码
lua_State *L = luaL_newstate();

// 加载函数库,否则加载的 Lua 无法使用内置库
luaL_openlibs(L);

auto luaFilePath = PROJECT_PATH + "/3、C++调用Lua的错误处理和内存分配/C++调用Lua异常处理/C运行的Lua文件.lua";

// 加载 Lua 脚本
auto loadLuaResult = luaL_loadfile(L, luaFilePath.c_str());
if (loadLuaResult) {
    printf("加载 Lua 文件失败. file: %s\n", lua_tostring(L, -1));
    return;
}

// 压入参数,传递给 Lua 脚本
lua_pushstring(L, "jiang peng yong");
lua_pushinteger(L, 29);

printf("-------- lua_pcall 调用 --------\n");
auto result = safeCallLua(L, 2, 1);

if (result == 0) {
    auto resultContent = lua_tostring(L, -1);
    printf("lua result: 运行成功,结果:%s\n", resultContent);
} else {
    auto error = lua_tostring(L, -1);
    printf("----- lua result: 运行失败,错误堆栈 -----\n%s\n", error);
}

lua_close(L);

加载的 Lua 脚本(即上面的 "C运行的Lua文件.lua" 文件)

lua 复制代码
require("string")

local name, age = ...
local content = string.format("name: %s, age: %d", name, age)
print("Lua 脚本中打印", content)

-- 第一种异常:调用了 nil 的 name 属性
local error = noneInitTable.name

-- 第二种异常:自己抛出 error 异常
-- 抛出 table 类型异常
--local errorTable = {}
--setmetatable(errorTable, {
--    __tostring = function(table)
--        return "Error table..."
--    end
--})
--error(errorTable)
-- 抛出 nil 类型异常
--error(nil)
-- 抛出 数值 类型异常
--error(10)
-- 抛出 布尔 类型异常
--error(true)
-- 抛出 函数 类型异常
--local testFunction = function()
--    return "error function"
--end
--error(testFunction)

return content

safeCallLua 函数,这里是对 "错误处理函数" 的一个封装,便于直接用于项目,具体细节可以看其中注释

cpp 复制代码
const char *getLuaErrorMsg(lua_State *L) {
    // 获取字符串和数值错误信息
    const char *msg = lua_tostring(L, -1);
    if (msg) return msg;

    // 获取 table 的 __tostring 错误信息
    if (luaL_callmeta(L, -1, "__tostring")) {
        msg = lua_tostring(L, -1);
        if (msg) return msg;
    }

    // 获取布尔值的错误信息
    if (lua_isboolean(L, -1)) {
        if (lua_toboolean(L, -1)) {
            return "true";
        } else {
            return "false";
        }
    }
    
    // nil、函数、table 没有 __tostring 元方法则会返回这个错误信息
    return "no error message";
}

// 错误处理
int errorTraceback(lua_State *L) {
    auto msg = getLuaErrorMsg(L);

    // 获取函数调用堆栈信息的错误回溯信息
    luaL_traceback(L, L, msg, 1);

    return 1;
}

/**
 * 安全调用 Lua 函数,会自动设置异常处理函数
 * @param L lua_State
 * @param narg 入参个数
 * @param nres 出参个数
 * @return 是否运行成功
 *         - 0 运行成功
 *         - 非 0 运行失败,栈顶的第一个元素(即下标 -1 元素)为错误原因
 */
int safeCallLua(lua_State *L, int narg, int nres) {
    // 计算新压入多少个参数
    int base = lua_gettop(L) - narg;
    // 压入异常处理函数
    lua_pushcfunction(L, errorTraceback);
    // 将异常处理函数插入到 base 下标的位置
    lua_insert(L, base);
    // 调用 Lua 函数
    int status = lua_pcall(L, narg, nres, base);
    // 移除异常处理函数
    lua_remove(L, base);
    return status;
}

这里最后会输出

cpp 复制代码
----- lua result: 运行失败,错误堆栈 -----
...分配/C++调用Lua异常处理/C运行的Lua文件.lua:14: attempt to index a nil value (global 'noneInitTable')
stack traceback:
	...分配/C++调用Lua异常处理/C运行的Lua文件.lua:14: in main chunk

4、luaL_traceback

在上面的代码中,运用到了这一函数,用于生成 Lua 调用堆栈的回溯信息。

lua 复制代码
void luaL_traceback(lua_State *L, lua_State *L1, const char *msg, int level);

使用 luaL_traceback 可以生成带有函数调用堆栈信息的错误回溯信息,从而更好的报告 Lua 错误。

参数:

  • L:Lua 状态机的指针。
  • L1:错误发生时的 Lua 状态机的指针。
  • msg:错误信息的前缀。
  • level:要回溯的堆栈层级数。

返回值:

会将带有错误回溯信息压入栈顶。

三、Lua 处理 C++ 异常

1、Lua 如何调用 C++ 代码

C++ 作为宿主,需要事先把需要暴露的函数注入到 Lua 中,然后 Lua 则按照正常调用方式进行使用。(具体使用流程后续会有文章详细分享,这里先有一个大概的理解,把关注点聚焦到异常处理)。另一种方式便是作为 C 标准库,Lua 通过 require 方式获取到对应的函数进行使用。

在 C/C++ 代码中,如果直接抛出异常则会让整个程序因异常终止,所以无论是注入的函数或是标准库中的函数都需要在 C/C++ 层面对异常进行处理,在需要让 Lua 感知到某个异常的时候使用 lua_error 进行异常的抛出,这样会终止 Lua 代码的运行,返回至调用 C/C++ 的调用点的错误处理函数,即第二章节的内容。

2、lua_error

cpp 复制代码
LUA_API int   (lua_error) (lua_State *L);

lua_error 会将 lua_State 的栈顶元素作为错误信息,抛出一个 Lua 的异常(类似于 Lua 中调用 error 函数,不同点在于 lua_error 是 C/C++ 进行调用)。会终止调用 C/C++ 函数的 Lua 的代码。

还有一个更为方便的函数 luaL_error ,他可以接收一个字符串和可变参数,会将组装好的字符串作为错误异常信息抛出。大多数异常是抛出一个内容为 string 的异常内容,所以使用他会更方便

cpp 复制代码
LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...);

3、举个例子

举一个例子捋顺这一小节的使用流程,下面例子的流程为:

  1. 注入 C++ 函数到 Lua 中
  2. 加载并启动 Lua 脚本
  3. 在 Lua 脚本中调用 C++ 的函数
  4. C++ 函数中抛出异常

C++ 的代码如下,cppError 为被注入函数,luaHandleCppError 为入口函数,具体每一个步骤看备注

cpp 复制代码
int cppError(lua_State *L) {
    // 第一种方式,lua_error 会将栈顶的元素作为 Lua 的错误信息
    lua_pushstring(L, "C++ throw error. 来自 lua_error");
    lua_error(L);

    // 第二种方式,luaL_error 会将字符串作为错误信息
//    luaL_error(L, "C++ throw error. 来自 luaL_error");
    return 0;
}

void luaHandleCppError() {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // 压入 cppError 函数
    lua_pushcfunction(L, cppError);
    // 将压入的函数 cppError 设置为 cppError 变量
    lua_setglobal(L, "cppError");

    std::string fname = PROJECT_PATH + "/3、C++调用Lua的错误处理和内存分配/Lua调用C++异常处理/Lua处理C++异常.lua";
    if (luaL_loadfile(L, fname.c_str()) || LuaExt::safeCallLua(L, 0, 0)) {
        printf("运行 Lua 文件失败.\n%s\n", lua_tostring(L, -1));
    }
}

下面是 "Lua处理C++异常.lua" 文件的内容

lua 复制代码
print("Lua 调用异常代码之前")

cppError()

print("Lua 调用异常代码之后")

运行之后会看到 C++ 函数抛出的异常最后在调用点被捕获

cpp 复制代码
Lua 调用异常代码之前
运行 Lua 文件失败.
C++ throw error. 来自 lua_error
stack traceback:
	[C]: in function 'cppError'
	...分配/Lua调用C++异常处理/Lua处理C++异常.lua:9: in main chunk

四、紧急函数

当 lua 发生了未捕获的异常时,会调用 panic 函数,然后调用 abort() 退出进程。

可以通过 lua_atpanic 设置 panic 方法,在方法中收集日志等操作,或是跳转到恢复点让 panic 函数永不返回,因为 panic 函数一旦返回,程序就终止了,但这不是推荐的用法

cpp 复制代码
LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf);

参数 panicf 是一个接收 lua_State 指针参数,返回 int 类型的函数,格式如下:

cpp 复制代码
typedef int (*lua_CFunction) (lua_State *L);

举个例子:

C++ 中设置 panic ,并加载一个有异常的 Lua 脚本,然后用不保护的模式 lua_call 运行。

cpp 复制代码
int panicHandle(lua_State *L) {
    const char *error = lua_tostring(L, -1);
    printf("Lua panic: %s\n", error);

    // 可以进行其他自定义操作,如记录日志、释放资源等

    return 0;
}

void panicMain() {
    lua_State *L = luaL_newstate();

    auto luaFilePath = PROJECT_PATH + "/3、C++调用Lua的错误处理和内存分配/紧急函数/Lua异常.lua";

    // 加载 Lua 脚本
    auto loadLuaResult = luaL_loadfile(L, luaFilePath.c_str());
    if (loadLuaResult) {
        printf("加载 Lua 文件失败. file: %s\n", lua_tostring(L, -1));
        return;
    }

    lua_atpanic(L, panicHandle);
    lua_call(L, 0, 0);

    lua_close(L);
}

"Lua异常.lua" 文件内容

lua 复制代码
local error = noneInitTable.name

运行后可以看到

cpp 复制代码
Lua panic: ...错误处理和内存分配/紧急函数/Lua异常.lua:7: attempt to index a nil value (global 'noneInitTable')

五、写在最后

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

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

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

相关推荐
wangjing_05222 小时前
C语言练习.if.else语句.strstr
c语言·开发语言
时光の尘5 小时前
C语言菜鸟入门·关键字·int的用法
c语言·开发语言·数据结构·c++·单片机·链表·c
C++忠实粉丝5 小时前
计算机网络socket编程(6)_TCP实网络编程现 Command_server
网络·c++·网络协议·tcp/ip·计算机网络·算法
Edward-tan5 小时前
c语言数据结构与算法--简单实现线性表(顺序表+链表)的插入与删除
c语言·开发语言·链表
武昌库里写JAVA6 小时前
一文读懂Redis6的--bigkeys选项源码以及redis-bigkey-online项目介绍
c语言·开发语言·数据结构·算法·二维数组
禊月初三6 小时前
LeetCode 4.寻找两个中序数组的中位数
c++·算法·leetcode
程序员与背包客_CoderZ7 小时前
C++设计模式——Abstract Factory Pattern抽象工厂模式
c语言·开发语言·c++·设计模式·抽象工厂模式
fancc椰7 小时前
C++基础入门篇
开发语言·c++
晚安,cheems8 小时前
c++(入门)
开发语言·c++
Mcworld8578 小时前
C语言:strcpy
c语言·开发语言