ThePhD / sol2

Sol3 (sol2 v3.0) - a C++ <-> Lua API wrapper with advanced features and top notch performance - is here, and it's great! Documentation:
http://sol2.rtfd.io/
MIT License
4.16k stars 504 forks source link

Interrupting execution #1375

Closed anthonyclark-as closed 2 years ago

anthonyclark-as commented 2 years ago

ThePhD,

Thanks so much for sol2. Is it possible to terminate a function call initiated from the C++ interface? Imagine I invoke a function lua["run"](); but want to eventually cancel or abort. Of course I can detach an OS thread then terminate it, but I'm curious if there exists a sol2-idiomatic way to do this.

I've previously done this with the LUA C API and a debug hook for counting instructions and checking some external global stop variables. I tried to get that to integrate with sol2 but it seems it'll take some work.

Thanks!

anthonyclark-as commented 2 years ago

Following up. Is this a smart way to do this?

Summary:

  1. sol::state in a class. That class has an operator() that runs in a thread. Essential 1:1 thread to state
  2. Store this in each state so we can access in a LUA hook. Could have used a global std::map<> or something...
  3. Install a LUA_MASKCOUNT hook against each state
  4. When hook happens, using this from the state to check a field
  5. If the field is true (in this case), call luaL_error but I'm not sure if this is safe to do. The result of this is that the sol::protected_function "run" has a proper exit path. I'm hoping this is safe to do while the function is being "yielded" by a LUA internal convention ; hopefully this means it's "safe" to inject an error call in the middle.
#include <cassert>
#include <csignal>
#include <atomic>
#include <vector>
#include <map>
#include <string>
#include <thread>

//#include "asi/lua/lua.hh"

#define SOL_ALL_SAFETIES_ON 1
#include "sol/sol.hpp"

std::atomic_bool gStop{false};

static void yield_hook(lua_State* L, lua_Debug *ar);

struct LuaTask
{
    std::unique_ptr<sol::state> m_lua;
    std::atomic_bool & m_stop;

    LuaTask(std::atomic_bool & stop) :
        m_stop{stop}
    {
        m_lua = std::make_unique<sol::state>();
        m_lua->open_libraries(sol::lib::base);

        // store this in the lua state so the yield hook can
        // get it out. Maybe there is a better way
        (*m_lua)["__tcb"] = this;

        ::lua_sethook(m_lua->lua_state(),
                      &yield_hook, LUA_MASKCOUNT, 50 /* bigger? */);

        // example
        m_lua->script(R"(
            x = 0
            function run()
                while true do
                    x = x + 1
                end
            end
        )");
    }

    void operator()()
    {
        auto result = sol::protected_function{(*m_lua)["run"]}();

        if (! result.valid()) {
            // Call failed
            sol::error err = result;
            std::string what = err.what();
            std::cout << what << std::endl;
        }

        // just get some global state to show that run() was
        // working as intented. TODO I did print() but it was
        // hogging stdio to a point that was confusing.
        auto x = (*m_lua)["x"].get<int>();
        std::cout << "Run is done: X? " << x << std::endl;
    }

    auto & signal() const {
        return m_stop;
    }
};

static void yield_hook(lua_State* L, lua_Debug *ar)
{
    sol::state_view state{L};

    // get the object out, safety?
    LuaTask * that = state["__tcb"];

    // is calling luaL_error OK?
    if (that->signal()) {
        ::luaL_error(L, "Aborting via signal");
    }

    std::this_thread::yield();
}

int main()
{
    std::signal(SIGINT, [](int) { gStop = true; });

    LuaTask task{gStop};

    // call operator()
    std::thread task_thread{std::ref(task)};

    // simulate main thread work
    while (! gStop)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "MAIN THREAD POLL\n";
    }

    task_thread.join();
}

Edit: some reference output

$ ./a.out 
MAIN THREAD POLL
MAIN THREAD POLL
MAIN THREAD POLL
MAIN THREAD POLL
^CAborting via signal
stack traceback:
        [string "..."]:5: in function 'base.run'
Run is done: X? 42231649
MAIN THREAD POLL