]> git.lizzy.rs Git - dragonfireclient.git/commitdiff
Async environment for mods to do concurrent tasks (#11131)
authorsfan5 <sfan5@live.de>
Mon, 2 May 2022 18:55:04 +0000 (20:55 +0200)
committersfan5 <sfan5@live.de>
Mon, 2 May 2022 18:56:06 +0000 (20:56 +0200)
39 files changed:
builtin/async/game.lua [new file with mode: 0644]
builtin/async/init.lua [deleted file]
builtin/async/mainmenu.lua [new file with mode: 0644]
builtin/game/async.lua [new file with mode: 0644]
builtin/game/init.lua
builtin/game/misc.lua
builtin/init.lua
doc/lua_api.txt
games/devtest/mods/unittests/async_env.lua [new file with mode: 0644]
games/devtest/mods/unittests/init.lua
games/devtest/mods/unittests/inside_async_env.lua [new file with mode: 0644]
src/map.cpp
src/map.h
src/script/common/CMakeLists.txt
src/script/common/c_internal.cpp
src/script/common/c_internal.h
src/script/common/c_packer.cpp [new file with mode: 0644]
src/script/common/c_packer.h [new file with mode: 0644]
src/script/cpp_api/s_async.cpp
src/script/cpp_api/s_async.h
src/script/lua_api/l_craft.cpp
src/script/lua_api/l_craft.h
src/script/lua_api/l_internal.h
src/script/lua_api/l_item.cpp
src/script/lua_api/l_item.h
src/script/lua_api/l_noise.cpp
src/script/lua_api/l_noise.h
src/script/lua_api/l_server.cpp
src/script/lua_api/l_server.h
src/script/lua_api/l_util.cpp
src/script/lua_api/l_util.h
src/script/lua_api/l_vmanip.cpp
src/script/lua_api/l_vmanip.h
src/script/scripting_server.cpp
src/script/scripting_server.h
src/server.cpp
src/server.h
src/serverenvironment.cpp
src/util/basic_macros.h

diff --git a/builtin/async/game.lua b/builtin/async/game.lua
new file mode 100644 (file)
index 0000000..212a33e
--- /dev/null
@@ -0,0 +1,46 @@
+core.log("info", "Initializing asynchronous environment (game)")
+
+local function pack2(...)
+       return {n=select('#', ...), ...}
+end
+
+-- Entrypoint to run async jobs, called by C++
+function core.job_processor(func, params)
+       local retval = pack2(func(unpack(params, 1, params.n)))
+
+       return retval
+end
+
+-- Import a bunch of individual files from builtin/game/
+local gamepath = core.get_builtin_path() .. "game" .. DIR_DELIM
+
+dofile(gamepath .. "constants.lua")
+dofile(gamepath .. "item_s.lua")
+dofile(gamepath .. "misc_s.lua")
+dofile(gamepath .. "features.lua")
+dofile(gamepath .. "voxelarea.lua")
+
+-- Transfer of globals
+do
+       assert(core.transferred_globals)
+       local all = core.deserialize(core.transferred_globals, true)
+       core.transferred_globals = nil
+
+       -- reassemble other tables
+       all.registered_nodes = {}
+       all.registered_craftitems = {}
+       all.registered_tools = {}
+       for k, v in pairs(all.registered_items) do
+               if v.type == "node" then
+                       all.registered_nodes[k] = v
+               elseif v.type == "craftitem" then
+                       all.registered_craftitems[k] = v
+               elseif v.type == "tool" then
+                       all.registered_tools[k] = v
+               end
+       end
+
+       for k, v in pairs(all) do
+               core[k] = v
+       end
+end
diff --git a/builtin/async/init.lua b/builtin/async/init.lua
deleted file mode 100644 (file)
index 3803994..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-
-core.log("info", "Initializing Asynchronous environment")
-
-function core.job_processor(func, serialized_param)
-       local param = core.deserialize(serialized_param)
-
-       local retval = core.serialize(func(param))
-
-       return retval or core.serialize(nil)
-end
-
diff --git a/builtin/async/mainmenu.lua b/builtin/async/mainmenu.lua
new file mode 100644 (file)
index 0000000..0e9c222
--- /dev/null
@@ -0,0 +1,9 @@
+core.log("info", "Initializing asynchronous environment")
+
+function core.job_processor(func, serialized_param)
+       local param = core.deserialize(serialized_param)
+
+       local retval = core.serialize(func(param))
+
+       return retval or core.serialize(nil)
+end
diff --git a/builtin/game/async.lua b/builtin/game/async.lua
new file mode 100644 (file)
index 0000000..469f179
--- /dev/null
@@ -0,0 +1,22 @@
+
+core.async_jobs = {}
+
+function core.async_event_handler(jobid, retval)
+       local callback = core.async_jobs[jobid]
+       assert(type(callback) == "function")
+       callback(unpack(retval, 1, retval.n))
+       core.async_jobs[jobid] = nil
+end
+
+function core.handle_async(func, callback, ...)
+       assert(type(func) == "function" and type(callback) == "function",
+               "Invalid minetest.handle_async invocation")
+       local args = {n = select("#", ...), ...}
+       local mod_origin = core.get_last_run_mod()
+
+       local jobid = core.do_async_callback(func, args, mod_origin)
+       core.async_jobs[jobid] = callback
+
+       return true
+end
+
index c5f08113bdee59872d77e5b63337701775b90d5b..68d6a10f8d164585fa65e7ed6942f7645f57c332 100644 (file)
@@ -34,5 +34,6 @@ dofile(gamepath .. "voxelarea.lua")
 dofile(gamepath .. "forceloading.lua")
 dofile(gamepath .. "statbars.lua")
 dofile(gamepath .. "knockback.lua")
+dofile(gamepath .. "async.lua")
 
 profiler = nil
index 18d5a73107befc04d0877da15f0a7e0d3bb02ba1..9f5e3312bb42077a86d9579787162b09a23c93c7 100644 (file)
@@ -235,3 +235,32 @@ end
 
 -- Used for callback handling with dynamic_add_media
 core.dynamic_media_callbacks = {}
+
+
+-- Transfer of certain globals into async environment
+-- see builtin/async/game.lua for the other side
+
+local function copy_filtering(t, seen)
+       if type(t) == "userdata" or type(t) == "function" then
+               return true -- don't use nil so presence can still be detected
+       elseif type(t) ~= "table" then
+               return t
+       end
+       local n = {}
+       seen = seen or {}
+       seen[t] = n
+       for k, v in pairs(t) do
+               local k_ = seen[k] or copy_filtering(k, seen)
+               local v_ = seen[v] or copy_filtering(v, seen)
+               n[k_] = v_
+       end
+       return n
+end
+
+function core.get_globals_to_transfer()
+       local all = {
+               registered_items = copy_filtering(core.registered_items),
+               registered_aliases = core.registered_aliases,
+       }
+       return core.serialize(all)
+end
index 7a9b5c42718002d7cc75b6d31712ce7b5b2bc1a2..8691360169379bc82ede6e9892a41442fea6a3b8 100644 (file)
@@ -56,8 +56,10 @@ elseif INIT == "mainmenu" then
        if not custom_loaded then
                dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua")
        end
-elseif INIT == "async" then
-       dofile(asyncpath .. "init.lua")
+elseif INIT == "async"  then
+       dofile(asyncpath .. "mainmenu.lua")
+elseif INIT == "async_game" then
+       dofile(asyncpath .. "game.lua")
 elseif INIT == "client" then
        dofile(clientpath .. "init.lua")
 else
index f54672db7de5626d27f40e153d715cea613d98b5..339ce8a27a351df5ab0971666bc0c176e99518ea 100644 (file)
@@ -5767,6 +5767,68 @@ Timing
 * `job:cancel()`
     * Cancels the job function from being called
 
+Async environment
+-----------------
+
+The engine allows you to submit jobs to be ran in an isolated environment
+concurrently with normal server operation.
+A job consists of a function to be ran in the async environment, any amount of
+arguments (will be serialized) and a callback that will be called with the return
+value of the job function once it is finished.
+
+The async environment does *not* have access to the map, entities, players or any
+globals defined in the 'usual' environment. Consequently, functions like
+`minetest.get_node()` or `minetest.get_player_by_name()` simply do not exist in it.
+
+Arguments and return values passed through this can contain certain userdata
+objects that will be seamlessly copied (not shared) to the async environment.
+This allows you easy interoperability for delegating work to jobs.
+
+* `minetest.handle_async(func, callback, ...)`:
+    * Queue the function `func` to be ran in an async environment.
+      Note that there are multiple persistent workers and any of them may
+      end up running a given job. The engine will scale the amount of
+      worker threads automatically.
+    * When `func` returns the callback is called (in the normal environment)
+      with all of the return values as arguments.
+    * Optional: Variable number of arguments that are passed to `func`
+* `minetest.register_async_dofile(path)`:
+    * Register a path to a Lua file to be imported when an async environment
+      is initialized. You can use this to preload code which you can then call
+      later using `minetest.handle_async()`.
+
+### List of APIs available in an async environment
+
+Classes:
+* `ItemStack`
+* `PerlinNoise`
+* `PerlinNoiseMap`
+* `PseudoRandom`
+* `PcgRandom`
+* `SecureRandom`
+* `VoxelArea`
+* `VoxelManip`
+    * only if transferred into environment; can't read/write to map
+* `Settings`
+
+Class instances that can be transferred between environments:
+* `ItemStack`
+* `PerlinNoise`
+* `PerlinNoiseMap`
+* `VoxelManip`
+
+Functions:
+* Standalone helpers such as logging, filesystem, encoding,
+  hashing or compression APIs
+* `minetest.request_insecure_environment` (same restrictions apply)
+
+Variables:
+* `minetest.settings`
+* `minetest.registered_items`, `registered_nodes`, `registered_tools`,
+  `registered_craftitems` and `registered_aliases`
+    * with all functions and userdata values replaced by `true`, calling any
+      callbacks here is obviously not possible
+
 Server
 ------
 
diff --git a/games/devtest/mods/unittests/async_env.lua b/games/devtest/mods/unittests/async_env.lua
new file mode 100644 (file)
index 0000000..aff1fc4
--- /dev/null
@@ -0,0 +1,149 @@
+-- helper
+
+core.register_async_dofile(core.get_modpath(core.get_current_modname()) ..
+       DIR_DELIM .. "inside_async_env.lua")
+
+local function deepequal(a, b)
+       if type(a) == "function" then
+               return type(b) == "function"
+       elseif type(a) ~= "table" then
+               return a == b
+       elseif type(b) ~= "table" then
+               return false
+       end
+       for k, v in pairs(a) do
+               if not deepequal(v, b[k]) then
+                       return false
+               end
+       end
+       for k, v in pairs(b) do
+               if not deepequal(a[k], v) then
+                       return false
+               end
+       end
+       return true
+end
+
+-- Object Passing / Serialization
+
+local test_object = {
+       name = "stairs:stair_glass",
+       type = "node",
+       groups = {oddly_breakable_by_hand = 3, cracky = 3, stair = 1},
+       description = "Glass Stair",
+       sounds = {
+               dig = {name = "default_glass_footstep", gain = 0.5},
+               footstep = {name = "default_glass_footstep", gain = 0.3},
+               dug = {name = "default_break_glass", gain = 1}
+       },
+       node_box = {
+               fixed = {
+                       {-0.5, -0.5, -0.5, 0.5, 0, 0.5},
+                       {-0.5, 0, 0, 0.5, 0.5, 0.5}
+               },
+               type = "fixed"
+       },
+       tiles = {
+               {name = "stairs_glass_split.png", backface_culling = true},
+               {name = "default_glass.png", backface_culling = true},
+               {name = "stairs_glass_stairside.png^[transformFX", backface_culling = true}
+       },
+       on_place = function(itemstack, placer)
+               return core.is_player(placer)
+       end,
+       sunlight_propagates = true,
+       is_ground_content = false,
+       light_source = 0,
+}
+
+local function test_object_passing()
+       local tmp = core.serialize_roundtrip(test_object)
+       assert(deepequal(test_object, tmp))
+
+       -- Circular key, should error
+       tmp = {"foo", "bar"}
+       tmp[tmp] = true
+       assert(not pcall(core.serialize_roundtrip, tmp))
+
+       -- Circular value, should error
+       tmp = {"foo"}
+       tmp[2] = tmp
+       assert(not pcall(core.serialize_roundtrip, tmp))
+end
+unittests.register("test_object_passing", test_object_passing)
+
+local function test_userdata_passing(_, pos)
+       -- basic userdata passing
+       local obj = table.copy(test_object.tiles[1])
+       obj.test = ItemStack("default:cobble 99")
+       local tmp = core.serialize_roundtrip(obj)
+       assert(type(tmp.test) == "userdata")
+       assert(obj.test:to_string() == tmp.test:to_string())
+
+       -- object can't be passed, should error
+       obj = core.raycast(pos, pos)
+       assert(not pcall(core.serialize_roundtrip, obj))
+
+       -- VManip
+       local vm = core.get_voxel_manip(pos, pos)
+       local expect = vm:get_node_at(pos)
+       local vm2 = core.serialize_roundtrip(vm)
+       assert(deepequal(vm2:get_node_at(pos), expect))
+end
+unittests.register("test_userdata_passing", test_userdata_passing, {map=true})
+
+-- Asynchronous jobs
+
+local function test_handle_async(cb)
+       -- Basic test including mod name tracking and unittests.async_test()
+       -- which is defined inside_async_env.lua
+       local func = function(x)
+               return core.get_last_run_mod(), _VERSION, unittests[x]()
+       end
+       local expect = {core.get_last_run_mod(), _VERSION, true}
+
+       core.handle_async(func, function(...)
+               if not deepequal(expect, {...}) then
+                       cb("Values did not equal")
+               end
+               if core.get_last_run_mod() ~= expect[1] then
+                       cb("Mod name not tracked correctly")
+               end
+
+               -- Test passing of nil arguments and return values
+               core.handle_async(function(a, b)
+                       return a, b
+               end, function(a, b)
+                       if b ~= 123 then
+                               cb("Argument went missing")
+                       end
+                       cb()
+               end, nil, 123)
+       end, "async_test")
+end
+unittests.register("test_handle_async", test_handle_async, {async=true})
+
+local function test_userdata_passing2(cb, _, pos)
+       -- VManip: check transfer into other env
+       local vm = core.get_voxel_manip(pos, pos)
+       local expect = vm:get_node_at(pos)
+
+       core.handle_async(function(vm_, pos_)
+               return vm_:get_node_at(pos_)
+       end, function(ret)
+               if not deepequal(expect, ret) then
+                       cb("Node data mismatch (one-way)")
+               end
+
+               -- VManip: test a roundtrip
+               core.handle_async(function(vm_)
+                       return vm_
+               end, function(vm2)
+                       if not deepequal(expect, vm2:get_node_at(pos)) then
+                               cb("Node data mismatch (roundtrip)")
+                       end
+                       cb()
+               end, vm)
+       end, vm, pos)
+end
+unittests.register("test_userdata_passing2", test_userdata_passing2, {map=true, async=true})
index 0754d507f9416a49e3532de3373ce30a9eb32f45..0608f2dd21ed18a91bd146d07d0cef7c7e047492 100644 (file)
@@ -175,6 +175,7 @@ dofile(modpath .. "/misc.lua")
 dofile(modpath .. "/player.lua")
 dofile(modpath .. "/crafting.lua")
 dofile(modpath .. "/itemdescription.lua")
+dofile(modpath .. "/async_env.lua")
 
 --------------
 
diff --git a/games/devtest/mods/unittests/inside_async_env.lua b/games/devtest/mods/unittests/inside_async_env.lua
new file mode 100644 (file)
index 0000000..9774771
--- /dev/null
@@ -0,0 +1,15 @@
+unittests = {}
+
+core.log("info", "Hello World")
+
+function unittests.async_test()
+       assert(core == minetest)
+       -- stuff that should not be here
+       assert(not core.get_player_by_name)
+       assert(not core.set_node)
+       assert(not core.object_refs)
+       -- stuff that should be here
+       assert(ItemStack)
+       assert(core.registered_items[""])
+       return true
+end
index 9c9324f5f578ccbfc2899ecf204b7c7594413dd0..5153dcaa95604321fd8edae71fd61fb5d72e975d 100644 (file)
@@ -1896,6 +1896,7 @@ MMVManip::MMVManip(Map *map):
                VoxelManipulator(),
                m_map(map)
 {
+       assert(map);
 }
 
 void MMVManip::initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max,
@@ -1903,6 +1904,8 @@ void MMVManip::initialEmerge(v3s16 blockpos_min, v3s16 blockpos_max,
 {
        TimeTaker timer1("initialEmerge", &emerge_time);
 
+       assert(m_map);
+
        // Units of these are MapBlocks
        v3s16 p_min = blockpos_min;
        v3s16 p_max = blockpos_max;
@@ -1986,6 +1989,7 @@ void MMVManip::blitBackAll(std::map<v3s16, MapBlock*> *modified_blocks,
 {
        if(m_area.getExtent() == v3s16(0,0,0))
                return;
+       assert(m_map);
 
        /*
                Copy data of all blocks
@@ -2006,4 +2010,33 @@ void MMVManip::blitBackAll(std::map<v3s16, MapBlock*> *modified_blocks,
        }
 }
 
+MMVManip *MMVManip::clone() const
+{
+       MMVManip *ret = new MMVManip();
+
+       const s32 size = m_area.getVolume();
+       ret->m_area = m_area;
+       if (m_data) {
+               ret->m_data = new MapNode[size];
+               memcpy(ret->m_data, m_data, size * sizeof(MapNode));
+       }
+       if (m_flags) {
+               ret->m_flags = new u8[size];
+               memcpy(ret->m_flags, m_flags, size * sizeof(u8));
+       }
+
+       ret->m_is_dirty = m_is_dirty;
+       // Even if the copy is disconnected from a map object keep the information
+       // needed to write it back to one
+       ret->m_loaded_blocks = m_loaded_blocks;
+
+       return ret;
+}
+
+void MMVManip::reparent(Map *map)
+{
+       assert(map && !m_map);
+       m_map = map;
+}
+
 //END
index d8ed291061f8f2167ec4541707a4b32a8baa7bff..21e3dbd6c3c73d8727187e3c30a58723ee459eb2 100644 (file)
--- a/src/map.h
+++ b/src/map.h
@@ -446,10 +446,25 @@ class MMVManip : public VoxelManipulator
        void blitBackAll(std::map<v3s16, MapBlock*> * modified_blocks,
                bool overwrite_generated = true);
 
+       /*
+               Creates a copy of this VManip including contents, the copy will not be
+               associated with a Map.
+       */
+       MMVManip *clone() const;
+
+       // Reassociates a copied VManip to a map
+       void reparent(Map *map);
+
+       // Is it impossible to call initialEmerge / blitBackAll?
+       inline bool isOrphan() const { return !m_map; }
+
        bool m_is_dirty = false;
 
 protected:
-       Map *m_map;
+       MMVManip() {};
+
+       // may be null
+       Map *m_map = nullptr;
        /*
                key = blockpos
                value = flags describing the block
index d07f6ab1be16be485536d713e7bec37ebed3cbb9..3e84b46c76a7a9d488154a96ddfe3c8d84cd53f0 100644 (file)
@@ -3,6 +3,7 @@ set(common_SCRIPT_COMMON_SRCS
        ${CMAKE_CURRENT_SOURCE_DIR}/c_converter.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/c_types.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/c_internal.cpp
+       ${CMAKE_CURRENT_SOURCE_DIR}/c_packer.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/helper.cpp
        PARENT_SCOPE)
 
index df82dba146ec7ddcb5d0becc7446b743e8c71e17..ddd2d184c5b320186846e7647326871f4dc9799b 100644 (file)
@@ -166,3 +166,17 @@ void log_deprecated(lua_State *L, std::string message, int stack_depth)
                infostream << script_get_backtrace(L) << std::endl;
 }
 
+void call_string_dump(lua_State *L, int idx)
+{
+       // Retrieve string.dump from insecure env to avoid it being tampered with
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
+       if (!lua_isnil(L, -1))
+               lua_getfield(L, -1, "string");
+       else
+               lua_getglobal(L, "string");
+       lua_getfield(L, -1, "dump");
+       lua_remove(L, -2); // remove _G
+       lua_remove(L, -2); // remove 'string' table
+       lua_pushvalue(L, idx);
+       lua_call(L, 1, 1);
+}
index c43db34aa2091a5572ef1e7e2ac4853277fe5010..272a3994147b3307b140b8987b6e181c426aac04 100644 (file)
@@ -56,6 +56,7 @@ extern "C" {
 #define CUSTOM_RIDX_BACKTRACE           (CUSTOM_RIDX_BASE + 3)
 #define CUSTOM_RIDX_HTTP_API_LUA        (CUSTOM_RIDX_BASE + 4)
 #define CUSTOM_RIDX_VECTOR_METATABLE    (CUSTOM_RIDX_BASE + 5)
+#define CUSTOM_RIDX_METATABLE_MAP       (CUSTOM_RIDX_BASE + 6)
 
 
 // Determine if CUSTOM_RIDX_SCRIPTAPI will hold a light or full userdata
@@ -139,3 +140,7 @@ DeprecatedHandlingMode get_deprecated_handling_mode();
  * @param stack_depth How far on the stack to the first user function (ie: not builtin or core)
  */
 void log_deprecated(lua_State *L, std::string message, int stack_depth = 1);
+
+// Safely call string.dump on a function value
+// (does not pop, leaves one value on stack)
+void call_string_dump(lua_State *L, int idx);
diff --git a/src/script/common/c_packer.cpp b/src/script/common/c_packer.cpp
new file mode 100644 (file)
index 0000000..fc52773
--- /dev/null
@@ -0,0 +1,583 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include <cstdio>
+#include <cstring>
+#include <cmath>
+#include <cassert>
+#include <unordered_set>
+#include <unordered_map>
+#include "c_packer.h"
+#include "c_internal.h"
+#include "log.h"
+#include "debug.h"
+#include "threading/mutex_auto_lock.h"
+
+extern "C" {
+#include <lauxlib.h>
+}
+
+//
+// Helpers
+//
+
+// convert negative index to absolute position on Lua stack
+static inline int absidx(lua_State *L, int idx)
+{
+       assert(idx < 0);
+       return lua_gettop(L) + idx + 1;
+}
+
+// does the type put anything into PackedInstr::sdata?
+static inline bool uses_sdata(int type)
+{
+       switch (type) {
+               case LUA_TSTRING:
+               case LUA_TFUNCTION:
+               case LUA_TUSERDATA:
+                       return true;
+               default:
+                       return false;
+       }
+}
+
+// does the type put anything into PackedInstr::<union>?
+static inline bool uses_union(int type)
+{
+       switch (type) {
+               case LUA_TNIL:
+               case LUA_TSTRING:
+               case LUA_TFUNCTION:
+                       return false;
+               default:
+                       return true;
+       }
+}
+
+static inline bool can_set_into(int ktype, int vtype)
+{
+       switch (ktype) {
+               case LUA_TNUMBER:
+                       return !uses_union(vtype);
+               case LUA_TSTRING:
+                       return !uses_sdata(vtype);
+               default:
+                       return false;
+       }
+}
+
+// is the key suitable for use with set_into?
+static inline bool suitable_key(lua_State *L, int idx)
+{
+       if (lua_type(L, idx) == LUA_TSTRING) {
+               // strings may not have a NULL byte (-> lua_setfield)
+               size_t len;
+               const char *str = lua_tolstring(L, idx, &len);
+               return strlen(str) == len;
+       } else {
+               assert(lua_type(L, idx) == LUA_TNUMBER);
+               // numbers must fit into an s32 and be integers (-> lua_rawseti)
+               lua_Number n = lua_tonumber(L, idx);
+               return std::floor(n) == n && n >= S32_MIN && n <= S32_MAX;
+       }
+}
+
+namespace {
+       // checks if you left any values on the stack, for debugging
+       class StackChecker {
+               lua_State *L;
+               int top;
+       public:
+               StackChecker(lua_State *L) : L(L), top(lua_gettop(L)) {}
+               ~StackChecker() {
+                       assert(lua_gettop(L) >= top);
+                       if (lua_gettop(L) > top) {
+                               rawstream << "Lua stack not cleaned up: "
+                                       << lua_gettop(L) << " != " << top
+                                       << " (false-positive if exception thrown)" << std::endl;
+                       }
+               }
+       };
+
+       // Since an std::vector may reallocate, this is the only safe way to keep
+       // a reference to a particular element.
+       template <typename T>
+       class VectorRef {
+               std::vector<T> *vec;
+               size_t idx;
+               VectorRef(std::vector<T> *vec, size_t idx) : vec(vec), idx(idx) {}
+       public:
+               static VectorRef<T> front(std::vector<T> &vec) {
+                       return VectorRef(&vec, 0);
+               }
+               static VectorRef<T> back(std::vector<T> &vec) {
+                       return VectorRef(&vec, vec.size() - 1);
+               }
+               T &operator*() { return (*vec)[idx]; }
+               T *operator->() { return &(*vec)[idx]; }
+       };
+
+       struct Packer {
+               PackInFunc fin;
+               PackOutFunc fout;
+       };
+
+       typedef std::pair<std::string, Packer> PackerTuple;
+}
+
+static inline auto emplace(PackedValue &pv, s16 type)
+{
+       pv.i.emplace_back();
+       auto ref = VectorRef<PackedInstr>::back(pv.i);
+       ref->type = type;
+       // Initialize fields that may be left untouched
+       if (type == LUA_TTABLE) {
+               ref->uidata1 = 0;
+               ref->uidata2 = 0;
+       } else if (type == LUA_TUSERDATA) {
+               ref->ptrdata = nullptr;
+       } else if (type == INSTR_POP) {
+               ref->sidata2 = 0;
+       }
+       return ref;
+}
+
+//
+// Management of registered packers
+//
+
+static std::unordered_map<std::string, Packer> g_packers;
+static std::mutex g_packers_lock;
+
+void script_register_packer(lua_State *L, const char *regname,
+       PackInFunc fin, PackOutFunc fout)
+{
+       // Store away callbacks
+       {
+               MutexAutoLock autolock(g_packers_lock);
+               auto it = g_packers.find(regname);
+               if (it == g_packers.end()) {
+                       auto &ref = g_packers[regname];
+                       ref.fin = fin;
+                       ref.fout = fout;
+               } else {
+                       FATAL_ERROR_IF(it->second.fin != fin || it->second.fout != fout,
+                               "Packer registered twice with mismatching callbacks");
+               }
+       }
+
+       // Save metatable so we can identify instances later
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_METATABLE_MAP);
+       if (lua_isnil(L, -1)) {
+               lua_newtable(L);
+               lua_pushvalue(L, -1);
+               lua_rawseti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_METATABLE_MAP);
+       }
+
+       luaL_getmetatable(L, regname);
+       FATAL_ERROR_IF(lua_isnil(L, -1), "No metatable registered with that name");
+
+       // CUSTOM_RIDX_METATABLE_MAP contains { [metatable] = "regname", ... }
+       // check first
+       lua_pushstring(L, regname);
+       lua_rawget(L, -3);
+       if (!lua_isnil(L, -1)) {
+               FATAL_ERROR_IF(lua_topointer(L, -1) != lua_topointer(L, -2),
+                               "Packer registered twice with inconsistent metatable");
+       }
+       lua_pop(L, 1);
+       // then set
+       lua_pushstring(L, regname);
+       lua_rawset(L, -3);
+
+       lua_pop(L, 1);
+}
+
+static bool find_packer(const char *regname, PackerTuple &out)
+{
+       MutexAutoLock autolock(g_packers_lock);
+       auto it = g_packers.find(regname);
+       if (it == g_packers.end())
+               return false;
+       // copy data for thread safety
+       out.first = it->first;
+       out.second = it->second;
+       return true;
+}
+
+static bool find_packer(lua_State *L, int idx, PackerTuple &out)
+{
+#ifndef NDEBUG
+       StackChecker checker(L);
+#endif
+
+       // retrieve metatable of the object
+       if (lua_getmetatable(L, idx) != 1)
+               return false;
+
+       // use our global table to map it to the registry name
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_METATABLE_MAP);
+       assert(lua_istable(L, -1));
+       lua_pushvalue(L, -2);
+       lua_rawget(L, -2);
+       if (lua_isnil(L, -1)) {
+               lua_pop(L, 3);
+               return false;
+       }
+
+       // load the associated data
+       bool found = find_packer(lua_tostring(L, -1), out);
+       FATAL_ERROR_IF(!found, "Inconsistent internal state");
+       lua_pop(L, 3);
+       return true;
+}
+
+//
+// Packing implementation
+//
+
+// recursively goes through the structure and ensures there are no circular references
+static void pack_validate(lua_State *L, int idx, std::unordered_set<const void*> &seen)
+{
+#ifndef NDEBUG
+       StackChecker checker(L);
+       assert(idx > 0);
+#endif
+
+       if (lua_type(L, idx) != LUA_TTABLE)
+               return;
+
+       const void *ptr = lua_topointer(L, idx);
+       assert(ptr);
+
+       if (seen.find(ptr) != seen.end())
+               throw LuaError("Circular references cannot be packed (yet)");
+       seen.insert(ptr);
+
+       lua_checkstack(L, 5);
+       lua_pushnil(L);
+       while (lua_next(L, idx) != 0) {
+               // key at -2, value at -1
+               pack_validate(L, absidx(L, -2), seen);
+               pack_validate(L, absidx(L, -1), seen);
+
+               lua_pop(L, 1);
+       }
+
+       seen.erase(ptr);
+}
+
+static VectorRef<PackedInstr> pack_inner(lua_State *L, int idx, int vidx, PackedValue &pv)
+{
+#ifndef NDEBUG
+       StackChecker checker(L);
+       assert(idx > 0);
+       assert(vidx > 0);
+#endif
+
+       switch (lua_type(L, idx)) {
+               case LUA_TNONE:
+               case LUA_TNIL:
+                       return emplace(pv, LUA_TNIL);
+               case LUA_TBOOLEAN: {
+                       auto r = emplace(pv, LUA_TBOOLEAN);
+                       r->bdata = lua_toboolean(L, idx);
+                       return r;
+               }
+               case LUA_TNUMBER: {
+                       auto r = emplace(pv, LUA_TNUMBER);
+                       r->ndata = lua_tonumber(L, idx);
+                       return r;
+               }
+               case LUA_TSTRING: {
+                       auto r = emplace(pv, LUA_TSTRING);
+                       size_t len;
+                       const char *str = lua_tolstring(L, idx, &len);
+                       assert(str);
+                       r->sdata.assign(str, len);
+                       return r;
+               }
+               case LUA_TTABLE:
+                       break; // execution continues
+               case LUA_TFUNCTION: {
+                       auto r = emplace(pv, LUA_TFUNCTION);
+                       call_string_dump(L, idx);
+                       size_t len;
+                       const char *str = lua_tolstring(L, -1, &len);
+                       assert(str);
+                       r->sdata.assign(str, len);
+                       lua_pop(L, 1);
+                       return r;
+               }
+               case LUA_TUSERDATA: {
+                       PackerTuple ser;
+                       if (!find_packer(L, idx, ser))
+                               throw LuaError("Cannot serialize unsupported userdata");
+                       pv.contains_userdata = true;
+                       auto r = emplace(pv, LUA_TUSERDATA);
+                       r->sdata = ser.first;
+                       r->ptrdata = ser.second.fin(L, idx);
+                       return r;
+               }
+               default: {
+                       std::string err = "Cannot serialize type ";
+                       err += lua_typename(L, lua_type(L, idx));
+                       throw LuaError(err);
+               }
+       }
+
+       // LUA_TTABLE
+       lua_checkstack(L, 5);
+
+       auto rtable = emplace(pv, LUA_TTABLE);
+       const int vi_table = vidx++;
+
+       lua_pushnil(L);
+       while (lua_next(L, idx) != 0) {
+               // key at -2, value at -1
+               const int ktype = lua_type(L, -2), vtype = lua_type(L, -1);
+               if (ktype == LUA_TNUMBER)
+                       rtable->uidata1++; // narr
+               else
+                       rtable->uidata2++; // nrec
+
+               // check if we can use a shortcut
+               if (can_set_into(ktype, vtype) && suitable_key(L, -2)) {
+                       // push only the value
+                       auto rval = pack_inner(L, absidx(L, -1), vidx, pv);
+                       rval->pop = vtype != LUA_TTABLE;
+                       // and where to put it:
+                       rval->set_into = vi_table;
+                       if (ktype == LUA_TSTRING)
+                               rval->sdata = lua_tostring(L, -2);
+                       else
+                               rval->sidata1 = lua_tointeger(L, -2);
+                       // pop tables after the fact
+                       if (!rval->pop) {
+                               auto ri1 = emplace(pv, INSTR_POP);
+                               ri1->sidata1 = vidx;
+                       }
+               } else {
+                       // push the key and value
+                       pack_inner(L, absidx(L, -2), vidx, pv);
+                       vidx++;
+                       pack_inner(L, absidx(L, -1), vidx, pv);
+                       vidx++;
+                       // push an instruction to set them
+                       auto ri1 = emplace(pv, INSTR_SETTABLE);
+                       ri1->set_into = vi_table;
+                       ri1->sidata1 = vidx - 2;
+                       ri1->sidata2 = vidx - 1;
+                       ri1->pop = true;
+                       vidx -= 2;
+               }
+
+               lua_pop(L, 1);
+       }
+
+       assert(vidx == vi_table + 1);
+       return rtable;
+}
+
+PackedValue *script_pack(lua_State *L, int idx)
+{
+       if (idx < 0)
+               idx = absidx(L, idx);
+
+       std::unordered_set<const void*> seen;
+       pack_validate(L, idx, seen);
+       assert(seen.size() == 0);
+
+       // Actual serialization
+       PackedValue pv;
+       pack_inner(L, idx, 1, pv);
+
+       return new PackedValue(std::move(pv));
+}
+
+//
+// Unpacking implementation
+//
+
+void script_unpack(lua_State *L, PackedValue *pv)
+{
+       const int top = lua_gettop(L);
+       int ctr = 0;
+
+       for (auto &i : pv->i) {
+               // If leaving values on stack make sure there's space (every 5th iteration)
+               if (!i.pop && (ctr++) >= 5) {
+                       lua_checkstack(L, 5);
+                       ctr = 0;
+               }
+
+               /* Instructions */
+               switch (i.type) {
+                       case INSTR_SETTABLE:
+                               lua_pushvalue(L, top + i.sidata1); // key
+                               lua_pushvalue(L, top + i.sidata2); // value
+                               lua_rawset(L, top + i.set_into);
+                               if (i.pop) {
+                                       if (i.sidata1 != i.sidata2) {
+                                               // removing moves indices so pop higher index first
+                                               lua_remove(L, top + std::max(i.sidata1, i.sidata2));
+                                               lua_remove(L, top + std::min(i.sidata1, i.sidata2));
+                                       } else {
+                                               lua_remove(L, top + i.sidata1);
+                                       }
+                               }
+                               continue;
+                       case INSTR_POP:
+                               lua_remove(L, top + i.sidata1);
+                               if (i.sidata2 > 0)
+                                       lua_remove(L, top + i.sidata2);
+                               continue;
+                       default:
+                               break;
+               }
+
+               /* Lua types */
+               switch (i.type) {
+                       case LUA_TNIL:
+                               lua_pushnil(L);
+                               break;
+                       case LUA_TBOOLEAN:
+                               lua_pushboolean(L, i.bdata);
+                               break;
+                       case LUA_TNUMBER:
+                               lua_pushnumber(L, i.ndata);
+                               break;
+                       case LUA_TSTRING:
+                               lua_pushlstring(L, i.sdata.data(), i.sdata.size());
+                               break;
+                       case LUA_TTABLE:
+                               lua_createtable(L, i.uidata1, i.uidata2);
+                               break;
+                       case LUA_TFUNCTION:
+                               luaL_loadbuffer(L, i.sdata.data(), i.sdata.size(), nullptr);
+                               break;
+                       case LUA_TUSERDATA: {
+                               PackerTuple ser;
+                               sanity_check(find_packer(i.sdata.c_str(), ser));
+                               ser.second.fout(L, i.ptrdata);
+                               i.ptrdata = nullptr; // ownership taken by callback
+                               break;
+                       }
+                       default:
+                               assert(0);
+                               break;
+               }
+
+               if (i.set_into) {
+                       if (!i.pop)
+                               lua_pushvalue(L, -1);
+                       if (uses_sdata(i.type))
+                               lua_rawseti(L, top + i.set_into, i.sidata1);
+                       else
+                               lua_setfield(L, top + i.set_into, i.sdata.c_str());
+               } else {
+                       if (i.pop)
+                               lua_pop(L, 1);
+               }
+       }
+
+       // as part of the unpacking process we take ownership of all userdata
+       pv->contains_userdata = false;
+       // leave exactly one value on the stack
+       lua_settop(L, top+1);
+}
+
+//
+// PackedValue
+//
+
+PackedValue::~PackedValue()
+{
+       if (!contains_userdata)
+               return;
+       for (auto &i : this->i) {
+               if (i.type == LUA_TUSERDATA && i.ptrdata) {
+                       PackerTuple ser;
+                       if (find_packer(i.sdata.c_str(), ser)) {
+                               // tell it to deallocate object
+                               ser.second.fout(nullptr, i.ptrdata);
+                       } else {
+                               assert(false);
+                       }
+               }
+       }
+}
+
+//
+// script_dump_packed
+//
+
+#ifndef NDEBUG
+void script_dump_packed(const PackedValue *val)
+{
+       printf("instruction stream: [\n");
+       for (const auto &i : val->i) {
+               printf("\t(");
+               switch (i.type) {
+                       case INSTR_SETTABLE:
+                               printf("SETTABLE(%d, %d)", i.sidata1, i.sidata2);
+                               break;
+                       case INSTR_POP:
+                               printf(i.sidata2 ? "POP(%d, %d)" : "POP(%d)", i.sidata1, i.sidata2);
+                               break;
+                       case LUA_TNIL:
+                               printf("nil");
+                               break;
+                       case LUA_TBOOLEAN:
+                               printf(i.bdata ? "true" : "false");
+                               break;
+                       case LUA_TNUMBER:
+                               printf("%f", i.ndata);
+                               break;
+                       case LUA_TSTRING:
+                               printf("\"%s\"", i.sdata.c_str());
+                               break;
+                       case LUA_TTABLE:
+                               printf("table(%d, %d)", i.uidata1, i.uidata2);
+                               break;
+                       case LUA_TFUNCTION:
+                               printf("function(%d byte)", i.sdata.size());
+                               break;
+                       case LUA_TUSERDATA:
+                               printf("userdata %s %p", i.sdata.c_str(), i.ptrdata);
+                               break;
+                       default:
+                               printf("!!UNKNOWN!!");
+                               break;
+               }
+               if (i.set_into) {
+                       if (i.type >= 0 && uses_sdata(i.type))
+                               printf(", k=%d, into=%d", i.sidata1, i.set_into);
+                       else if (i.type >= 0)
+                               printf(", k=\"%s\", into=%d", i.sdata.c_str(), i.set_into);
+                       else
+                               printf(", into=%d", i.set_into);
+               }
+               if (i.pop)
+                       printf(", pop");
+               printf(")\n");
+       }
+       printf("]\n");
+}
+#endif
diff --git a/src/script/common/c_packer.h b/src/script/common/c_packer.h
new file mode 100644 (file)
index 0000000..8bccca9
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+Minetest
+Copyright (C) 2022 sfan5 <sfan5@live.de>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include "irrlichttypes.h"
+#include "util/basic_macros.h"
+
+extern "C" {
+#include <lua.h>
+}
+
+/*
+       This file defines an in-memory representation of Lua objects including
+       support for functions and userdata. It it used to move data between Lua
+       states and cannot be used for persistence or network transfer.
+*/
+
+#define INSTR_SETTABLE (-10)
+#define INSTR_POP      (-11)
+
+/**
+ * Represents a single instruction that pushes a new value or works with existing ones.
+ */
+struct PackedInstr
+{
+       s16 type; // LUA_T* or INSTR_*
+       u16 set_into; // set into table on stack
+       bool pop; // remove from stack?
+       union {
+               bool bdata; // boolean: value
+               lua_Number ndata; // number: value
+               struct {
+                       u16 uidata1, uidata2; // table: narr, nrec
+               };
+               struct {
+                       /*
+                               SETTABLE: key index, value index
+                               POP: indices to remove
+                               otherwise w/ set_into: numeric key, -
+                       */
+                       s32 sidata1, sidata2;
+               };
+               void *ptrdata; // userdata: implementation defined
+       };
+       /*
+               - string: value
+               - function: buffer
+               - w/ set_into: string key (no null bytes!)
+               - userdata: name in registry
+       */
+       std::string sdata;
+
+       PackedInstr() : type(0), set_into(0), pop(false) {}
+};
+
+/**
+ * A packed value can be a primitive like a string or number but also a table
+ * including all of its contents. It is made up of a linear stream of
+ * 'instructions' that build the final value when executed.
+ */
+struct PackedValue
+{
+       std::vector<PackedInstr> i;
+       // Indicates whether there are any userdata pointers that need to be deallocated
+       bool contains_userdata = false;
+
+       PackedValue() = default;
+       ~PackedValue();
+
+       DISABLE_CLASS_COPY(PackedValue)
+
+       ALLOW_CLASS_MOVE(PackedValue)
+};
+
+/*
+ * Packing callback: Turns a Lua value at given index into a void*
+ */
+typedef void *(*PackInFunc)(lua_State *L, int idx);
+/*
+ * Unpacking callback: Turns a void* back into the Lua value (left on top of stack)
+ *
+ * Note that this function must take ownership of the pointer, so make sure
+ * to free or keep the memory.
+ * `L` can be nullptr to indicate that data should just be discarded.
+ */
+typedef void (*PackOutFunc)(lua_State *L, void *ptr);
+/*
+ * Register a packable type with the name of its metatable.
+ *
+ * Even though the callbacks are global this must be called for every Lua state
+ * that supports objects of this type.
+ * This function is thread-safe.
+ */
+void script_register_packer(lua_State *L, const char *regname,
+               PackInFunc fin, PackOutFunc fout);
+
+// Pack a Lua value
+PackedValue *script_pack(lua_State *L, int idx);
+// Unpack a Lua value (left on top of stack)
+// Note that this may modify the PackedValue, you can't reuse it!
+void script_unpack(lua_State *L, PackedValue *val);
+
+// Dump contents of PackedValue to stdout for debugging
+void script_dump_packed(const PackedValue *val);
index dacdcd75a396344ef11aa2959a6e20a43a6e5808..42a794cebcbf49863787c05f3f21b6bfbce5c619 100644 (file)
@@ -21,9 +21,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <cstdlib>
 
 extern "C" {
-#include "lua.h"
-#include "lauxlib.h"
-#include "lualib.h"
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
 }
 
 #include "server.h"
@@ -32,6 +32,7 @@ extern "C" {
 #include "filesys.h"
 #include "porting.h"
 #include "common/c_internal.h"
+#include "common/c_packer.h"
 #include "lua_api/l_base.h"
 
 /******************************************************************************/
@@ -76,19 +77,34 @@ void AsyncEngine::initialize(unsigned int numEngines)
 {
        initDone = true;
 
-       for (unsigned int i = 0; i < numEngines; i++) {
-               AsyncWorkerThread *toAdd = new AsyncWorkerThread(this,
-                       std::string("AsyncWorker-") + itos(i));
-               workerThreads.push_back(toAdd);
-               toAdd->start();
+       if (numEngines == 0) {
+               // Leave one core for the main thread and one for whatever else
+               autoscaleMaxWorkers = Thread::getNumberOfProcessors();
+               if (autoscaleMaxWorkers >= 2)
+                       autoscaleMaxWorkers -= 2;
+               infostream << "AsyncEngine: using at most " << autoscaleMaxWorkers
+                       << " threads with automatic scaling" << std::endl;
+
+               addWorkerThread();
+       } else {
+               for (unsigned int i = 0; i < numEngines; i++)
+                       addWorkerThread();
        }
 }
 
+void AsyncEngine::addWorkerThread()
+{
+       AsyncWorkerThread *toAdd = new AsyncWorkerThread(this,
+               std::string("AsyncWorker-") + itos(workerThreads.size()));
+       workerThreads.push_back(toAdd);
+       toAdd->start();
+}
+
 /******************************************************************************/
 u32 AsyncEngine::queueAsyncJob(std::string &&func, std::string &&params,
                const std::string &mod_origin)
 {
-       jobQueueMutex.lock();
+       MutexAutoLock autolock(jobQueueMutex);
        u32 jobId = jobIdCounter++;
 
        jobQueue.emplace_back();
@@ -99,7 +115,23 @@ u32 AsyncEngine::queueAsyncJob(std::string &&func, std::string &&params,
        to_add.mod_origin = mod_origin;
 
        jobQueueCounter.post();
-       jobQueueMutex.unlock();
+       return jobId;
+}
+
+u32 AsyncEngine::queueAsyncJob(std::string &&func, PackedValue *params,
+               const std::string &mod_origin)
+{
+       MutexAutoLock autolock(jobQueueMutex);
+       u32 jobId = jobIdCounter++;
+
+       jobQueue.emplace_back();
+       auto &to_add = jobQueue.back();
+       to_add.id = jobId;
+       to_add.function = std::move(func);
+       to_add.params_ext.reset(params);
+       to_add.mod_origin = mod_origin;
+
+       jobQueueCounter.post();
        return jobId;
 }
 
@@ -131,6 +163,12 @@ void AsyncEngine::putJobResult(LuaJobInfo &&result)
 
 /******************************************************************************/
 void AsyncEngine::step(lua_State *L)
+{
+       stepJobResults(L);
+       stepAutoscale();
+}
+
+void AsyncEngine::stepJobResults(lua_State *L)
 {
        int error_handler = PUSH_ERROR_HANDLER(L);
        lua_getglobal(L, "core");
@@ -148,7 +186,10 @@ void AsyncEngine::step(lua_State *L)
                luaL_checktype(L, -1, LUA_TFUNCTION);
 
                lua_pushinteger(L, j.id);
-               lua_pushlstring(L, j.result.data(), j.result.size());
+               if (j.result_ext)
+                       script_unpack(L, j.result_ext.get());
+               else
+                       lua_pushlstring(L, j.result.data(), j.result.size());
 
                // Call handler
                const char *origin = j.mod_origin.empty() ? nullptr : j.mod_origin.c_str();
@@ -161,12 +202,71 @@ void AsyncEngine::step(lua_State *L)
        lua_pop(L, 2); // Pop core and error handler
 }
 
+void AsyncEngine::stepAutoscale()
+{
+       if (workerThreads.size() >= autoscaleMaxWorkers)
+               return;
+
+       MutexAutoLock autolock(jobQueueMutex);
+
+       // 2) If the timer elapsed, check again
+       if (autoscaleTimer && porting::getTimeMs() >= autoscaleTimer) {
+               autoscaleTimer = 0;
+               // Determine overlap with previous snapshot
+               unsigned int n = 0;
+               for (const auto &it : jobQueue)
+                       n += autoscaleSeenJobs.count(it.id);
+               autoscaleSeenJobs.clear();
+               infostream << "AsyncEngine: " << n << " jobs were still waiting after 1s" << std::endl;
+               // Start this many new threads
+               while (workerThreads.size() < autoscaleMaxWorkers && n > 0) {
+                       addWorkerThread();
+                       n--;
+               }
+               return;
+       }
+
+       // 1) Check if there's anything in the queue
+       if (!autoscaleTimer && !jobQueue.empty()) {
+               // Take a snapshot of all jobs we have seen
+               for (const auto &it : jobQueue)
+                       autoscaleSeenJobs.emplace(it.id);
+               // and set a timer for 1 second
+               autoscaleTimer = porting::getTimeMs() + 1000;
+       }
+}
+
 /******************************************************************************/
-void AsyncEngine::prepareEnvironment(lua_State* L, int top)
+bool AsyncEngine::prepareEnvironment(lua_State* L, int top)
 {
        for (StateInitializer &stateInitializer : stateInitializers) {
                stateInitializer(L, top);
        }
+
+       auto *script = ModApiBase::getScriptApiBase(L);
+       try {
+               script->loadMod(Server::getBuiltinLuaPath() + DIR_DELIM + "init.lua",
+                       BUILTIN_MOD_NAME);
+       } catch (const ModError &e) {
+               errorstream << "Execution of async base environment failed: "
+                       << e.what() << std::endl;
+               FATAL_ERROR("Execution of async base environment failed");
+       }
+
+       // Load per mod stuff
+       if (server) {
+               const auto &list = server->m_async_init_files;
+               try {
+                       for (auto &it : list)
+                               script->loadMod(it.second, it.first);
+               } catch (const ModError &e) {
+                       errorstream << "Failed to load mod script inside async environment." << std::endl;
+                       server->setAsyncFatalError(e.what());
+                       return false;
+               }
+       }
+
+       return true;
 }
 
 /******************************************************************************/
@@ -178,15 +278,25 @@ AsyncWorkerThread::AsyncWorkerThread(AsyncEngine* jobDispatcher,
 {
        lua_State *L = getStack();
 
+       if (jobDispatcher->server) {
+               setGameDef(jobDispatcher->server);
+
+               if (g_settings->getBool("secure.enable_security"))
+                       initializeSecurity();
+       }
+
        // Prepare job lua environment
        lua_getglobal(L, "core");
        int top = lua_gettop(L);
 
        // Push builtin initialization type
-       lua_pushstring(L, "async");
+       lua_pushstring(L, jobDispatcher->server ? "async_game" : "async");
        lua_setglobal(L, "INIT");
 
-       jobDispatcher->prepareEnvironment(L, top);
+       if (!jobDispatcher->prepareEnvironment(L, top)) {
+               // can't throw from here so we're stuck with this
+               isErrored = true;
+       }
 }
 
 /******************************************************************************/
@@ -198,19 +308,20 @@ AsyncWorkerThread::~AsyncWorkerThread()
 /******************************************************************************/
 void* AsyncWorkerThread::run()
 {
-       lua_State *L = getStack();
+       if (isErrored)
+               return nullptr;
 
-       try {
-               loadMod(getServer()->getBuiltinLuaPath() + DIR_DELIM + "init.lua",
-                       BUILTIN_MOD_NAME);
-       } catch (const ModError &e) {
-               errorstream << "Execution of async base environment failed: "
-                       << e.what() << std::endl;
-               FATAL_ERROR("Execution of async base environment failed");
-       }
+       lua_State *L = getStack();
 
        int error_handler = PUSH_ERROR_HANDLER(L);
 
+       auto report_error = [this] (const ModError &e) {
+               if (jobDispatcher->server)
+                       jobDispatcher->server->setAsyncFatalError(e.what());
+               else
+                       errorstream << e.what() << std::endl;
+       };
+
        lua_getglobal(L, "core");
        if (lua_isnil(L, -1)) {
                FATAL_ERROR("Unable to find core within async environment!");
@@ -223,6 +334,8 @@ void* AsyncWorkerThread::run()
                if (!jobDispatcher->getJob(&j) || stopRequested())
                        continue;
 
+               const bool use_ext = !!j.params_ext;
+
                lua_getfield(L, -1, "job_processor");
                if (lua_isnil(L, -1))
                        FATAL_ERROR("Unable to get async job processor!");
@@ -232,7 +345,10 @@ void* AsyncWorkerThread::run()
                        errorstream << "ASYNC WORKER: Unable to deserialize function" << std::endl;
                        lua_pushnil(L);
                }
-               lua_pushlstring(L, j.params.data(), j.params.size());
+               if (use_ext)
+                       script_unpack(L, j.params_ext.get());
+               else
+                       lua_pushlstring(L, j.params.data(), j.params.size());
 
                // Call it
                setOriginDirect(j.mod_origin.empty() ? nullptr : j.mod_origin.c_str());
@@ -241,19 +357,28 @@ void* AsyncWorkerThread::run()
                        try {
                                scriptError(result, "<async>");
                        } catch (const ModError &e) {
-                               errorstream << e.what() << std::endl;
+                               report_error(e);
                        }
                } else {
                        // Fetch result
-                       size_t length;
-                       const char *retval = lua_tolstring(L, -1, &length);
-                       j.result.assign(retval, length);
+                       if (use_ext) {
+                               try {
+                                       j.result_ext.reset(script_pack(L, -1));
+                               } catch (const ModError &e) {
+                                       report_error(e);
+                                       result = LUA_ERRERR;
+                               }
+                       } else {
+                               size_t length;
+                               const char *retval = lua_tolstring(L, -1, &length);
+                               j.result.assign(retval, length);
+                       }
                }
 
                lua_pop(L, 1);  // Pop retval
 
                // Put job result
-               if (!j.result.empty())
+               if (result == 0)
                        jobDispatcher->putJobResult(std::move(j));
        }
 
index 697cb02215334f8848270d446401497c22083467..1e34e40ea55358e2d66f60fd71b1b9df880eb4c0 100644 (file)
@@ -21,11 +21,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include <vector>
 #include <deque>
+#include <unordered_set>
+#include <memory>
 
+#include <lua.h>
 #include "threading/semaphore.h"
 #include "threading/thread.h"
-#include "lua.h"
+#include "common/c_packer.h"
 #include "cpp_api/s_base.h"
+#include "cpp_api/s_security.h"
 
 // Forward declarations
 class AsyncEngine;
@@ -42,8 +46,12 @@ struct LuaJobInfo
        std::string function;
        // Parameter to be passed to function (serialized)
        std::string params;
+       // Alternative parameters
+       std::unique_ptr<PackedValue> params_ext;
        // Result of function call (serialized)
        std::string result;
+       // Alternative result
+       std::unique_ptr<PackedValue> result_ext;
        // Name of the mod who invoked this call
        std::string mod_origin;
        // JobID used to identify a job and match it to callback
@@ -51,7 +59,8 @@ struct LuaJobInfo
 };
 
 // Asynchronous working environment
-class AsyncWorkerThread : public Thread, virtual public ScriptApiBase {
+class AsyncWorkerThread : public Thread,
+       virtual public ScriptApiBase, public ScriptApiSecurity {
        friend class AsyncEngine;
 public:
        virtual ~AsyncWorkerThread();
@@ -63,6 +72,7 @@ class AsyncWorkerThread : public Thread, virtual public ScriptApiBase {
 
 private:
        AsyncEngine *jobDispatcher = nullptr;
+       bool isErrored = false;
 };
 
 // Asynchornous thread and job management
@@ -71,6 +81,7 @@ class AsyncEngine {
        typedef void (*StateInitializer)(lua_State *L, int top);
 public:
        AsyncEngine() = default;
+       AsyncEngine(Server *server) : server(server) {};
        ~AsyncEngine();
 
        /**
@@ -81,7 +92,7 @@ class AsyncEngine {
 
        /**
         * Create async engine tasks and lock function registration
-        * @param numEngines Number of async threads to be started
+        * @param numEngines Number of worker threads, 0 for automatic scaling
         */
        void initialize(unsigned int numEngines);
 
@@ -94,9 +105,17 @@ class AsyncEngine {
        u32 queueAsyncJob(std::string &&func, std::string &&params,
                        const std::string &mod_origin = "");
 
+       /**
+        * Queue an async job
+        * @param func Serialized lua function
+        * @param params Serialized parameters (takes ownership!)
+        * @return ID of queued job
+        */
+       u32 queueAsyncJob(std::string &&func, PackedValue *params,
+                       const std::string &mod_origin = "");
+
        /**
         * Engine step to process finished jobs
-        *   the engine step is one way to pass events back, PushFinishedJobs another
         * @param L The Lua stack
         */
        void step(lua_State *L);
@@ -116,19 +135,44 @@ class AsyncEngine {
         */
        void putJobResult(LuaJobInfo &&result);
 
+       /**
+        * Start an additional worker thread
+        */
+       void addWorkerThread();
+
+       /**
+        * Process finished jobs callbacks
+        */
+       void stepJobResults(lua_State *L);
+
+       /**
+        * Handle automatic scaling of worker threads
+        */
+       void stepAutoscale();
+
        /**
         * Initialize environment with current registred functions
         *  this function adds all functions registred by registerFunction to the
         *  passed lua stack
         * @param L Lua stack to initialize
         * @param top Stack position
+        * @return false if a mod error ocurred
         */
-       void prepareEnvironment(lua_State* L, int top);
+       bool prepareEnvironment(lua_State* L, int top);
 
 private:
        // Variable locking the engine against further modification
        bool initDone = false;
 
+       // Maximum number of worker threads for automatic scaling
+       // 0 if disabled
+       unsigned int autoscaleMaxWorkers = 0;
+       u64 autoscaleTimer = 0;
+       std::unordered_set<u32> autoscaleSeenJobs;
+
+       // Only set for the server async environment (duh)
+       Server *server = nullptr;
+
        // Internal store for registred state initializers
        std::vector<StateInitializer> stateInitializers;
 
index c2c5a5551c1b908d716c32e4ca3fd8ac54da3272..137b210be2a8c13876ef5715b8ed4a06cdaa1f84 100644 (file)
@@ -525,3 +525,11 @@ void ModApiCraft::Initialize(lua_State *L, int top)
        API_FCT(register_craft);
        API_FCT(clear_craft);
 }
+
+void ModApiCraft::InitializeAsync(lua_State *L, int top)
+{
+       // all read-only functions
+       API_FCT(get_all_craft_recipes);
+       API_FCT(get_craft_recipe);
+       API_FCT(get_craft_result);
+}
index 9002b23efe1b28f926ab616166d24277bef7435f..5234af56f2d2d61b90993dc6733636f2dc6549e5 100644 (file)
@@ -45,4 +45,5 @@ class ModApiCraft : public ModApiBase {
 
 public:
        static void Initialize(lua_State *L, int top);
+       static void InitializeAsync(lua_State *L, int top);
 };
index 672e535ca56b9a85f3a728e3a5aa7cabc56be7ac..de73ff42ab02675b36abc199549302f1baaef20e 100644 (file)
@@ -69,7 +69,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 // Retrieve Environment pointer as `env` (no map lock)
 #define GET_PLAIN_ENV_PTR_NO_MAP_LOCK            \
-       Environment *env = (Environment *)getEnv(L); \
+       Environment *env = getEnv(L);                \
        if (env == NULL)                             \
                return 0
 
index fc97a1736ca8a85625e12b69afca7218985609cb..b58b994d95a78ea02f4df921e9b05397955c7751 100644 (file)
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_internal.h"
 #include "common/c_converter.h"
 #include "common/c_content.h"
+#include "common/c_packer.h"
 #include "itemdef.h"
 #include "nodedef.h"
 #include "server.h"
@@ -441,6 +442,7 @@ int LuaItemStack::create_object(lua_State *L)
        lua_setmetatable(L, -2);
        return 1;
 }
+
 // Not callable from Lua
 int LuaItemStack::create(lua_State *L, const ItemStack &item)
 {
@@ -457,6 +459,20 @@ LuaItemStack *LuaItemStack::checkobject(lua_State *L, int narg)
        return *(LuaItemStack **)luaL_checkudata(L, narg, className);
 }
 
+void *LuaItemStack::packIn(lua_State *L, int idx)
+{
+       LuaItemStack *o = checkobject(L, idx);
+       return new ItemStack(o->getItem());
+}
+
+void LuaItemStack::packOut(lua_State *L, void *ptr)
+{
+       ItemStack *stack = reinterpret_cast<ItemStack*>(ptr);
+       if (L)
+               create(L, *stack);
+       delete stack;
+}
+
 void LuaItemStack::Register(lua_State *L)
 {
        lua_newtable(L);
@@ -488,6 +504,8 @@ void LuaItemStack::Register(lua_State *L)
 
        // Can be created from Lua (ItemStack(itemstack or itemstring or table or nil))
        lua_register(L, className, create_object);
+
+       script_register_packer(L, className, packIn, packOut);
 }
 
 const char LuaItemStack::className[] = "ItemStack";
@@ -673,3 +691,10 @@ void ModApiItemMod::Initialize(lua_State *L, int top)
        API_FCT(get_content_id);
        API_FCT(get_name_from_content_id);
 }
+
+void ModApiItemMod::InitializeAsync(lua_State *L, int top)
+{
+       // all read-only functions
+       API_FCT(get_content_id);
+       API_FCT(get_name_from_content_id);
+}
index 16878c101df34a1d598b43d83203ab3d6fd3436b..180975061317225a04929fcc97d271dbbc1b1265 100644 (file)
@@ -141,8 +141,11 @@ class LuaItemStack : public ModApiBase {
        // Not callable from Lua
        static int create(lua_State *L, const ItemStack &item);
        static LuaItemStack* checkobject(lua_State *L, int narg);
-       static void Register(lua_State *L);
 
+       static void *packIn(lua_State *L, int idx);
+       static void packOut(lua_State *L, void *ptr);
+
+       static void Register(lua_State *L);
 };
 
 class ModApiItemMod : public ModApiBase {
@@ -152,6 +155,8 @@ class ModApiItemMod : public ModApiBase {
        static int l_register_alias_raw(lua_State *L);
        static int l_get_content_id(lua_State *L);
        static int l_get_name_from_content_id(lua_State *L);
+
 public:
        static void Initialize(lua_State *L, int top);
+       static void InitializeAsync(lua_State *L, int top);
 };
index 0eee49b7d7682d8daec95f20ef5d6f1b4f3d3332..5561eaebf90c01e074d7f19bb794809c1f2ad399 100644 (file)
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_internal.h"
 #include "common/c_converter.h"
 #include "common/c_content.h"
+#include "common/c_packer.h"
 #include "log.h"
 #include "porting.h"
 #include "util/numeric.h"
@@ -101,6 +102,25 @@ LuaPerlinNoise *LuaPerlinNoise::checkobject(lua_State *L, int narg)
 }
 
 
+void *LuaPerlinNoise::packIn(lua_State *L, int idx)
+{
+       LuaPerlinNoise *o = checkobject(L, idx);
+       return new NoiseParams(o->np);
+}
+
+void LuaPerlinNoise::packOut(lua_State *L, void *ptr)
+{
+       NoiseParams *np = reinterpret_cast<NoiseParams*>(ptr);
+       if (L) {
+               LuaPerlinNoise *o = new LuaPerlinNoise(np);
+               *(void **)(lua_newuserdata(L, sizeof(void *))) = o;
+               luaL_getmetatable(L, className);
+               lua_setmetatable(L, -2);
+       }
+       delete np;
+}
+
+
 void LuaPerlinNoise::Register(lua_State *L)
 {
        lua_newtable(L);
@@ -126,6 +146,8 @@ void LuaPerlinNoise::Register(lua_State *L)
        lua_pop(L, 1);
 
        lua_register(L, className, create_object);
+
+       script_register_packer(L, className, packIn, packOut);
 }
 
 
@@ -357,6 +379,35 @@ LuaPerlinNoiseMap *LuaPerlinNoiseMap::checkobject(lua_State *L, int narg)
 }
 
 
+struct NoiseMapParams {
+       NoiseParams np;
+       s32 seed;
+       v3s16 size;
+};
+
+void *LuaPerlinNoiseMap::packIn(lua_State *L, int idx)
+{
+       LuaPerlinNoiseMap *o = checkobject(L, idx);
+       NoiseMapParams *ret = new NoiseMapParams();
+       ret->np = o->noise->np;
+       ret->seed = o->noise->seed;
+       ret->size = v3s16(o->noise->sx, o->noise->sy, o->noise->sz);
+       return ret;
+}
+
+void LuaPerlinNoiseMap::packOut(lua_State *L, void *ptr)
+{
+       NoiseMapParams *p = reinterpret_cast<NoiseMapParams*>(ptr);
+       if (L) {
+               LuaPerlinNoiseMap *o = new LuaPerlinNoiseMap(&p->np, p->seed, p->size);
+               *(void **)(lua_newuserdata(L, sizeof(void *))) = o;
+               luaL_getmetatable(L, className);
+               lua_setmetatable(L, -2);
+       }
+       delete p;
+}
+
+
 void LuaPerlinNoiseMap::Register(lua_State *L)
 {
        lua_newtable(L);
@@ -382,6 +433,8 @@ void LuaPerlinNoiseMap::Register(lua_State *L)
        lua_pop(L, 1);
 
        lua_register(L, className, create_object);
+
+       script_register_packer(L, className, packIn, packOut);
 }
 
 
index 29ab41a31f538cd54cbf8e4f6bf6c3b81b2683ef..5d34a479bdd86efedacba60676a473ac6fd88597 100644 (file)
@@ -52,6 +52,9 @@ class LuaPerlinNoise : public ModApiBase
 
        static LuaPerlinNoise *checkobject(lua_State *L, int narg);
 
+       static void *packIn(lua_State *L, int idx);
+       static void packOut(lua_State *L, void *ptr);
+
        static void Register(lua_State *L);
 };
 
@@ -91,6 +94,9 @@ class LuaPerlinNoiseMap : public ModApiBase
 
        static LuaPerlinNoiseMap *checkobject(lua_State *L, int narg);
 
+       static void *packIn(lua_State *L, int idx);
+       static void packOut(lua_State *L, void *ptr);
+
        static void Register(lua_State *L);
 };
 
index 42725e5d2e6d7426c393803b1481685140f53168..4b0b45887f5bc11a8b7a549ac256814aaeec204a 100644 (file)
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_internal.h"
 #include "common/c_converter.h"
 #include "common/c_content.h"
+#include "common/c_packer.h"
 #include "cpp_api/s_base.h"
 #include "cpp_api/s_security.h"
 #include "scripting_server.h"
@@ -526,6 +527,76 @@ int ModApiServer::l_notify_authentication_modified(lua_State *L)
        return 0;
 }
 
+// do_async_callback(func, params, mod_origin)
+int ModApiServer::l_do_async_callback(lua_State *L)
+{
+       NO_MAP_LOCK_REQUIRED;
+       ServerScripting *script = getScriptApi<ServerScripting>(L);
+
+       luaL_checktype(L, 1, LUA_TFUNCTION);
+       luaL_checktype(L, 2, LUA_TTABLE);
+       luaL_checktype(L, 3, LUA_TSTRING);
+
+       call_string_dump(L, 1);
+       size_t func_length;
+       const char *serialized_func_raw = lua_tolstring(L, -1, &func_length);
+
+       PackedValue *param = script_pack(L, 2);
+
+       std::string mod_origin = readParam<std::string>(L, 3);
+
+       u32 jobId = script->queueAsync(
+               std::string(serialized_func_raw, func_length),
+               param, mod_origin);
+
+       lua_settop(L, 0);
+       lua_pushinteger(L, jobId);
+       return 1;
+}
+
+// register_async_dofile(path)
+int ModApiServer::l_register_async_dofile(lua_State *L)
+{
+       NO_MAP_LOCK_REQUIRED;
+
+       std::string path = readParam<std::string>(L, 1);
+       CHECK_SECURE_PATH(L, path.c_str(), false);
+
+       // Find currently running mod name (only at init time)
+       lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_CURRENT_MOD_NAME);
+       if (!lua_isstring(L, -1))
+               return 0;
+       std::string modname = readParam<std::string>(L, -1);
+
+       getServer(L)->m_async_init_files.emplace_back(modname, path);
+       lua_pushboolean(L, true);
+       return 1;
+}
+
+// serialize_roundtrip(value)
+// Meant for unit testing the packer from Lua
+int ModApiServer::l_serialize_roundtrip(lua_State *L)
+{
+       NO_MAP_LOCK_REQUIRED;
+
+       int top = lua_gettop(L);
+       auto *pv = script_pack(L, 1);
+       if (top != lua_gettop(L))
+               throw LuaError("stack values leaked");
+
+#ifndef NDEBUG
+       script_dump_packed(pv);
+#endif
+
+       top = lua_gettop(L);
+       script_unpack(L, pv);
+       delete pv;
+       if (top + 1 != lua_gettop(L))
+               throw LuaError("stack values leaked");
+
+       return 1;
+}
+
 void ModApiServer::Initialize(lua_State *L, int top)
 {
        API_FCT(request_shutdown);
@@ -559,4 +630,18 @@ void ModApiServer::Initialize(lua_State *L, int top)
        API_FCT(remove_player);
        API_FCT(unban_player_or_ip);
        API_FCT(notify_authentication_modified);
+
+       API_FCT(do_async_callback);
+       API_FCT(register_async_dofile);
+       API_FCT(serialize_roundtrip);
+}
+
+void ModApiServer::InitializeAsync(lua_State *L, int top)
+{
+       API_FCT(get_worldpath);
+       API_FCT(is_singleplayer);
+
+       API_FCT(get_current_modname);
+       API_FCT(get_modpath);
+       API_FCT(get_modnames);
 }
index f05c0b7c9bdf37ed77f610a07dead1608df240e3..a4f38c34e7d7c06581f0c58e05d7e2753b52d995 100644 (file)
@@ -106,6 +106,16 @@ class ModApiServer : public ModApiBase
        // notify_authentication_modified(name)
        static int l_notify_authentication_modified(lua_State *L);
 
+       // do_async_callback(func, params, mod_origin)
+       static int l_do_async_callback(lua_State *L);
+
+       // register_async_dofile(path)
+       static int l_register_async_dofile(lua_State *L);
+
+       // serialize_roundtrip(obj)
+       static int l_serialize_roundtrip(lua_State *L);
+
 public:
        static void Initialize(lua_State *L, int top);
+       static void InitializeAsync(lua_State *L, int top);
 };
