mirror of
https://github.com/luanti-org/luanti.git
synced 2025-07-02 16:38:41 +00:00
Async environment for mods to do concurrent tasks (#11131)
This commit is contained in:
parent
663c936428
commit
e7659883cc
38 changed files with 1646 additions and 48 deletions
583
src/script/common/c_packer.cpp
Normal file
583
src/script/common/c_packer.cpp
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue