mirror of
https://github.com/minetest/minetest.git
synced 2025-03-06 20:48:40 +01:00
Add generic IPC mechanism between Lua envs
This commit is contained in:
parent
06907aa99b
commit
f1a436619f
12 changed files with 191 additions and 19 deletions
|
@ -6855,17 +6855,6 @@ This allows you easy interoperability for delegating work to jobs.
|
||||||
* Register a path to a Lua file to be imported when an async environment
|
* 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
|
is initialized. You can use this to preload code which you can then call
|
||||||
later using `minetest.handle_async()`.
|
later using `minetest.handle_async()`.
|
||||||
* `minetest.register_portable_metatable(name, mt)`:
|
|
||||||
* Register a metatable that should be preserved when data is transferred
|
|
||||||
between the main thread and the async environment.
|
|
||||||
* `name` is a string that identifies the metatable. It is recommended to
|
|
||||||
follow the `modname:name` convention for this identifier.
|
|
||||||
* `mt` is the metatable to register.
|
|
||||||
* Note that it is allowed to register the same metatable under multiple
|
|
||||||
names, but it is not allowed to register multiple metatables under the
|
|
||||||
same name.
|
|
||||||
* You must register the metatable in both the main environment
|
|
||||||
and the async environment for this mechanism to work.
|
|
||||||
|
|
||||||
|
|
||||||
### List of APIs available in an async environment
|
### List of APIs available in an async environment
|
||||||
|
@ -6895,7 +6884,8 @@ Functions:
|
||||||
|
|
||||||
* Standalone helpers such as logging, filesystem, encoding,
|
* Standalone helpers such as logging, filesystem, encoding,
|
||||||
hashing or compression APIs
|
hashing or compression APIs
|
||||||
* `minetest.register_portable_metatable` (see above)
|
* `minetest.register_portable_metatable`
|
||||||
|
* IPC
|
||||||
|
|
||||||
Variables:
|
Variables:
|
||||||
|
|
||||||
|
@ -6973,6 +6963,7 @@ Functions:
|
||||||
* `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`,
|
* `minetest.get_node`, `set_node`, `find_node_near`, `find_nodes_in_area`,
|
||||||
`spawn_tree` and similar
|
`spawn_tree` and similar
|
||||||
* these only operate on the current chunk (if inside a callback)
|
* these only operate on the current chunk (if inside a callback)
|
||||||
|
* IPC
|
||||||
|
|
||||||
Variables:
|
Variables:
|
||||||
|
|
||||||
|
@ -7050,6 +7041,31 @@ Server
|
||||||
this can make transfer of bigger files painless (if set up). Nevertheless
|
this can make transfer of bigger files painless (if set up). Nevertheless
|
||||||
it is advised not to use dynamic media for big media files.
|
it is advised not to use dynamic media for big media files.
|
||||||
|
|
||||||
|
IPC
|
||||||
|
---
|
||||||
|
|
||||||
|
The engine provides a generalized mechanism to enable sharing data between the
|
||||||
|
different Lua environments (main, mapgen and async).
|
||||||
|
It is essentially a shared in-memory key-value store.
|
||||||
|
|
||||||
|
* `minetest.ipc_get(key)`:
|
||||||
|
* Read a value from the shared data area.
|
||||||
|
* `key`: string, should use the `"modname:thing"` convention to avoid conflicts.
|
||||||
|
* returns an arbitrary Lua value, or `nil` if this key does not exist
|
||||||
|
* `minetest.ipc_set(key, value)`:
|
||||||
|
* Write a value to the shared data area.
|
||||||
|
* `key`: as above
|
||||||
|
* `value`: an arbitrary Lua value, cannot be or contain userdata.
|
||||||
|
|
||||||
|
Interacting with the shared data will perform an operation comparable to
|
||||||
|
(de)serialization on each access.
|
||||||
|
For that reason modifying references will not have any effect, as in this example:
|
||||||
|
```lua
|
||||||
|
minetest.ipc_set("test:foo", {})
|
||||||
|
minetest.ipc_get("test:foo").subkey = "value" -- WRONG!
|
||||||
|
minetest.ipc_get("test:foo") -- returns an empty table
|
||||||
|
```
|
||||||
|
|
||||||
Bans
|
Bans
|
||||||
----
|
----
|
||||||
|
|
||||||
|
@ -7449,6 +7465,17 @@ Misc.
|
||||||
* `minetest.global_exists(name)`
|
* `minetest.global_exists(name)`
|
||||||
* Checks if a global variable has been set, without triggering a warning.
|
* Checks if a global variable has been set, without triggering a warning.
|
||||||
|
|
||||||
|
* `minetest.register_portable_metatable(name, mt)`:
|
||||||
|
* Register a metatable that should be preserved when Lua data is transferred
|
||||||
|
between environments (via IPC or `handle_async`).
|
||||||
|
* `name` is a string that identifies the metatable. It is recommended to
|
||||||
|
follow the `modname:name` convention for this identifier.
|
||||||
|
* `mt` is the metatable to register.
|
||||||
|
* Note that the same metatable can be registered under multiple names,
|
||||||
|
but multiple metatables must not be registered under the same name.
|
||||||
|
* You must register the metatable in both the main environment
|
||||||
|
and the async environment for this mechanism to work.
|
||||||
|
|
||||||
Global objects
|
Global objects
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,8 @@ local function do_tests()
|
||||||
assert(core.registered_items["unittests:description_test"].on_place == true)
|
assert(core.registered_items["unittests:description_test"].on_place == true)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- there's no (usable) communcation path between mapgen and the regular env
|
-- this is checked from the main env
|
||||||
-- so we just run the test unconditionally
|
core.ipc_set("unittests:mg", { pcall(do_tests) })
|
||||||
do_tests()
|
|
||||||
|
|
||||||
core.register_on_generated(function(vm, pos1, pos2, blockseed)
|
core.register_on_generated(function(vm, pos1, pos2, blockseed)
|
||||||
local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1
|
local n = tonumber(core.get_mapgen_setting("chunksize")) * 16 - 1
|
||||||
|
|
|
@ -254,3 +254,28 @@ local function test_gennotify_api()
|
||||||
assert(#custom == 0, "custom ids not empty")
|
assert(#custom == 0, "custom ids not empty")
|
||||||
end
|
end
|
||||||
unittests.register("test_gennotify_api", test_gennotify_api)
|
unittests.register("test_gennotify_api", test_gennotify_api)
|
||||||
|
|
||||||
|
-- <=> inside_mapgen_env.lua
|
||||||
|
local function test_mapgen_env(cb)
|
||||||
|
-- emerge threads start delayed so this can take a second
|
||||||
|
local res = core.ipc_get("unittests:mg")
|
||||||
|
if res == nil then
|
||||||
|
return core.after(0, test_mapgen_env, cb)
|
||||||
|
end
|
||||||
|
-- handle error status
|
||||||
|
if res[1] then
|
||||||
|
cb()
|
||||||
|
else
|
||||||
|
cb(res[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
unittests.register("test_mapgen_env", test_mapgen_env, {async=true})
|
||||||
|
|
||||||
|
local function test_ipc_vector_preserve(cb)
|
||||||
|
-- the IPC also uses register_portable_metatable
|
||||||
|
core.ipc_set("unittests:v", vector.new(4, 0, 4))
|
||||||
|
local v = core.ipc_get("unittests:v")
|
||||||
|
assert(type(v) == "table")
|
||||||
|
assert(vector.check(v))
|
||||||
|
end
|
||||||
|
unittests.register("test_ipc_vector_preserve", test_ipc_vector_preserve)
|
||||||
|
|
|
@ -34,19 +34,19 @@ class Camera;
|
||||||
class ModChannel;
|
class ModChannel;
|
||||||
class ModStorage;
|
class ModStorage;
|
||||||
class ModStorageDatabase;
|
class ModStorageDatabase;
|
||||||
|
struct SubgameSpec;
|
||||||
|
struct ModSpec;
|
||||||
|
struct ModIPCStore;
|
||||||
|
|
||||||
namespace irr::scene {
|
namespace irr::scene {
|
||||||
class IAnimatedMesh;
|
class IAnimatedMesh;
|
||||||
class ISceneManager;
|
class ISceneManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SubgameSpec;
|
|
||||||
struct ModSpec;
|
|
||||||
/*
|
/*
|
||||||
An interface for fetching game-global definitions like tool and
|
An interface for fetching game-global definitions like tool and
|
||||||
mapnode properties
|
mapnode properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class IGameDef
|
class IGameDef
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -63,6 +63,9 @@ public:
|
||||||
// environment thread.
|
// environment thread.
|
||||||
virtual IRollbackManager* getRollbackManager() { return NULL; }
|
virtual IRollbackManager* getRollbackManager() { return NULL; }
|
||||||
|
|
||||||
|
// Only usable on server.
|
||||||
|
virtual ModIPCStore *getModIPCStore() { return nullptr; }
|
||||||
|
|
||||||
// Shorthands
|
// Shorthands
|
||||||
// TODO: these should be made const-safe so that a const IGameDef* is
|
// TODO: these should be made const-safe so that a const IGameDef* is
|
||||||
// actually usable
|
// actually usable
|
||||||
|
|
|
@ -50,11 +50,12 @@ AsyncEngine::~AsyncEngine()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for threads to finish
|
// Wait for threads to finish
|
||||||
|
infostream << "AsyncEngine: Waiting for " << workerThreads.size()
|
||||||
|
<< " threads" << std::endl;
|
||||||
for (AsyncWorkerThread *workerThread : workerThreads) {
|
for (AsyncWorkerThread *workerThread : workerThreads) {
|
||||||
workerThread->wait();
|
workerThread->wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force kill all threads
|
|
||||||
for (AsyncWorkerThread *workerThread : workerThreads) {
|
for (AsyncWorkerThread *workerThread : workerThreads) {
|
||||||
delete workerThread;
|
delete workerThread;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ set(common_SCRIPT_LUA_API_SRCS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_http.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_http.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_inventory.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_inventory.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/l_ipc.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_item.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_item.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_itemstackmeta.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_itemstackmeta.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/l_mapgen.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/l_mapgen.cpp
|
||||||
|
|
68
src/script/lua_api/l_ipc.cpp
Normal file
68
src/script/lua_api/l_ipc.cpp
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Minetest
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
#include "lua_api/l_ipc.h"
|
||||||
|
#include "lua_api/l_internal.h"
|
||||||
|
#include "common/c_packer.h"
|
||||||
|
#include "server.h"
|
||||||
|
#include "debug.h"
|
||||||
|
|
||||||
|
typedef std::shared_lock<std::shared_mutex> SharedReadLock;
|
||||||
|
typedef std::unique_lock<std::shared_mutex> SharedWriteLock;
|
||||||
|
|
||||||
|
int ModApiIPC::l_ipc_get(lua_State *L)
|
||||||
|
{
|
||||||
|
auto *store = getGameDef(L)->getModIPCStore();
|
||||||
|
|
||||||
|
auto key = readParam<std::string>(L, 1);
|
||||||
|
|
||||||
|
{
|
||||||
|
SharedReadLock autolock(store->mutex);
|
||||||
|
auto it = store->map.find(key);
|
||||||
|
if (it == store->map.end())
|
||||||
|
lua_pushnil(L);
|
||||||
|
else
|
||||||
|
script_unpack(L, it->second.get());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ModApiIPC::l_ipc_set(lua_State *L)
|
||||||
|
{
|
||||||
|
auto *store = getGameDef(L)->getModIPCStore();
|
||||||
|
|
||||||
|
auto key = readParam<std::string>(L, 1);
|
||||||
|
|
||||||
|
luaL_checkany(L, 2);
|
||||||
|
std::unique_ptr<PackedValue> pv;
|
||||||
|
if (!lua_isnil(L, 2)) {
|
||||||
|
pv.reset(script_pack(L, 2));
|
||||||
|
if (pv->contains_userdata)
|
||||||
|
throw LuaError("Userdata not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SharedWriteLock autolock(store->mutex);
|
||||||
|
if (pv)
|
||||||
|
store->map[key] = std::move(pv);
|
||||||
|
else
|
||||||
|
store->map.erase(key); // delete the map value for nil
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Implementation note:
|
||||||
|
* Iterating over the IPC table is intentionally not supported.
|
||||||
|
* Mods should know what they have set.
|
||||||
|
* This has the nice side effect that mods are able to use a randomly generated key
|
||||||
|
* if they really *really* want to avoid other code touching their data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void ModApiIPC::Initialize(lua_State *L, int top)
|
||||||
|
{
|
||||||
|
FATAL_ERROR_IF(!getGameDef(L)->getModIPCStore(), "ModIPCStore missing from gamedef");
|
||||||
|
|
||||||
|
API_FCT(ipc_get);
|
||||||
|
API_FCT(ipc_set);
|
||||||
|
}
|
15
src/script/lua_api/l_ipc.h
Normal file
15
src/script/lua_api/l_ipc.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// Minetest
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lua_api/l_base.h"
|
||||||
|
|
||||||
|
class ModApiIPC : public ModApiBase {
|
||||||
|
private:
|
||||||
|
static int l_ipc_get(lua_State *L);
|
||||||
|
static int l_ipc_set(lua_State *L);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static void Initialize(lua_State *L, int top);
|
||||||
|
};
|
|
@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
#include "lua_api/l_util.h"
|
#include "lua_api/l_util.h"
|
||||||
#include "lua_api/l_vmanip.h"
|
#include "lua_api/l_vmanip.h"
|
||||||
#include "lua_api/l_settings.h"
|
#include "lua_api/l_settings.h"
|
||||||
|
#include "lua_api/l_ipc.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <lualib.h>
|
#include <lualib.h>
|
||||||
|
@ -89,5 +90,6 @@ void EmergeScripting::InitializeModApi(lua_State *L, int top)
|
||||||
ModApiMapgen::InitializeEmerge(L, top);
|
ModApiMapgen::InitializeEmerge(L, top);
|
||||||
ModApiServer::InitializeAsync(L, top);
|
ModApiServer::InitializeAsync(L, top);
|
||||||
ModApiUtil::InitializeAsync(L, top);
|
ModApiUtil::InitializeAsync(L, top);
|
||||||
|
ModApiIPC::Initialize(L, top);
|
||||||
// TODO ^ these should also be renamed to InitializeRO or such
|
// TODO ^ these should also be renamed to InitializeRO or such
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
#include "lua_api/l_settings.h"
|
#include "lua_api/l_settings.h"
|
||||||
#include "lua_api/l_http.h"
|
#include "lua_api/l_http.h"
|
||||||
#include "lua_api/l_storage.h"
|
#include "lua_api/l_storage.h"
|
||||||
|
#include "lua_api/l_ipc.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <lualib.h>
|
#include <lualib.h>
|
||||||
|
@ -121,6 +122,7 @@ void ServerScripting::initAsync()
|
||||||
asyncEngine.registerStateInitializer(ModApiCraft::InitializeAsync);
|
asyncEngine.registerStateInitializer(ModApiCraft::InitializeAsync);
|
||||||
asyncEngine.registerStateInitializer(ModApiItem::InitializeAsync);
|
asyncEngine.registerStateInitializer(ModApiItem::InitializeAsync);
|
||||||
asyncEngine.registerStateInitializer(ModApiServer::InitializeAsync);
|
asyncEngine.registerStateInitializer(ModApiServer::InitializeAsync);
|
||||||
|
asyncEngine.registerStateInitializer(ModApiIPC::Initialize);
|
||||||
// not added: ModApiMapgen is a minefield for thread safety
|
// not added: ModApiMapgen is a minefield for thread safety
|
||||||
// not added: ModApiHttp async api can't really work together with our jobs
|
// not added: ModApiHttp async api can't really work together with our jobs
|
||||||
// not added: ModApiStorage is probably not thread safe(?)
|
// not added: ModApiStorage is probably not thread safe(?)
|
||||||
|
@ -176,6 +178,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
|
||||||
ModApiHttp::Initialize(L, top);
|
ModApiHttp::Initialize(L, top);
|
||||||
ModApiStorage::Initialize(L, top);
|
ModApiStorage::Initialize(L, top);
|
||||||
ModApiChannels::Initialize(L, top);
|
ModApiChannels::Initialize(L, top);
|
||||||
|
ModApiIPC::Initialize(L, top);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServerScripting::InitializeAsync(lua_State *L, int top)
|
void ServerScripting::InitializeAsync(lua_State *L, int top)
|
||||||
|
|
|
@ -86,6 +86,15 @@ public:
|
||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ModIPCStore::~ModIPCStore()
|
||||||
|
{
|
||||||
|
// we don't have to do this, it's pure debugging aid
|
||||||
|
if (!std::unique_lock(mutex, std::try_to_lock).owns_lock()) {
|
||||||
|
errorstream << FUNCTION_NAME << ": lock is still in use!" << std::endl;
|
||||||
|
assert(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ServerThread : public Thread
|
class ServerThread : public Thread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
19
src/server.h
19
src/server.h
|
@ -47,6 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <shared_mutex>
|
||||||
|
|
||||||
class ChatEvent;
|
class ChatEvent;
|
||||||
struct ChatEventChat;
|
struct ChatEventChat;
|
||||||
|
@ -142,6 +143,20 @@ struct ClientInfo {
|
||||||
std::string vers_string, lang_code;
|
std::string vers_string, lang_code;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ModIPCStore {
|
||||||
|
ModIPCStore() = default;
|
||||||
|
~ModIPCStore();
|
||||||
|
|
||||||
|
/// RW lock for this entire structure
|
||||||
|
std::shared_mutex mutex;
|
||||||
|
/**
|
||||||
|
* Map storing the data
|
||||||
|
*
|
||||||
|
* @note Do not store `nil` data in this map, instead remove the whole key.
|
||||||
|
*/
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<PackedValue>> map;
|
||||||
|
};
|
||||||
|
|
||||||
class Server : public con::PeerHandler, public MapEventReceiver,
|
class Server : public con::PeerHandler, public MapEventReceiver,
|
||||||
public IGameDef
|
public IGameDef
|
||||||
{
|
{
|
||||||
|
@ -301,12 +316,14 @@ public:
|
||||||
NodeDefManager* getWritableNodeDefManager();
|
NodeDefManager* getWritableNodeDefManager();
|
||||||
IWritableCraftDefManager* getWritableCraftDefManager();
|
IWritableCraftDefManager* getWritableCraftDefManager();
|
||||||
|
|
||||||
|
// Not under envlock
|
||||||
virtual const std::vector<ModSpec> &getMods() const;
|
virtual const std::vector<ModSpec> &getMods() const;
|
||||||
virtual const ModSpec* getModSpec(const std::string &modname) const;
|
virtual const ModSpec* getModSpec(const std::string &modname) const;
|
||||||
virtual const SubgameSpec* getGameSpec() const { return &m_gamespec; }
|
virtual const SubgameSpec* getGameSpec() const { return &m_gamespec; }
|
||||||
static std::string getBuiltinLuaPath();
|
static std::string getBuiltinLuaPath();
|
||||||
virtual std::string getWorldPath() const { return m_path_world; }
|
virtual std::string getWorldPath() const { return m_path_world; }
|
||||||
virtual std::string getModDataPath() const { return m_path_mod_data; }
|
virtual std::string getModDataPath() const { return m_path_mod_data; }
|
||||||
|
virtual ModIPCStore *getModIPCStore() { return &m_ipcstore; }
|
||||||
|
|
||||||
inline bool isSingleplayer() const
|
inline bool isSingleplayer() const
|
||||||
{ return m_simple_singleplayer_mode; }
|
{ return m_simple_singleplayer_mode; }
|
||||||
|
@ -666,6 +683,8 @@ private:
|
||||||
|
|
||||||
std::unordered_map<std::string, Translations> server_translations;
|
std::unordered_map<std::string, Translations> server_translations;
|
||||||
|
|
||||||
|
ModIPCStore m_ipcstore;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Threads
|
Threads
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Reference in a new issue