File size: 9,069 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#include "lua_vm.h"
#include "../core/core.h"
#include "../serialization/stream.h"
#include <cstdlib>
#include <cmath>
#include <algorithm>
// Lua is compiled as C++ (see CMakeLists.txt) so script errors unwind with exceptions,
// which runs destructors in binding code instead of longjmp-ing over them.
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

namespace unify {

const char* const kEntityMeta = "unify.Entity";
const char* const kBufferMeta = "unify.Buffer";
const char* const kKernelMeta = "unify.Kernel";

void* LuaVM::alloc(void* ud, void* ptr, size_t osize, size_t nsize) {
    auto* vm = static_cast<LuaVM*>(ud);
    if (ptr == nullptr) osize = 0;  // osize encodes the object type when ptr is NULL
    if (nsize == 0) {
        vm->used_ -= osize;
        std::free(ptr);
        return nullptr;
    }
    if (vm->used_ - osize + nsize > vm->limit_) { vm->failures_++; return nullptr; }  // Lua raises "not enough memory"
    void* p = std::realloc(ptr, nsize);
    if (!p) { vm->failures_++; return nullptr; }
    vm->used_ = vm->used_ - osize + nsize;
    if (vm->used_ > vm->peak_) vm->peak_ = vm->used_;
    return p;
}

LuaVM::LuaVM(size_t memory_limit) : limit_(memory_limit) {
    L_ = lua_newstate(&LuaVM::alloc, this);
    luaL_openlibs(L_);
    // The engine owns collection timing: stop the automatic collector and use
    // incremental steps scheduled once per tick with a fixed budget.
    lua_gc(L_, LUA_GCINC, 0, 0, 0);
    lua_gc(L_, LUA_GCSTOP);
}

LuaVM::~LuaVM() { if (L_) lua_close(L_); }

static int deterministic_random(lua_State* L) {
    Rng* rng = static_cast<Rng*>(lua_touserdata(L, lua_upvalueindex(1)));
    int n = lua_gettop(L);
    if (n == 0) { lua_pushnumber(L, double(rng->next_float())); return 1; }
    lua_Integer lo = 1, hi;
    if (n == 1) hi = luaL_checkinteger(L, 1);
    else { lo = luaL_checkinteger(L, 1); hi = luaL_checkinteger(L, 2); }
    luaL_argcheck(L, lo <= hi, n, "interval is empty");
    lua_pushinteger(L, rng->range_int(int32_t(lo), int32_t(hi)));
    return 1;
}

void LuaVM::sandbox(Rng* rng) {
    // Nondeterministic or unsafe services are not reachable from gameplay code.
    for (const char* g : {"os", "io", "debug", "dofile", "loadfile", "require", "package", "collectgarbage"}) {
        lua_pushnil(L_);
        lua_setglobal(L_, g);
    }
    lua_getglobal(L_, "math");
    lua_pushlightuserdata(L_, rng);
    lua_pushcclosure(L_, deterministic_random, 1);
    lua_setfield(L_, -2, "random");
    lua_pushnil(L_);
    lua_setfield(L_, -2, "randomseed");
    lua_pop(L_, 1);
}

bool LuaVM::run(const std::string& code, const std::string& chunk_name, int results) {
    if (luaL_loadbuffer(L_, code.data(), code.size(), ("@" + chunk_name).c_str()) != LUA_OK) {
        error_ = lua_tostring(L_, -1);
        lua_pop(L_, 1);
        return false;
    }
    if (lua_pcall(L_, 0, results, 0) != LUA_OK) {
        error_ = lua_tostring(L_, -1);
        lua_pop(L_, 1);
        return false;
    }
    return true;
}

bool LuaVM::call_global(const char* name, int nargs, int nresults) {
    lua_getglobal(L_, name);
    if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1 + nargs); return true; }
    if (nargs) lua_insert(L_, -(nargs + 1));
    if (lua_pcall(L_, nargs, nresults, 0) != LUA_OK) {
        error_ = lua_tostring(L_, -1);
        lua_pop(L_, 1);
        return false;
    }
    return true;
}

void LuaVM::gc_step() { if (!paused_) lua_gc(L_, LUA_GCSTEP, budget_kb_); }
void LuaVM::gc_budget(int kb) { budget_kb_ = kb < 1 ? 1 : kb; }
void LuaVM::gc_pause() { paused_ = true; }
void LuaVM::gc_resume() { paused_ = false; }
void LuaVM::gc_collect() { lua_gc(L_, LUA_GCCOLLECT); lua_gc(L_, LUA_GCSTOP); }