index b04f26fda974b85d5b4a15226d0c277380fc1418..97068ce4c36c9b1a8a2cdb6683e4c2785b5defe9 100644 (file)
@@ -671,6 +671,9 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
        API_FCT(cpdir);
        API_FCT(mvdir);
        API_FCT(get_dir_list);
+       API_FCT(safe_file_write);
+
+       API_FCT(request_insecure_environment);
 
        API_FCT(encode_base64);
        API_FCT(decode_base64);
@@ -680,6 +683,8 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top)
        API_FCT(colorspec_to_colorstring);
        API_FCT(colorspec_to_bytes);
 
+       API_FCT(encode_png);
+
        API_FCT(get_last_run_mod);
        API_FCT(set_last_run_mod);
 
index fcf8a10576fe2c5bc55764aa67cbf9f815645542..cc55635776bddb00f03b0e0e43ba3c20ebb24a93 100644 (file)
@@ -129,6 +129,4 @@ class ModApiUtil : public ModApiBase
        static void Initialize(lua_State *L, int top);
        static void InitializeAsync(lua_State *L, int top);
        static void InitializeClient(lua_State *L, int top);
-
-       static void InitializeAsync(AsyncEngine &engine);
 };
index a3ece627c7322589e474865a22bfb735f5b13758..6187a47db2c2b238605702201e021c5d2d96a6cc 100644 (file)
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_internal.h"
 #include "common/c_content.h"
 #include "common/c_converter.h"
