考古 《【Qt项目实战】使用脚本拓展CPP应用程序(2)——Lua脚本及编辑器》
考古 《【Qt项目实战 】:使用脚本拓展CPP应用程序(1)——Lua脚本及编辑器》
在本系列的前两篇我们介绍了Lua脚本编辑器的简单创建,以及Lua和C++接口的相互调用过程。本章作为前文的优化篇,介绍如何实现在C++端,优雅的中断Lua脚本中的循环过程。
一、循环中断控制
将Lua作为生产力辅助工具,我们显然不能限制只执行单一脚本,所以在多线程环境下并发运行多个Lua脚本是必备的(~这将作为我们后续章节的话题);某些场景下,我们需要在Lua脚本中使用循环方式来执行某些指令,譬如特殊状态寄存器的循环监听等。
那么,自然涉及到一个问题,如何在程序结束时,优雅的中断Lua脚本中的循环控制,进而优雅的结束线程,释放资源,优雅的关闭进程呢?
答案很简单,埋点!
单就C++的程序而言,你在循环中也会这样写:
while(flag){
do_something();
::Sleep(100);
}
void setFlag(bool flag){...}
通过控制标识符,轻松解决循环中断的问题。回到Lua脚本中,其实也是一样的策略。
可能对于新手朋友来讲,唯一麻烦的是,C++来控制Lua脚本的循环中断,听起来有点头大,其实一点也不麻烦。
二、思路及代码示例
2.1 思路1:通过标识符埋点控制循环中断
先上代码,我们有这样一段Lua脚本,其中使用了循环:
local utils = QtUtils() -- 这是我们在C++注册的类,提供一些接口给Lua调用
while true do
if checkStop() then
print("Stopping script")
break
end
print("Running...")
utils:luaCallSleepMs(500)
end
从上面的脚本来看,其实也是通过Flag标识符来控制循环中断。这样,思路就很清楚了,我们在C++的类型中,定义一个bool flag
的成员变量,提供checkStop()
方法给Lua来做检测,并提供stopScript()
方法允许修改变量的值。
以下,给出简单的代码示例:
class QtBridgeLua : public QObject
{
Q_OBJECT
public:
void luaCallQtLoadCode(QString code);
void stopScript();
static int luacheckStop(lua_State *L); // 注册给Lua调用
signals:
void finished(); // 可以在脚本执行完成时发送此信号
public:
bool stopFlag;
};
void QtBridgeLua::luaCallQtLoadCode(QString code)
{
stopFlag = false; // 开始状态
if (luaL_dostring(L, code.toStdString().c_str()) != LUA_OK) {
const char *error = lua_tostring(L, -1);
emit lua_print_info(QString("Lua Error: ") + QString::fromUtf8(error)); // 这里我们在系列前文中已经讲过,不赘述
lua_pop(L, 1); // 清除错误信息
}
stopFlag = false; // 结束状态
emit finished();
}
int QtBridgeLua::luacheckStop(lua_State *L)
{
lua_getglobal(L, "qtBridgeLuaInstance");
QtBridgeLua *instance = static_cast<QtBridgeLua*>(lua_touserdata(L, -1));
lua_pop(L, 1);
lua_pushboolean(L, instance->stopFlag);
return 1;
}
void QtBridgeLua::stopScript()
{
stopFlag = true;
}
多线程,看情况加锁。~是不是很简单
2.2 思路2:通过 lua hook钩子触发中断
借助lua_sethook
钩子我们可以实现中断。
先简单了解下lua钩子
lua_sethook
是 Lua C API 中的一个函数,用于设置钩子函数,以便在特定事件发生时调用该函数。钩子可以用于调试、性能分析、代码覆盖率等场景。以下是 lua_sethook
函数的详细使用说明。
函数原型
void lua_sethook(lua_State *L, lua_Hook f, int mask, int count);
参数说明
-
lua_State *L
: Lua 状态机的指针,表示当前的 Lua 环境。 -
lua_Hook f
: 指向钩子函数的指针。钩子函数的原型如下:void (*lua_Hook)(lua_State *L, lua_Debug *ar);
lua_State *L
: 当前的 Lua 状态。lua_Debug *ar
: 指向lua_Debug
结构的指针,包含关于当前执行状态的信息。
-
int mask
: 指定钩子触发的事件类型,可以是以下值的组合:LUA_MASKLINE
: 行钩子,在每执行一行代码后触发。LUA_MASKCOUNT
: 计数钩子,每执行指定数量的指令后触发。LUA_MASKCALL
: 函数调用钩子,在函数调用时触发。LUA_MASKRET
: 函数返回钩子,在函数返回时触发。
-
int count
: 指定计数钩子的触发频率。每执行count
条指令后触发一次。如果count
为 0,则不使用计数钩子。
使用示例
以下是一个使用 lua_sethook
的示例,展示了如何设置行钩子和计数钩子。
#include <lua.hpp>
#include <iostream>
// 钩子函数
void myHook(lua_State *L, lua_Debug *ar) {
// 获取当前执行的行号
lua_getinfo(L, "l", ar);
std::cout << "Line executed: " << ar->currentline << std::endl;
}
int main() {
lua_State *L = luaL_newstate(); // 创建 Lua 状态
luaL_openlibs(L); // 打开 Lua 库
// 设置行钩子
lua_sethook(L, myHook, LUA_MASKLINE, 0);
// 执行 Lua 代码
luaL_dostring(L, "for i = 1, 5 do print(i) end");
lua_close(L); // 关闭 Lua 状态
return 0;
}
说明
-
钩子函数
myHook
:- 这个函数在每执行一行代码时被调用,使用
lua_getinfo
获取当前行号并打印。
- 这个函数在每执行一行代码时被调用,使用
-
设置钩子:
lua_sethook(L, myHook, LUA_MASKLINE, 0)
将myHook
设置为行钩子。每当 Lua 执行一行代码时,myHook
将被调用。
-
执行 Lua 代码:
- 使用
luaL_dostring
执行一段 Lua 代码。在这个示例中,Lua 将打印数字 1 到 5,每执行一行都会触发钩子。
- 使用
注意事项
- 性能影响: 使用钩子可能会影响性能,尤其是在高频率调用的情况下。应谨慎使用。
- 调试信息: 在钩子函数中,可以使用
lua_getinfo
获取关于当前执行状态的详细信息。 - 多次设置: 可以多次调用
lua_sethook
来更改钩子函数或事件类型。
以下,给出一些通过钩子触发中断的思路:
#include <lua.hpp>
#include <iostream>
#include <atomic>
#include <thread>
// 全局变量,用于控制循环是否中断
std::atomic<bool> stopLoop(false);
// 钩子函数
void myHook(lua_State *L, lua_Debug *ar) {
// 检查是否需要中断循环
if (stopLoop.load()) {
luaL_error(L, "Loop interrupted by hook"); // 抛出错误以中断循环
}
}
int main() {
lua_State *L = luaL_newstate(); // 创建 Lua 状态
luaL_openlibs(L); // 打开 Lua 库
// 设置行钩子
lua_sethook(L, myHook, LUA_MASKLINE, 0);
// 执行 Lua 代码
luaL_dostring(L, R"(
function myFunction()
for i = 1, 10 do
print("Iteration: " .. i)
-- 模拟一些工作
os.execute("sleep 1") -- 在 Windows 上使用 os.execute("timeout 1")
end
end
myFunction()
)");
// 模拟运行一段时间后中断循环
std::this_thread::sleep_for(std::chrono::seconds(3));
stopLoop.store(true); // 设置中断标志
// 处理 Lua 错误
try {
luaL_dostring(L, "myFunction()");
} catch (const std::exception& e) {
std::cerr << "Lua Error: " << e.what() << std::endl;
}
lua_close(L); // 关闭 Lua 状态
return 0;
}
说明
-
全局变量
stopLoop
:- 使用
std::atomic<bool>
来控制循环是否中断。这个变量在 C++ 中被设置为true
时,钩子函数会抛出错误,从而中断 Lua 的执行。
- 使用
-
钩子函数
myHook
:- 这个函数在每执行一行代码时被调用。它检查
stopLoop
的值,如果为true
,则调用luaL_error
抛出错误,导致 Lua 脚本中断。
- 这个函数在每执行一行代码时被调用。它检查
-
设置钩子:
- 使用
lua_sethook(L, myHook, LUA_MASKLINE, 0)
将myHook
设置为行钩子。
- 使用
-
执行 Lua 代码:
- 在 Lua 中定义了一个简单的循环函数
myFunction
,每次迭代打印当前迭代次数并模拟工作(使用os.execute
暂停 1 秒)。
- 在 Lua 中定义了一个简单的循环函数
-
中断循环:
- 在主线程中,等待 3 秒后设置
stopLoop
为true
,这将导致钩子函数抛出错误,从而中断 Lua 的执行。
- 在主线程中,等待 3 秒后设置
注意事项
- 错误处理: 在 Lua 中抛出错误后,确保在 C++ 中捕获并处理这些错误,以避免程序崩溃。
- 性能影响: 使用钩子可能会影响性能,尤其是在高频率调用的情况下。应谨慎使用。
- 多线程: 如果在多线程环境中使用,确保对共享变量的访问是线程安全的。
通过这种方式,你可以在 Lua 脚本中使用 lua_sethook
设置钩子,以便在特定条件下中断循环。
业精于勤荒于嬉。否,勤于嬉戏,将此当做喜爱的游戏,会有不一样的感受~诸君共勉。