// ------------------------------------------------------------------------------ serialization
// Tags: 0 nil, 1 false, 2 true, 3 integer, 4 float, 5 string, 6 table, 7 entity, 8 buffer, 9 kernel.
// Handles serialize as handle bits; the owning subsystems restore the same handles.
static bool write_value(lua_State* L, int idx, BinaryWriter& w, std::string& err, int depth) {
    if (depth > 64) { err = "table nesting too deep (cycle?)"; return false; }
    idx = lua_absindex(L, idx);
    switch (lua_type(L, idx)) {
        case LUA_TNIL: w.u8(0); return true;
        case LUA_TBOOLEAN: w.u8(lua_toboolean(L, idx) ? 2 : 1); return true;
        case LUA_TNUMBER:
            if (lua_isinteger(L, idx)) { w.u8(3); w.u64(uint64_t(lua_tointeger(L, idx))); }
            else { w.u8(4); w.f64(lua_tonumber(L, idx)); }
            return true;
        case LUA_TSTRING: { size_t n; const char* s = lua_tolstring(L, idx, &n); w.u8(5); w.str(std::string(s, n)); return true; }
        case LUA_TUSERDATA:
            if (luaL_testudata(L, idx, kEntityMeta)) { w.u8(7); w.u32(*static_cast<uint32_t*>(lua_touserdata(L, idx))); return true; }
            if (luaL_testudata(L, idx, kBufferMeta)) { auto* b = static_cast<LuaBuffer*>(lua_touserdata(L, idx)); w.u8(8); w.u32(b->bits); w.boolean(b->is_int); return true; }
            if (luaL_testudata(L, idx, kKernelMeta)) { w.u8(9); w.u32(*static_cast<uint32_t*>(lua_touserdata(L, idx))); return true; }
            err = "cannot serialize userdata";
            return false;
        case LUA_TTABLE: {
            // Deterministic key order: collect keys, sort (numbers before strings).
            struct Key { bool is_num; lua_Integer i; double d; bool is_int; std::string s; };
            std::vector<Key> keys;
            lua_pushnil(L);
            while (lua_next(L, idx)) {
                lua_pop(L, 1);
                Key k{};
                if (lua_type(L, -1) == LUA_TNUMBER) { k.is_num = true; k.is_int = lua_isinteger(L, -1); k.i = lua_tointeger(L, -1); k.d = lua_tonumber(L, -1); }
                else if (lua_type(L, -1) == LUA_TSTRING) { k.s = lua_tostring(L, -1); }
                else { lua_pop(L, 1); err = "table keys must be numbers or strings"; return false; }
                keys.push_back(k);
            }
            std::sort(keys.begin(), keys.end(), [](const Key& a, const Key& b) {
                if (a.is_num != b.is_num) return a.is_num;
                if (a.is_num) return a.d < b.d;
                return a.s < b.s;
            });
            w.u8(6);
            w.u32(uint32_t(keys.size()));
            for (auto& k : keys) {
                if (k.is_num) { if (k.is_int) lua_pushinteger(L, k.i); else lua_pushnumber(L, k.d); }
                else lua_pushlstring(L, k.s.data(), k.s.size());
                if (!write_value(L, -1, w, err, depth + 1)) { lua_pop(L, 1); return false; }
                lua_gettable(L, idx);
                bool ok = write_value(L, -1, w, err, depth + 1);
                lua_pop(L, 1);
                if (!ok) return false;
            }
            return true;
        }
        default:
            err = std::string("cannot serialize a ") + lua_typename(L, lua_type(L, idx));
            return false;
    }
}

static bool read_value(lua_State* L, BinaryReader& r, std::string& err, int depth) {
    if (depth > 64) { err = "table nesting too deep"; return false; }
    luaL_checkstack(L, 4, "deserialize");
    switch (r.u8()) {
        case 0: lua_pushnil(L); return true;
        case 1: lua_pushboolean(L, 0); return true;
        case 2: lua_pushboolean(L, 1); return true;
        case 3: lua_pushinteger(L, lua_Integer(r.u64())); return true;
        case 4: lua_pushnumber(L, r.f64()); return true;
        case 5: { std::string s = r.str(); lua_pushlstring(L, s.data(), s.size()); return true; }
        case 7: {
            auto* p = static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0));
            *p = r.u32();
            luaL_setmetatable(L, kEntityMeta);
            return true;
        }
        case 8: {
            auto* b = static_cast<LuaBuffer*>(lua_newuserdatauv(L, sizeof(LuaBuffer), 0));
            b->bits = r.u32();
            b->is_int = r.boolean();
            luaL_setmetatable(L, kBufferMeta);
            return true;
        }
        case 9: {
            *static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0)) = r.u32();
            luaL_setmetatable(L, kKernelMeta);
            return true;
        }
        case 6: {
            uint32_t n = r.u32();
            lua_createtable(L, 0, int(n));
            for (uint32_t i = 0; i < n; i++) {
                if (!read_value(L, r, err, depth + 1)) return false;
                if (!read_value(L, r, err, depth + 1)) return false;
                lua_settable(L, -3);
            }
            return true;
        }
        default: err = "corrupt Lua value tag"; return false;
    }
}

bool LuaVM::serialize_value(int index, BinaryWriter& w, std::string& error) { return write_value(L_, index, w, error, 0); }
bool LuaVM::deserialize_value(BinaryReader& r, std::string& error) {
    int top = lua_gettop(L_);
    try {
        if (read_value(L_, r, error, 0)) return true;
    } catch (const SerializationError& e) {
        error = e.what();
    }
    lua_settop(L_, top);
    return false;
}

}  // namespace unify