考古 《【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);

参数说明

  1. lua_State *L: Lua 状态机的指针,表示当前的 Lua 环境。

  2. lua_Hook f: 指向钩子函数的指针。钩子函数的原型如下:

    void (*lua_Hook)(lua_State *L, lua_Debug *ar);
    
    • lua_State *L: 当前的 Lua 状态。
    • lua_Debug *ar: 指向 lua_Debug 结构的指针,包含关于当前执行状态的信息。
  3. int mask: 指定钩子触发的事件类型,可以是以下值的组合:

    • LUA_MASKLINE: 行钩子,在每执行一行代码后触发。
    • LUA_MASKCOUNT: 计数钩子,每执行指定数量的指令后触发。
    • LUA_MASKCALL: 函数调用钩子,在函数调用时触发。
    • LUA_MASKRET: 函数返回钩子,在函数返回时触发。
  4. 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;
}

说明

  1. 钩子函数 myHook:

    • 这个函数在每执行一行代码时被调用,使用 lua_getinfo 获取当前行号并打印。
  2. 设置钩子:

    • lua_sethook(L, myHook, LUA_MASKLINE, 0)myHook 设置为行钩子。每当 Lua 执行一行代码时,myHook 将被调用。
  3. 执行 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;
}

说明

  1. 全局变量 stopLoop:

    • 使用 std::atomic<bool> 来控制循环是否中断。这个变量在 C++ 中被设置为 true 时,钩子函数会抛出错误,从而中断 Lua 的执行。
  2. 钩子函数 myHook:

    • 这个函数在每执行一行代码时被调用。它检查 stopLoop 的值,如果为 true,则调用 luaL_error 抛出错误,导致 Lua 脚本中断。
  3. 设置钩子:

    • 使用 lua_sethook(L, myHook, LUA_MASKLINE, 0)myHook 设置为行钩子。
  4. 执行 Lua 代码:

    • 在 Lua 中定义了一个简单的循环函数 myFunction,每次迭代打印当前迭代次数并模拟工作(使用 os.execute 暂停 1 秒)。
  5. 中断循环:

    • 在主线程中,等待 3 秒后设置 stopLooptrue,这将导致钩子函数抛出错误,从而中断 Lua 的执行。

注意事项

  • 错误处理: 在 Lua 中抛出错误后,确保在 C++ 中捕获并处理这些错误,以避免程序崩溃。
  • 性能影响: 使用钩子可能会影响性能,尤其是在高频率调用的情况下。应谨慎使用。
  • 多线程: 如果在多线程环境中使用,确保对共享变量的访问是线程安全的。

通过这种方式,你可以在 Lua 脚本中使用 lua_sethook 设置钩子,以便在特定条件下中断循环。


业精于勤荒于嬉。否,勤于嬉戏,将此当做喜爱的游戏,会有不一样的感受~诸君共勉。

12-16 14:42