sol2
sol2 copied to clipboard
how to make sol::thread running in sandbox
I am a lua newbie.
This is not a bug report nor a feature request.
The problem maybe simple for you guys but really drive me nuts, that how to make sol::thread run in sandbox.
I think many people trying to figure out this in a gentle way, as following links: https://blog.rubenwardy.com/2020/07/26/sol3-script-sandbox/ https://stackoverflow.com/a/24358483/1490269 https://stackoverflow.com/questions/57030514/changing-the-env-of-a-lua-thread-using-c-api
Let me re-state it: We have one lua state to interact with thousands of senders:
sol::state lua;
lua.open_libraries();
lua.script_file("user_defined_functions_and_variables.lua");
create_thousands_of_threads(lua); // one thread per sender
while(!quit){
auto [event, event_sender] = wait_event();
auto &th = find_thread(lua, event_sender);
th.resume(event);
}
The lua state owns thousands of thread/coroutine, but this event-driven is sequential, means thread/coroutine will sequentially get the event to drive the yield/resume.
And each thread/coroutine yield/resume with their own environment, means:
- Every
thread/coroutinehas a localized global variable table_G_sandbox, and it should override the default_G. - When
thread/coroutineread-accesses a global variable, it firstly try to find in the_G_sandbox, if not find then try to find in_G, if still not find, returnnil. - When
thread/coroutinewrite-access to a global variable, it firstly try to find in the_G_sandbox, access it if found, else- if the accessing function is user-defined, create new global variable in
_G_sandbox. - if this access is from lua standard libraries, report error and crash out. (optional if hard to implement)
- if the accessing function is user-defined, create new global variable in
When you finish the read, you know I want each thread/coroutine to run in sandbox that won't interfere with each other. They can read-access existing global variables, but shall not write/modify it, alternatively it should be in a localized _G_sandbox.
I currently have an implementation, looks working (or has bug I am not aware of), question is:
- is this the supposed way for sol2 to implement it?
- it uses raw lua functions
lua_rawgeti/lua_rawsetinot through sol2 interface, can this mess up any sol2 internal states? - Do we have better/simplier way to implement with sol2?
#include<bits/stdc++.h>
#include "sol/sol.hpp"
void checkError(const sol::protected_function_result &pfr)
{
if(pfr.valid()){
return;
}
const sol::error err = pfr;
std::stringstream errStream(err.what());
std::string errLine;
while(std::getline(errStream, errLine, '\n')){
std::cout << "callback error: " << errLine << std::endl;
}
}
struct LuaThreadRunner
{
sol::thread runner;
sol::coroutine callback;
LuaThreadRunner(sol::state &lua, const std::string &entry)
: runner(sol::thread::create(lua.lua_state()))
, callback(sol::state_view(runner.state())[entry])
{}
};
int main()
{
sol::state lua;
lua.open_libraries();
lua.script(R"(
local _G = _G
local error = error
local coroutine = coroutine
local _G_sandbox = {}
function clearTLSTable()
local threadId, inMainThread = coroutine.running()
if inMainThread then
error('call clearTLSTable in main thread')
else
_G_sandbox[threadId] = nil
end
end
replaceEnv_metatable = {
__index = function(table, key)
local threadId, inMainThread = coroutine.running()
if not inMainThread then
if _G_sandbox[threadId] ~= nil and _G_sandbox[threadId][key] ~= nil then
return _G_sandbox[threadId][key]
end
end
return _G[key]
end,
__newindex = function(table, key, value)
local threadId, inMainThread = coroutine.running()
if inMainThread then
_G[key] = value
else
if _G_sandbox[threadId] == nil then
_G_sandbox[threadId] = {}
end
_G_sandbox[threadId][key] = value
end
end
}
)");
sol::environment replaceEnv(lua, sol::create);
replaceEnv[sol::metatable_key] = sol::table(lua["replaceEnv_metatable"]);
// idea from: https://blog.rubenwardy.com/2020/07/26/sol3-script-sandbox/
// set replaceEnv as default environment, otherwise I don't know how to setup replaceEnv to thread/coroutine
lua_rawgeti(lua.lua_state(), LUA_REGISTRYINDEX, replaceEnv.registry_index());
lua_rawseti(lua.lua_state(), LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
lua.script(R"(
function coth_main(runner)
local threadId, mainThread = coroutine.running()
if mainThread then
error('coth_main(runner) called in main thread', runner)
end
-- test require
-- require should still work and accesses global variable: package
local mod = require('io')
print(mod)
coroutine.yield()
counter = 0 -- localized global varible tested
counterMax = 10 --
while counter < counterMax do
print(string.format('runner %d counter %d, you can resume %d more times', runner, counter, counterMax - counter - 1))
counter = counter + 1
coroutine.yield()
end
clearTLSTable()
end
)");
LuaThreadRunner runner1(lua, "coth_main");
checkError(runner1.callback(1));
LuaThreadRunner runner2(lua, "coth_main");
checkError(runner2.callback(2));
const auto fnResume = [](auto &callback, int index)
{
if(callback){
checkError(callback());
}
else{
std::cout << "runner " << index << " has exited" << std::endl;
}
};
while(runner1.callback || runner2.callback){
int event_from = 0;
std::cout << "wait event from: ";
std::cin >> event_from;
switch(event_from){
case 1: fnResume(runner1.callback, 1); break;
case 2: fnResume(runner2.callback, 2); break;
default: std::cout << "no runner " << event_from << std::endl;
}
}
return 0;
}