+#include "common/c_packer.h"
 #include "emerge.h"
 #include "environment.h"
 #include "map.h"
@@ -45,6 +46,8 @@ int LuaVoxelManip::l_read_from_map(lua_State *L)
 
        LuaVoxelManip *o = checkobject(L, 1);
        MMVManip *vm = o->vm;
+       if (vm->isOrphan())
+               return 0;
 
        v3s16 bp1 = getNodeBlockPos(check_v3s16(L, 2));
        v3s16 bp2 = getNodeBlockPos(check_v3s16(L, 3));
@@ -429,6 +432,34 @@ LuaVoxelManip *LuaVoxelManip::checkobject(lua_State *L, int narg)
        return *(LuaVoxelManip **)ud;  // unbox pointer
 }
 
+void *LuaVoxelManip::packIn(lua_State *L, int idx)
+{
+       LuaVoxelManip *o = checkobject(L, idx);
+
+       if (o->is_mapgen_vm)
+               throw LuaError("nope");
+       return o->vm->clone();
+}
+
+void LuaVoxelManip::packOut(lua_State *L, void *ptr)
+{
+       MMVManip *vm = reinterpret_cast<MMVManip*>(ptr);
+       if (!L) {
+               delete vm;
+               return;
+       }
+
+       // Associate vmanip with map if the Lua env has one
+       Environment *env = getEnv(L);
+       if (env)
+               vm->reparent(&(env->getMap()));
+
+       LuaVoxelManip *o = new LuaVoxelManip(vm, false);
+       *(void **)(lua_newuserdata(L, sizeof(void *))) = o;
+       luaL_getmetatable(L, className);
+       lua_setmetatable(L, -2);
+}
+
 void LuaVoxelManip::Register(lua_State *L)
 {
        lua_newtable(L);
@@ -455,6 +486,8 @@ void LuaVoxelManip::Register(lua_State *L)
 
        // Can be created from Lua (VoxelManip())
        lua_register(L, className, create_object);
+
+       script_register_packer(L, className, packIn, packOut);
 }
 
 const char LuaVoxelManip::className[] = "VoxelManip";
