diff --git a/data/lua/core/plugin.lua b/data/lua/core/plugin.lua index 14abbf3f513..4d4a937d806 100644 --- a/data/lua/core/plugin.lua +++ b/data/lua/core/plugin.lua @@ -4,7 +4,6 @@ if wesnoth.kernel_type() == "Application Lua Kernel" then print("Loading plugin module...") - wesnoth.plugin = {} ---Yields control back to the game until the next slice. ---@return WMLTable diff --git a/src/play_controller.cpp b/src/play_controller.cpp index 1e7080120c6..6c2aa4e391e 100644 --- a/src/play_controller.cpp +++ b/src/play_controller.cpp @@ -280,6 +280,7 @@ void play_controller::init(const config& level) plugins_context_->set_callback("save_game", [this](const config& cfg) { save_game_auto(cfg["filename"]); }, true); plugins_context_->set_callback("save_replay", [this](const config& cfg) { save_replay_auto(cfg["filename"]); }, true); plugins_context_->set_callback("quit", [](const config&) { throw_quit_game_exception(); }, false); + plugins_context_->set_callback_execute(*resources::lua_kernel); plugins_context_->set_accessor_string("scenario_name", [this](config) { return get_scenario_name(); }); }); } diff --git a/src/scripting/application_lua_kernel.cpp b/src/scripting/application_lua_kernel.cpp index 7bdba18c26a..cef496ce927 100644 --- a/src/scripting/application_lua_kernel.cpp +++ b/src/scripting/application_lua_kernel.cpp @@ -36,6 +36,7 @@ #include "scripting/lua_preferences.hpp" #include "scripting/plugins/context.hpp" #include "scripting/plugins/manager.hpp" +#include "scripting/push_check.hpp" #ifdef DEBUG_LUA #include "scripting/debug_lua.hpp" @@ -52,7 +53,6 @@ #include "lua/wrapper_lauxlib.h" - static lg::log_domain log_scripting_lua("scripting/lua"); #define DBG_LUA LOG_STREAM(debug, log_scripting_lua) #define LOG_LUA LOG_STREAM(info, log_scripting_lua) @@ -93,6 +93,8 @@ static int intf_delay(lua_State* L) return 0; } +static int intf_execute(lua_State* L); + application_lua_kernel::application_lua_kernel() : lua_kernel_base() { @@ -108,6 +110,14 @@ application_lua_kernel::application_lua_kernel() // Create the preferences table. cmd_log_ << lua_preferences::register_table(mState); + + // Create the wesnoth.plugin table + luaW_getglobal(mState, "wesnoth"); + lua_newtable(mState); + lua_pushcfunction(mState, intf_execute); + lua_setfield(mState, -2, "execute"); + lua_setfield(mState, -2, "plugin"); + lua_pop(mState, 1); } application_lua_kernel::thread::thread(application_lua_kernel& owner, lua_State * T) : owner_(owner), T_(T), started_(false) {} @@ -216,6 +226,7 @@ application_lua_kernel::thread * application_lua_kernel::load_script_from_file(c struct lua_context_backend { std::vector requests; + lua_kernel_base* execute; bool valid; lua_context_backend() @@ -257,6 +268,47 @@ static int impl_context_accessor(lua_State * L, std::shared_ptr* context = static_cast*>(lua_touserdata(L, EXEC)); + luaW_pushconfig(L, data); + impl_context_backend(L, *context, "execute"); + } catch(luafunc_serialize_error& e) { + lua_pushboolean(L, false); + lua_pushstring(L, e.what()); + return 2; + } + lua_pushboolean(L, true); + return 1; +} +bool luaW_copy_upvalues(lua_State* L, const config& cfg); application_lua_kernel::request_list application_lua_kernel::thread::run_script(const plugins_context & ctxt, const std::vector & queue) { // There are two possibilities: (1) this is the first execution, and the C function is the only thing on the stack @@ -281,6 +333,11 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script( lua_cpp::push_function(T_, std::bind(&impl_context_backend, std::placeholders::_1, this_context_backend, key)); lua_settable(T_, -3); } + if(ctxt.execute_kernel_) { + lua_pushstring(T_, "execute"); + lua_pushlightuserdata(T_, &this_context_backend); + lua_settable(T_, -3); + } // Now we have to create the info object (context accessors). It is arranged as a table of boost functions. lua_newtable(T_); // this will be the info table @@ -351,8 +408,59 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script( application_lua_kernel::request_list results; for (const plugins_manager::event & req : this_context_backend->requests) { + if(ctxt.execute_kernel_ && req.name == "execute") { + results.push_back([this, lk = ctxt.execute_kernel_, data = req.data]() { + auto result = lk->run_binary_lua_tag(data); + int ref = result["ref"]; + auto func = result.mandatory_child("executed"); + result.remove_children("executed"); + result.remove_attribute("ref"); + plugins_manager::get()->notify_event(result["name"], result); + lua_rawgeti(T_, LUA_REGISTRYINDEX, ref); + luaW_copy_upvalues(T_, func); + luaL_unref(T_, LUA_REGISTRYINDEX, ref); + lua_pop(T_, 1); + return true; + }); + continue; + } results.push_back(std::bind(ctxt.callbacks_.find(req.name)->second, req.data)); //results.emplace_back(ctxt.callbacks_.find(req.name)->second, req.data); } return results; } + +bool luaW_copy_upvalues(lua_State* L, const config& cfg) +{ + if(auto upvalues = cfg.optional_child("upvalues")) { + lua_pushvalue(L, -1); // duplicate function because lua_getinfo will pop it + lua_Debug info; + lua_getinfo(L, ">u", &info); + int funcindex = lua_absindex(L, -1); + for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) { + std::string_view name = lua_getupvalue(L, funcindex, i); + if(name == "_ENV") { + lua_pushglobaltable(L); + } else if(upvalues->has_attribute(name)) { + luaW_pushscalar(L, (*upvalues)[name]); + } else if(upvalues->has_child(name)) { + const auto& child = upvalues->mandatory_child(name); + if(child["upvalue_type"] == "array") { + auto children = upvalues->child_range(name); + lua_createtable(L, children.size(), 0); + for(const auto& cfg : children) { + luaW_pushscalar(L, cfg["value"]); + lua_rawseti(L, -2, lua_rawlen(L, -2) + 1); + } + } else if(child["upvalue_type"] == "config") { + luaW_pushconfig(L, child); + } else if(child["upvalue_type"] == "function") { + luaW_copy_upvalues(L, child); + lua_pushvalue(L, -1); + } + } else continue; + lua_setupvalue(L, funcindex, i); + } + } + return true; +} diff --git a/src/scripting/lua_kernel_base.cpp b/src/scripting/lua_kernel_base.cpp index e0d64787dd5..afb8a2e941c 100644 --- a/src/scripting/lua_kernel_base.cpp +++ b/src/scripting/lua_kernel_base.cpp @@ -1065,10 +1065,10 @@ bool lua_kernel_base::protected_call(lua_State * L, int nArgs, int nRets, error_ return true; } -bool lua_kernel_base::load_string(char const * prog, const std::string& name, error_handler e_h) +bool lua_kernel_base::load_string(const std::string& prog, const std::string& name, error_handler e_h, bool allow_unsafe) { // pass 't' to prevent loading bytecode which is unsafe and can be used to escape the sandbox. - int errcode = luaL_loadbufferx(mState, prog, strlen(prog), name.empty() ? prog : name.c_str(), "t"); + int errcode = luaL_loadbufferx(mState, prog.c_str(), prog.size(), name.empty() ? prog.c_str() : name.c_str(), allow_unsafe ? "tb" : "t"); if (errcode != LUA_OK) { char const * msg = lua_tostring(mState, -1); std::string message = msg ? msg : "null string"; @@ -1101,6 +1101,147 @@ void lua_kernel_base::run_lua_tag(const config& cfg) } this->run(cfg["code"].str().c_str(), cfg["name"].str(), nArgs); } + +config luaW_serialize_function(lua_State* L, int func) +{ + if(lua_iscfunction(L, func)) { + throw luafunc_serialize_error("cannot serialize C function"); + } + if(!lua_isfunction(L, func)) { + throw luafunc_serialize_error("cannot serialize callable non-function"); + } + config data; + lua_Debug info; + lua_pushvalue(L, func); // push copy of function because lua_getinfo will pop it + lua_getinfo(L, ">u", &info); + data["params"] = info.nparams; + luaW_getglobal(L, "string", "dump"); + lua_pushvalue(L, func); + lua_call(L, 1, 1); + data["code"] = lua_check(L, -1); + lua_pop(L, 1); + config upvalues; + for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) { + std::string_view name = lua_getupvalue(L, func, i); + if(name == "_ENV") { + upvalues.add_child(name)["upvalue_type"] = "_ENV"; + continue; + } + int idx = lua_absindex(L, -1); + switch(lua_type(L, idx)) { + case LUA_TBOOLEAN: case LUA_TNUMBER: case LUA_TSTRING: + luaW_toscalar(L, idx, upvalues[name]); + break; + case LUA_TFUNCTION: + upvalues.add_child(name, luaW_serialize_function(L, idx))["upvalue_type"] = "function"; + break; + case LUA_TTABLE: + if(config cfg; luaW_toconfig(L, idx, cfg)) { + upvalues.add_child(name, cfg)["upvalue_type"] = "config"; + break; + } else { + for(size_t i = 1; i <= lua_rawlen(L, -1); i++, lua_pop(L, 1)) { + lua_rawgeti(L, idx, i); + config& cfg = upvalues.add_child(name); + luaW_toscalar(L, -1, cfg["value"]); + cfg["upvalue_type"] = "array"; + } + bool found_non_array = false; + for(lua_pushnil(L); lua_next(L, idx); lua_pop(L, 1)) { + if(lua_type(L, -2) != LUA_TNUMBER) { + found_non_array = true; + break; + } + } + if(!found_non_array) break; + } + [[fallthrough]]; + default: + std::ostringstream os; + os << "cannot serialize function with upvalue " << name << " = "; + luaW_getglobal(L, "wesnoth", "as_text"); + lua_pushvalue(L, idx); + lua_call(L, 1, 1); + os << luaL_checkstring(L, -1); + lua_pushboolean(L, false); + throw luafunc_serialize_error(os.str()); + } + } + if(!upvalues.empty()) data.add_child("upvalues", upvalues); + return data; +} + +bool lua_kernel_base::load_binary(const config& cfg, error_handler eh) +{ + if(!load_string(cfg["code"].str(), cfg["name"], eh, true)) return false; + if(auto upvalues = cfg.optional_child("upvalues")) { + lua_pushvalue(mState, -1); // duplicate function because lua_getinfo will pop it + lua_Debug info; + lua_getinfo(mState, ">u", &info); + int funcindex = lua_absindex(mState, -1); + for(int i = 1; i <= info.nups; i++) { + std::string_view name = lua_getupvalue(mState, funcindex, i); + lua_pop(mState, 1); // we only want the upvalue's name, not its value + if(name == "_ENV") { + lua_pushglobaltable(mState); + } else if(upvalues->has_attribute(name)) { + luaW_pushscalar(mState, (*upvalues)[name]); + } else if(upvalues->has_child(name)) { + const auto& child = upvalues->mandatory_child(name); + if(child["upvalue_type"] == "array") { + auto children = upvalues->child_range(name); + lua_createtable(mState, children.size(), 0); + for(const auto& cfg : children) { + luaW_pushscalar(mState, cfg["value"]); + lua_rawseti(mState, -2, lua_rawlen(mState, -2) + 1); + } + } else if(child["upvalue_type"] == "config") { + luaW_pushconfig(mState, child); + } else if(child["upvalue_type"] == "function") { + if(!load_binary(child, eh)) return false; + } + } else continue; + lua_setupvalue(mState, funcindex, i); + } + } + return true; +} + +config lua_kernel_base::run_binary_lua_tag(const config& cfg) +{ + int top = lua_gettop(mState); + try { + error_handler eh = std::bind(&lua_kernel_base::throw_exception, this, std::placeholders::_1, std::placeholders::_2 ); + if(load_binary(cfg, eh)) { + lua_pushvalue(mState, -1); + protected_call(0, LUA_MULTRET, eh); + } + } catch (const game::lua_error & e) { + cmd_log_ << e.what() << "\n"; + lua_kernel_base::log_error(e.what(), "In function lua_kernel::run()"); + config error; + error["name"] = "execute_error"; + error["error"] = e.what(); + return error; + } + config result; + result["ref"] = cfg["ref"]; + result.add_child("executed") = luaW_serialize_function(mState, top + 1); + lua_remove(mState, top + 1); + result["name"] = "execute_result"; + for(int i = top + 1; i < lua_gettop(mState); i++) { + std::string index = std::to_string(i - top); + switch(lua_type(mState, i)) { + case LUA_TNUMBER: case LUA_TBOOLEAN: case LUA_TSTRING: + luaW_toscalar(mState, i, result[index]); + break; + case LUA_TTABLE: + luaW_toconfig(mState, i, result.add_child(index)); + break; + } + } + return result; +} // Call load_string and protected call. Make them throw exceptions. // void lua_kernel_base::throwing_run(const char * prog, const std::string& name, int nArgs, bool in_interpreter) diff --git a/src/scripting/lua_kernel_base.hpp b/src/scripting/lua_kernel_base.hpp index 243b2ac2ee0..2c24d508102 100644 --- a/src/scripting/lua_kernel_base.hpp +++ b/src/scripting/lua_kernel_base.hpp @@ -32,6 +32,9 @@ public: /** Runs a [lua] tag. Doesn't throw lua_error.*/ void run_lua_tag(const config& cfg); + /** Runs a binary [lua] tag. Doesn't throw lua_error.*/ + config run_binary_lua_tag(const config& cfg); + /** Runs a plain script. Doesn't throw lua_error.*/ void run(char const *prog, const std::string& name, int nArgs = 0); @@ -125,7 +128,8 @@ protected: // Execute a protected call, taking a lua_State as argument. For functions pushed into the lua environment, this version should be used, or the function cannot be used by coroutines without segfaulting (since they have a different lua_State pointer). This version is called by the above version. static bool protected_call(lua_State * L, int nArgs, int nRets, error_handler); // Load a string onto the stack as a function. Returns true if successful, error handler is called if not. - bool load_string(char const * prog, const std::string& name, error_handler); + bool load_string(const std::string& prog, const std::string& name, error_handler, bool allow_unsafe = false); + bool load_binary(const config& func, error_handler); virtual bool protected_call(int nArgs, int nRets); // select default error handler polymorphically virtual bool load_string(char const * prog, const std::string& name); // select default error handler polymorphically @@ -146,6 +150,12 @@ private: std::vector> registered_widget_definitions_; }; +config luaW_serialize_function(lua_State* L, int func); + +struct luafunc_serialize_error : public std::runtime_error { + using std::runtime_error::runtime_error; +}; + std::vector luaW_get_attributes(lua_State* L, int idx); struct game_config_tag { diff --git a/src/scripting/plugins/context.cpp b/src/scripting/plugins/context.cpp index 73b370c6548..aeabff3a734 100644 --- a/src/scripting/plugins/context.cpp +++ b/src/scripting/plugins/context.cpp @@ -16,6 +16,7 @@ #include "scripting/plugins/context.hpp" #include "scripting/plugins/manager.hpp" +#include "scripting/lua_kernel_base.hpp" #include #include @@ -103,3 +104,7 @@ void plugins_context::set_callback(const std::string & name, std::function function, bool preserves_context); + void set_callback_execute(class lua_kernel_base& kernel); std::size_t erase_callback(const std::string & name); std::size_t clear_callbacks(); @@ -67,4 +68,5 @@ private: callback_list callbacks_; accessor_list accessors_; std::string name_; + lua_kernel_base* execute_kernel_; }; diff --git a/utils/emmylua/plugin.lua b/utils/emmylua/plugin.lua index ae3be3065f2..35ada165b2b 100644 --- a/utils/emmylua/plugin.lua +++ b/utils/emmylua/plugin.lua @@ -10,4 +10,14 @@ ---Contains accessors for the current context ---@class plugin_info ---@field name string The name of the current context. ----@field [string] plugin_accessor An accessor takes a WML table as its argument and returns a string, integer, or WML table. \ No newline at end of file +---@field [string] plugin_accessor An accessor takes a WML table as its argument and returns a string, integer, or WML table. + +---Execute a function within the current context's game state, if supported. +---Functions returning a value can request that the result be returned in an event in the next slice. +---If the function raises an error, that too will be returned as an event in the next slice. +---@param context plugin_context The current plugin context. +---@param fcn function An arbitrary function to execute. The function will be run in a different Lua kernel and thus cannot access the wesnoth.plugin module. +---@param event_name? string The name to use for the event that contains the function's result +---@return boolean #True if the function will be executed; false if unsupported +---@return string? #If the first value is false, this will hold an explanatory string +function wesnoth.plugin.execute(context, fcn, event_name) end