index 5113070dc573d0092398ad857e8f1570dd5dc1e1..00513333567c142d817967efc514481e44ddfbd5 100644 (file)
@@ -75,5 +75,8 @@ class LuaVoxelManip : public ModApiBase
 
        static LuaVoxelManip *checkobject(lua_State *L, int narg);
 
+       static void *packIn(lua_State *L, int idx);
+       static void packOut(lua_State *L, void *ptr);
+
        static void Register(lua_State *L);
 };
index 85411ded460e5f68b885014544f94d068c72219d..5b99468dcc28c7d7b9d6f93a992c1eef0257e6f7 100644 (file)
@@ -47,11 +47,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_storage.h"
 
 extern "C" {
-#include "lualib.h"
+#include <lualib.h>
 }
 
 ServerScripting::ServerScripting(Server* server):
-               ScriptApiBase(ScriptingType::Server)
+               ScriptApiBase(ScriptingType::Server),
+               asyncEngine(server)
 {
        setGameDef(server);
 
@@ -88,6 +89,47 @@ ServerScripting::ServerScripting(Server* server):
        infostream << "SCRIPTAPI: Initialized game modules" << std::endl;
 }
 
+void ServerScripting::initAsync()
+{
+       // Save globals to transfer
+       {
+               lua_State *L = getStack();
+               lua_getglobal(L, "core");
+               luaL_checktype(L, -1, LUA_TTABLE);
+               lua_getfield(L, -1, "get_globals_to_transfer");
+               lua_call(L, 0, 1);
+               luaL_checktype(L, -1, LUA_TSTRING);
+               getServer()->m_async_globals_data.set(readParam<std::string>(L, -1));
+               lua_pushnil(L);
+               lua_setfield(L, -3, "get_globals_to_transfer"); // unset function too
+               lua_pop(L, 2); // pop 'core', return value
+       }
+
+       infostream << "SCRIPTAPI: Initializing async engine" << std::endl;
+       asyncEngine.registerStateInitializer(InitializeAsync);
+       asyncEngine.registerStateInitializer(ModApiUtil::InitializeAsync);
+       asyncEngine.registerStateInitializer(ModApiCraft::InitializeAsync);
+       asyncEngine.registerStateInitializer(ModApiItemMod::InitializeAsync);
+       asyncEngine.registerStateInitializer(ModApiServer::InitializeAsync);
+       // not added: ModApiMapgen is a minefield for thread safety
+       // not added: ModApiHttp async api can't really work together with our jobs
+       // not added: ModApiStorage is probably not thread safe(?)
+
+       asyncEngine.initialize(0);
+}
+
+void ServerScripting::stepAsync()
+{
+       asyncEngine.step(getStack());
+}
+
+u32 ServerScripting::queueAsync(std::string &&serialized_func,
+       PackedValue *param, const std::string &mod_origin)
+{
+       return asyncEngine.queueAsyncJob(std::move(serialized_func),
+                       param, mod_origin);
+}
+
 void ServerScripting::InitializeModApi(lua_State *L, int top)
 {
        // Register reference classes (userdata)
@@ -125,3 +167,24 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
        ModApiStorage::Initialize(L, top);
        ModApiChannels::Initialize(L, top);
 }
+
+void ServerScripting::InitializeAsync(lua_State *L, int top)
+{
+       // classes
+       LuaItemStack::Register(L);
+       LuaPerlinNoise::Register(L);
+       LuaPerlinNoiseMap::Register(L);
+       LuaPseudoRandom::Register(L);
+       LuaPcgRandom::Register(L);
+       LuaSecureRandom::Register(L);
+       LuaVoxelManip::Register(L);
+       LuaSettings::Register(L);
+
+       // globals data
+       lua_getglobal(L, "core");
+       luaL_checktype(L, -1, LUA_TTABLE);
+       std::string s = ModApiBase::getServer(L)->m_async_globals_data.get();
+       lua_pushlstring(L, s.c_str(), s.size());
+       lua_setfield(L, -2, "transferred_globals");
+       lua_pop(L, 1); // pop 'core'
+}
index bf06ab197fc49fe8e7ac088d24b2c4d84b1102f5..9803397c5390fa1a39a32c3003e1c12253f357f2 100644 (file)
@@ -27,6 +27,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "cpp_api/s_player.h"
 #include "cpp_api/s_server.h"
 #include "cpp_api/s_security.h"
+#include "cpp_api/s_async.h"
+
+struct PackedValue;
 
 /*****************************************************************************/
 /* Scripting <-> Server Game Interface                                       */
@@ -48,6 +51,20 @@ class ServerScripting:
 
        // use ScriptApiBase::loadMod() to load mods
 
+       // Initialize async engine, call this AFTER loading all mods
+       void initAsync();
+
+       // Global step handler to collect async results
+       void stepAsync();
+
+       // Pass job to async threads
+       u32 queueAsync(std::string &&serialized_func,
+               PackedValue *param, const std::string &mod_origin);
+
 private:
        void InitializeModApi(lua_State *L, int top);
+
+       static void InitializeAsync(lua_State *L, int top);
+
+       AsyncEngine asyncEngine;
 };
index dec6cf44ce3e28af18c452ee73aa32f00ffc6c1b..c9cd4e398686987efcbc2362923ecec61d7a693b 100644 (file)
@@ -243,6 +243,7 @@ Server::Server(
        m_clients(m_con),
        m_admin_chat(iface),
        m_on_shutdown_errmsg(on_shutdown_errmsg),
+       m_async_globals_data(""),
        m_modchannel_mgr(new ModChannelMgr())
 {
        if (m_path_world.empty())
@@ -480,6 +481,9 @@ void Server::init()
        // Give environment reference to scripting api
        m_script->initializeEnvironment(m_env);
 
+       // Do this after regular script init is done
+       m_script->initAsync();
+
        // Register us to receive map edit events
        servermap->addEventReceiver(this);
 
index bd799c31363288854d1d4ca686d8e4f7fe7fdd3a..5090a35792c25d6099d6091bcb7e051b116b819d 100644 (file)
@@ -292,7 +292,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 
        virtual const std::vector<ModSpec> &getMods() const;
        virtual const ModSpec* getModSpec(const std::string &modname) const;
-       std::string getBuiltinLuaPath();
+       static std::string getBuiltinLuaPath();
        virtual std::string getWorldPath() const { return m_path_world; }
 
        inline bool isSingleplayer() const
@@ -385,6 +385,12 @@ class Server : public con::PeerHandler, public MapEventReceiver,
        static bool migrateModStorageDatabase(const GameParams &game_params,
                        const Settings &cmd_args);
 
+       // Lua files registered for init of async env, pair of modname + path
+       std::vector<std::pair<std::string, std::string>> m_async_init_files;
+
+       // Serialized data transferred into async envs at init time
+       MutexedVariable<std::string> m_async_globals_data;
+
        // Bind address
        Address m_bind_addr;
 
index f8d84604b0bcc5a16077c09660c5c5ea2e79abbf..4932107449c96703d7f76799d1201dfd3ecc6337 100644 (file)
@@ -1481,6 +1481,8 @@ void ServerEnvironment::step(float dtime)
        */
        m_script->environment_Step(dtime);
 
+       m_script->stepAsync();
+
        /*
                Step active objects
        */
index 334e342e095e08a9e6b05c6ea3831913c40b5250..3910c6185278acd09f0cda82e50022baf2d1c02d 100644 (file)
@@ -29,13 +29,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define CONTAINS(c, v) (std::find((c).begin(), (c).end(), (v)) != (c).end())
 
 // To disable copy constructors and assignment operations for some class
-// 'Foobar', add the macro DISABLE_CLASS_COPY(Foobar) as a private member.
+// 'Foobar', add the macro DISABLE_CLASS_COPY(Foobar) in the class definition.
 // Note this also disables copying for any classes derived from 'Foobar' as well
 // as classes having a 'Foobar' member.
 #define DISABLE_CLASS_COPY(C)        \
        C(const C &) = delete;           \
        C &operator=(const C &) = delete;
 
+// If you have used DISABLE_CLASS_COPY with a class but still want to permit moving
+// use this macro to add the default move constructors back.
+#define ALLOW_CLASS_MOVE(C)      \
+       C(C &&other) = default;      \
+       C &operator=(C &&) = default;
+
 #ifndef _MSC_VER
        #define UNUSED_ATTRIBUTE __attribute__ ((unused))
 #else