#include "StarLua.hpp" #include "StarArray.hpp" #include "StarTime.hpp" namespace Star { std::ostream& operator<<(std::ostream& os, LuaValue const& value) { if (value.is()) { os << (value.get() ? "true" : "false"); } else if (value.is()) { os << value.get(); } else if (value.is()) { os << value.get(); } else if (value.is()) { os << value.get().ptr(); } else if (value.is()) { os << "{"; bool first = true; value.get().iterate([&os, &first](LuaValue const& key, LuaValue const& value) { if (first) first = false; else os << ", "; os << key << ": " << value; }); os << "}"; } else if (value.is()) { os << "().handleIndex() << ">"; } else if (value.is()) { os << "().handleIndex() << ">"; } else if (value.is()) { os << "().handleIndex() << ">"; } else { os << "nil"; } return os; } bool LuaTable::contains(char const* key) const { return engine().tableGet(false, handleIndex(), key) != LuaNil; } void LuaTable::remove(char const* key) const { engine().tableSet(false, handleIndex(), key, LuaNil); } LuaInt LuaTable::length() const { return engine().tableLength(false, handleIndex()); } Maybe LuaTable::getMetatable() const { return engine().tableGetMetatable(handleIndex()); } void LuaTable::setMetatable(LuaTable const& table) const { return engine().tableSetMetatable(handleIndex(), table); } LuaInt LuaTable::rawLength() const { return engine().tableLength(true, handleIndex()); } LuaCallbacks& LuaCallbacks::merge(LuaCallbacks const& callbacks) { try { for (auto const& pair : callbacks.m_callbacks) m_callbacks.add(pair.first, pair.second); } catch (MapException const& e) { throw LuaException(strf("Failed to merge LuaCallbacks: {}", outputException(e, true))); } return *this; } StringMap const& LuaCallbacks::callbacks() const { return m_callbacks; } bool LuaContext::containsPath(String path) const { return engine().contextGetPath(handleIndex(), std::move(path)) != LuaNil; } void LuaContext::load(char const* contents, size_t size, char const* name) { engine().contextLoad(handleIndex(), contents, size, name); } void LuaContext::load(String const& contents, String const& name) { load(contents.utf8Ptr(), contents.utf8Size(), name.utf8Ptr()); } void LuaContext::load(ByteArray const& contents, String const& name) { load(contents.ptr(), contents.size(), name.utf8Ptr()); } void LuaContext::setRequireFunction(RequireFunction requireFunction) { engine().setContextRequire(handleIndex(), std::move(requireFunction)); } void LuaContext::setCallbacks(String const& tableName, LuaCallbacks const& callbacks) const { auto& eng = engine(); if (LuaContext::contains(tableName)) return; auto callbackTable = eng.createTable(); for (auto const& p : callbacks.callbacks()) callbackTable.set(p.first, eng.createWrappedFunction(p.second)); LuaContext::set(tableName, callbackTable); } LuaString LuaContext::createString(String const& str) { return engine().createString(str); } LuaString LuaContext::createString(char const* str) { return engine().createString(str); } LuaTable LuaContext::createTable() { return engine().createTable(); } LuaNullEnforcer::LuaNullEnforcer(LuaEngine& engine) : m_engine(&engine) { ++m_engine->m_nullTerminated; }; LuaNullEnforcer::~LuaNullEnforcer() { --m_engine->m_nullTerminated; }; LuaValue LuaConverter::from(LuaEngine& engine, Json const& v) { if (v.isType(Json::Type::Null)) { return LuaNil; } else if (v.isType(Json::Type::Float)) { return LuaFloat(v.toDouble()); } else if (v.isType(Json::Type::Bool)) { return v.toBool(); } else if (v.isType(Json::Type::Int)) { return LuaInt(v.toInt()); } else if (v.isType(Json::Type::String)) { return engine.createString(*v.stringPtr()); } else { return LuaDetail::jsonContainerToTable(engine, v); } } Maybe LuaConverter::to(LuaEngine&, LuaValue const& v) { if (v == LuaNil) return Json(); if (auto b = v.ptr()) return Json(*b); if (auto i = v.ptr()) return Json(*i); if (auto f = v.ptr()) return Json(*f); if (auto s = v.ptr()) return Json(s->ptr()); if (v.is()) return LuaDetail::tableToJsonContainer(v.get()); return {}; } LuaValue LuaConverter::from(LuaEngine& engine, JsonObject v) { return engine.luaFrom(Json(std::move(v))); } Maybe LuaConverter::to(LuaEngine& engine, LuaValue v) { auto j = engine.luaTo(std::move(v)); if (j.type() == Json::Type::Object) { return j.toObject(); } else if (j.type() == Json::Type::Array) { auto list = j.arrayPtr(); if (list->empty()) return JsonObject(); } return {}; } LuaValue LuaConverter::from(LuaEngine& engine, JsonArray v) { return engine.luaFrom(Json(std::move(v))); } Maybe LuaConverter::to(LuaEngine& engine, LuaValue v) { auto j = engine.luaTo(std::move(v)); if (j.type() == Json::Type::Array) { return j.toArray(); } else if (j.type() == Json::Type::Object) { auto map = j.objectPtr(); if (map->empty()) return JsonArray(); } return {}; } LuaEnginePtr LuaEngine::create(bool safe) { LuaEnginePtr self(new LuaEngine); self->m_state = lua_newstate(allocate, nullptr); self->m_scriptDefaultEnvRegistryId = LUA_NOREF; self->m_wrappedFunctionMetatableRegistryId = LUA_NOREF; self->m_requireFunctionMetatableRegistryId = LUA_NOREF; self->m_instructionLimit = 0; self->m_profilingEnabled = false; self->m_instructionMeasureInterval = 1000; self->m_instructionCount = 0; self->m_recursionLevel = 0; self->m_recursionLimit = 0; self->m_nullTerminated = 0; if (!self->m_state) throw LuaException("Failed to initialize Lua"); lua_checkstack(self->m_state, 5); // Create handle stack thread and place it in the registry to prevent it from being garbage // collected. self->m_handleThread = lua_newthread(self->m_state); luaL_ref(self->m_state, LUA_REGISTRYINDEX); // We need 1 extra stack space to move values in and out of the handle stack. self->m_handleStackSize = LUA_MINSTACK - 1; self->m_handleStackMax = 0; // Set the extra space in the lua main state to the pointer to the main // LuaEngine *reinterpret_cast(lua_getextraspace(self->m_state)) = self.get(); // Create the common message handler function for pcall to print a better // message with a traceback lua_pushcfunction(self->m_state, [](lua_State* state) { // Don't modify the error if it is one of the special limit errrors if (lua_islightuserdata(state, 1)) { void* error = lua_touserdata(state, -1); if (error == &s_luaInstructionLimitExceptionKey || error == &s_luaRecursionLimitExceptionKey) return 1; } luaL_traceback(state, state, lua_tostring(state, 1), 0); lua_remove(state, 1); return 1; }); self->m_pcallTracebackMessageHandlerRegistryId = luaL_ref(self->m_state, LUA_REGISTRYINDEX); // Create the common metatable for wrapped functions lua_newtable(self->m_state); lua_pushcfunction(self->m_state, [](lua_State* state) { auto func = (LuaDetail::LuaWrappedFunction*)lua_touserdata(state, 1); func->~function(); return 0; }); LuaDetail::rawSetField(self->m_state, -2, "__gc"); lua_pushboolean(self->m_state, 0); LuaDetail::rawSetField(self->m_state, -2, "__metatable"); self->m_wrappedFunctionMetatableRegistryId = luaL_ref(self->m_state, LUA_REGISTRYINDEX); // Create the common metatable for require functions lua_newtable(self->m_state); lua_pushcfunction(self->m_state, [](lua_State* state) { auto func = (LuaContext::RequireFunction*)lua_touserdata(state, 1); func->~function(); return 0; }); LuaDetail::rawSetField(self->m_state, -2, "__gc"); lua_pushboolean(self->m_state, 0); LuaDetail::rawSetField(self->m_state, -2, "__metatable"); self->m_requireFunctionMetatableRegistryId = luaL_ref(self->m_state, LUA_REGISTRYINDEX); // Load all base libraries and prune them of unsafe functions luaL_requiref(self->m_state, "_ENV", luaopen_base, true); if (safe) { StringSet baseWhitelist = { "assert", "error", "getmetatable", "ipairs", "next", "pairs", "pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "select", "setmetatable", "tonumber", "tostring", "type", "unpack", "_VERSION", "xpcall"}; lua_pushnil(self->m_state); while (lua_next(self->m_state, -2) != 0) { lua_pop(self->m_state, 1); String key(lua_tostring(self->m_state, -1)); if (!baseWhitelist.contains(key)) { lua_pushvalue(self->m_state, -1); lua_pushnil(self->m_state); lua_rawset(self->m_state, -4); } } } lua_pop(self->m_state, 1); luaL_requiref(self->m_state, "os", luaopen_os, true); if (safe) { StringSet osWhitelist = {"clock", "difftime", "time"}; lua_pushnil(self->m_state); while (lua_next(self->m_state, -2) != 0) { lua_pop(self->m_state, 1); String key(lua_tostring(self->m_state, -1)); if (!osWhitelist.contains(key)) { lua_pushvalue(self->m_state, -1); lua_pushnil(self->m_state); lua_rawset(self->m_state, -4); } } } lua_pop(self->m_state, 1); // loads a lua base library, leaves it at the top of the stack auto loadBaseLibrary = [](lua_State* state, char const* modname, lua_CFunction openf) { luaL_requiref(state, modname, openf, true); // set __metatable metamethod to false // otherwise scripts can access and mutate the metatable, allowing passing values // between script contexts, breaking the sandbox lua_newtable(state); lua_pushliteral(state, "__metatable"); lua_pushboolean(state, 0); lua_rawset(state, -3); lua_setmetatable(state, -2); }; loadBaseLibrary(self->m_state, "coroutine", luaopen_coroutine); // replace coroutine resume with one that appends tracebacks lua_pushliteral(self->m_state, "resume"); lua_pushcfunction(self->m_state, &LuaEngine::coresumeWithTraceback); lua_rawset(self->m_state, -3); loadBaseLibrary(self->m_state, "math", luaopen_math); loadBaseLibrary(self->m_state, "string", luaopen_string); loadBaseLibrary(self->m_state, "table", luaopen_table); loadBaseLibrary(self->m_state, "utf8", luaopen_utf8); lua_pop(self->m_state, 5); if (!safe) { loadBaseLibrary(self->m_state, "io", luaopen_io); loadBaseLibrary(self->m_state, "package", luaopen_package); loadBaseLibrary(self->m_state, "debug", luaopen_debug); lua_pop(self->m_state, 3); } // Make a shallow copy of the default script environment and save it for // resetting the global state. lua_rawgeti(self->m_state, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); lua_newtable(self->m_state); LuaDetail::shallowCopy(self->m_state, -2, -1); self->m_scriptDefaultEnvRegistryId = luaL_ref(self->m_state, LUA_REGISTRYINDEX); lua_pop(self->m_state, 1); self->setGlobal("jarray", self->createFunction(&LuaDetail::jarray)); self->setGlobal("jobject", self->createFunction(&LuaDetail::jobject)); self->setGlobal("jremove", self->createFunction(&LuaDetail::jcontRemove)); self->setGlobal("jsize", self->createFunction(&LuaDetail::jcontSize)); self->setGlobal("jresize", self->createFunction(&LuaDetail::jcontResize)); self->setGlobal("shared", self->createTable()); return self; } LuaEngine::~LuaEngine() { // If we've had a stack space leak, this will not be zero starAssert(lua_gettop(m_state) == 0); lua_close(m_state); } void LuaEngine::setInstructionLimit(uint64_t instructionLimit) { if (instructionLimit != m_instructionLimit) { m_instructionLimit = instructionLimit; updateCountHook(); } } uint64_t LuaEngine::instructionLimit() const { return m_instructionLimit; } void LuaEngine::setProfilingEnabled(bool profilingEnabled) { if (profilingEnabled != m_profilingEnabled) { m_profilingEnabled = profilingEnabled; m_profileEntries.clear(); updateCountHook(); } } bool LuaEngine::profilingEnabled() const { return m_profilingEnabled; } List LuaEngine::getProfile() { List profileEntries; for (auto const& p : m_profileEntries) { profileEntries.append(*p.second); } return profileEntries; } void LuaEngine::setInstructionMeasureInterval(unsigned measureInterval) { if (measureInterval != m_instructionMeasureInterval) { m_instructionMeasureInterval = measureInterval; updateCountHook(); } } unsigned LuaEngine::instructionMeasureInterval() const { return m_instructionMeasureInterval; } void LuaEngine::setRecursionLimit(unsigned recursionLimit) { m_recursionLimit = recursionLimit; } unsigned LuaEngine::recursionLimit() const { return m_recursionLimit; } ByteArray LuaEngine::compile(char const* contents, size_t size, char const* name) { lua_checkstack(m_state, 1); handleError(m_state, luaL_loadbuffer(m_state, contents, size, name)); ByteArray compiledScript; lua_Writer writer = [](lua_State*, void const* data, size_t size, void* byteArrayPtr) -> int { ((ByteArray*)byteArrayPtr)->append((char const*)data, size); return 0; }; lua_dump(m_state, writer, &compiledScript, false); lua_pop(m_state, 1); return compiledScript; } ByteArray LuaEngine::compile(String const& contents, String const& name) { return compile(contents.utf8Ptr(), contents.utf8Size(), name.empty() ? nullptr : name.utf8Ptr()); } ByteArray LuaEngine::compile(ByteArray const& contents, String const& name) { return compile(contents.ptr(), contents.size(), name.empty() ? nullptr : name.utf8Ptr()); } LuaString LuaEngine::createString(String const& str) { lua_checkstack(m_state, 1); if (m_nullTerminated > 0) lua_pushstring(m_state, str.utf8Ptr()); else lua_pushlstring(m_state, str.utf8Ptr(), str.utf8Size()); return LuaString(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } LuaString LuaEngine::createString(char const* str) { lua_checkstack(m_state, 1); lua_pushstring(m_state, str); return LuaString(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } LuaTable LuaEngine::createTable(int narr, int nrec) { lua_checkstack(m_state, 1); lua_createtable(m_state, narr, nrec); return LuaTable(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } LuaThread LuaEngine::createThread() { lua_checkstack(m_state, 1); lua_newthread(m_state); return LuaThread(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } void LuaEngine::threadPushFunction(int threadIndex, int functionIndex) { lua_State* thread = lua_tothread(m_handleThread, threadIndex); int status = lua_status(thread); lua_Debug ar; if (status != LUA_OK || lua_getstack(thread, 0, &ar) > 0 || lua_gettop(thread) > 0) throw LuaException(strf("Cannot push function to active or errored thread with status {}", status)); pushHandle(thread, functionIndex); } LuaThread::Status LuaEngine::threadStatus(int handleIndex) { lua_State* thread = lua_tothread(m_handleThread, handleIndex); int status = lua_status(thread); if (status != LUA_OK && status != LUA_YIELD) return LuaThread::Status::Error; lua_Debug ar; if (status == LUA_YIELD || lua_getstack(thread, 0, &ar) > 0 || lua_gettop(thread) > 0) return LuaThread::Status::Active; return LuaThread::Status::Dead; } LuaContext LuaEngine::createContext() { lua_checkstack(m_state, 2); // Create a new blank environment and copy the default environment to it. lua_newtable(m_state); lua_rawgeti(m_state, LUA_REGISTRYINDEX, m_scriptDefaultEnvRegistryId); LuaDetail::shallowCopy(m_state, -1, -2); lua_pop(m_state, 1); // Then set that environment as the new context environment in the registry. return LuaContext(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } void LuaEngine::collectGarbage(Maybe steps) { for (auto handleIndex : take(m_handleFree)) { lua_pushnil(m_handleThread); lua_replace(m_handleThread, handleIndex); } if (steps) lua_gc(m_state, LUA_GCSTEP, *steps); else lua_gc(m_state, LUA_GCCOLLECT, 0); } void LuaEngine::setAutoGarbageCollection(bool autoGarbageColleciton) { lua_gc(m_state, LUA_GCSTOP, autoGarbageColleciton ? 1 : 0); } void LuaEngine::tuneAutoGarbageCollection(float pause, float stepMultiplier) { lua_gc(m_state, LUA_GCSETPAUSE, round(pause * 100)); lua_gc(m_state, LUA_GCSETSTEPMUL, round(stepMultiplier * 100)); } size_t LuaEngine::memoryUsage() const { return (size_t)lua_gc(m_state, LUA_GCCOUNT, 0) * 1024 + lua_gc(m_state, LUA_GCCOUNTB, 0); } LuaNullEnforcer LuaEngine::nullTerminate() { return LuaNullEnforcer(*this); } void LuaEngine::setNullTerminated(bool nullTerminated) { m_nullTerminated = nullTerminated ? 0 : INT_MIN; } LuaEngine* LuaEngine::luaEnginePtr(lua_State* state) { return (*reinterpret_cast(lua_getextraspace(state))); } void LuaEngine::countHook(lua_State* state, lua_Debug* ar) { starAssert(ar->event == LUA_HOOKCOUNT); lua_checkstack(state, 4); auto self = luaEnginePtr(state); // If the instruction count is 0, that means in this sequence of calls, // we have not hit a debug hook yet. Since we don't know the state of // the internal lua instruction counter at the start, we don't know how // many instructions have been executed, only that it is >= 1 and <= // m_instructionMeasureInterval, so we pick the low estimate. if (self->m_instructionCount == 0) self->m_instructionCount = 1; else self->m_instructionCount += self->m_instructionMeasureInterval; if (self->m_instructionLimit != 0 && self->m_instructionCount > self->m_instructionLimit) { lua_pushlightuserdata(state, &s_luaInstructionLimitExceptionKey); lua_error(state); } if (self->m_profilingEnabled) { // find bottom of the stack // ar will contain the stack info from the last call that returns 1 int stackLevel = -1; while (lua_getstack(state, stackLevel + 1, ar) == 1) stackLevel++; shared_ptr parentEntry = nullptr; while (true) { // Get the 'n' name info and 'S' source info if (lua_getinfo(state, "nS", ar) == 0) break; auto key = make_tuple(String(ar->short_src), (unsigned)ar->linedefined); auto& entryMap = parentEntry ? parentEntry->calls : self->m_profileEntries; if (!entryMap.contains(key)) { auto e = make_shared(); e->source = ar->short_src; e->sourceLine = ar->linedefined; entryMap.set(key, e); } auto entry = entryMap.get(key); // metadata if (!entry->name && ar->name) entry->name = String(ar->name); if (!entry->nameScope && String() != ar->namewhat) entry->nameScope = String(ar->namewhat); // Only add timeTaken to the self time for the function we are actually // in, not parent functions if (stackLevel == 0) { entry->totalTime += 1; entry->selfTime += 1; } else { entry->totalTime += 1; } parentEntry = entry; if (lua_getstack(state, --stackLevel, ar) == 0) break; } } } void* LuaEngine::allocate(void*, void* ptr, size_t oldSize, size_t newSize) { if (newSize == 0) { Star::free(ptr, oldSize); return nullptr; } else { return Star::realloc(ptr, newSize); } } void LuaEngine::handleError(lua_State* state, int res) { if (res != LUA_OK) { if (lua_islightuserdata(state, -1)) { void* error = lua_touserdata(state, -1); if (error == &s_luaInstructionLimitExceptionKey) { lua_pop(state, 1); throw LuaInstructionLimitReached(); } if (error == &s_luaRecursionLimitExceptionKey) { lua_pop(state, 1); throw LuaRecursionLimitReached(); } } String error; if (lua_isstring(state, -1)) error = strf("Error code {}, {}", res, lua_tostring(state, -1)); else error = strf("Error code {}, ", res); lua_pop(state, 1); // This seems terrible, but as far as I can tell, this is exactly what the // stock lua repl does. if (error.endsWith("")) throw LuaIncompleteStatementException(error.takeUtf8()); else throw LuaException(error.takeUtf8()); } } int LuaEngine::pcallWithTraceback(lua_State* state, int nargs, int nresults) { int msghPosition = lua_gettop(state) - nargs; lua_rawgeti(m_state, LUA_REGISTRYINDEX, m_pcallTracebackMessageHandlerRegistryId); lua_insert(state, msghPosition); int ret = lua_pcall(state, nargs, nresults, msghPosition); lua_remove(state, msghPosition); return ret; } int LuaEngine::coresumeWithTraceback(lua_State* state) { lua_State* co = lua_tothread(state, 1); if (!co) { lua_checkstack(state, 2); lua_pushboolean(state, 0); lua_pushliteral(state, "bad argument #1 to 'resume' (thread expected)"); return 2; } int args = lua_gettop(state) - 1; lua_checkstack(co, args); if (lua_status(co) == LUA_OK && lua_gettop(co) == 0) { lua_checkstack(state, 2); lua_pushboolean(state, 0); lua_pushliteral(state, "cannot resume dead coroutine"); return 2; } lua_xmove(state, co, args); int status = lua_resume(co, state, args); if (status == LUA_OK || status == LUA_YIELD) { int res = lua_gettop(co); lua_checkstack(state, res + 1); lua_pushboolean(state, 1); lua_xmove(co, state, res); return res + 1; } else { lua_checkstack(state, 2); lua_pushboolean(state, 0); propagateErrorWithTraceback(co, state); return 2; } } void LuaEngine::propagateErrorWithTraceback(lua_State* from, lua_State* to) { if (const char* error = lua_tostring(from, -1)) { luaL_traceback(to, from, error, 0); // error + traceback lua_pop(from, 1); } else { lua_xmove(from, to, 1); // just error, no traceback } } char const* LuaEngine::stringPtr(int handleIndex) { return lua_tostring(m_handleThread, handleIndex); } size_t LuaEngine::stringLength(int handleIndex) { if (m_nullTerminated > 0) return strlen(lua_tostring(m_handleThread, handleIndex)); else { size_t len = 0; lua_tolstring(m_handleThread, handleIndex, &len); return len; } } String LuaEngine::string(int handleIndex) { if (m_nullTerminated > 0) return String(lua_tostring(m_handleThread, handleIndex)); else { size_t len = 0; const char* data = lua_tolstring(m_handleThread, handleIndex, &len); return String(data, len); } } StringView LuaEngine::stringView(int handleIndex) { if (m_nullTerminated > 0) return StringView(lua_tostring(m_handleThread, handleIndex)); else { size_t len = 0; const char* data = lua_tolstring(m_handleThread, handleIndex, &len); return StringView(data, len); } } LuaValue LuaEngine::tableGet(bool raw, int handleIndex, LuaValue const& key) { lua_checkstack(m_state, 1); pushHandle(m_state, handleIndex); pushLuaValue(m_state, key); if (raw) lua_rawget(m_state, -2); else lua_gettable(m_state, -2); LuaValue v = popLuaValue(m_state); lua_pop(m_state, 1); return v; } LuaValue LuaEngine::tableGet(bool raw, int handleIndex, char const* key) { lua_checkstack(m_state, 1); pushHandle(m_state, handleIndex); if (raw) LuaDetail::rawGetField(m_state, -1, key); else lua_getfield(m_state, -1, key); lua_remove(m_state, -2); return popLuaValue(m_state); } void LuaEngine::tableSet(bool raw, int handleIndex, LuaValue const& key, LuaValue const& value) { lua_checkstack(m_state, 1); pushHandle(m_state, handleIndex); pushLuaValue(m_state, key); pushLuaValue(m_state, value); if (raw) lua_rawset(m_state, -3); else lua_settable(m_state, -3); lua_pop(m_state, 1); } void LuaEngine::tableSet(bool raw, int handleIndex, char const* key, LuaValue const& value) { lua_checkstack(m_state, 1); pushHandle(m_state, handleIndex); pushLuaValue(m_state, value); if (raw) LuaDetail::rawSetField(m_state, -2, key); else lua_setfield(m_state, -2, key); lua_pop(m_state, 1); } LuaInt LuaEngine::tableLength(bool raw, int handleIndex) { if (raw) { return lua_rawlen(m_handleThread, handleIndex); } else { lua_checkstack(m_state, 1); pushHandle(m_state, handleIndex); lua_len(m_state, -1); LuaInt len = lua_tointeger(m_state, -1); lua_pop(m_state, 2); return len; } } void LuaEngine::tableIterate(int handleIndex, function iterator) { lua_checkstack(m_state, 4); pushHandle(m_state, handleIndex); lua_pushnil(m_state); while (lua_next(m_state, -2) != 0) { lua_pushvalue(m_state, -2); LuaValue key = popLuaValue(m_state); LuaValue value = popLuaValue(m_state); bool cont = false; try { cont = iterator(std::move(key), std::move(value)); } catch (...) { lua_pop(m_state, 2); throw; } if (!cont) { lua_pop(m_state, 1); break; } } lua_pop(m_state, 1); } Maybe LuaEngine::tableGetMetatable(int handleIndex) { lua_checkstack(m_state, 2); pushHandle(m_state, handleIndex); if (lua_getmetatable(m_state, -1) == 0) { lua_pop(m_state, 1); return {}; } LuaTable table = popLuaValue(m_state).get(); lua_pop(m_state, 1); return table; } void LuaEngine::tableSetMetatable(int handleIndex, LuaTable const& table) { lua_checkstack(m_state, 2); pushHandle(m_state, handleIndex); pushHandle(m_state, table.handleIndex()); lua_setmetatable(m_state, -2); lua_pop(m_state, 1); } void LuaEngine::setContextRequire(int handleIndex, LuaContext::RequireFunction requireFunction) { lua_checkstack(m_state, 4); pushHandle(m_state, handleIndex); auto funcUserdata = (LuaContext::RequireFunction*)lua_newuserdata(m_state, sizeof(LuaContext::RequireFunction)); new (funcUserdata) LuaContext::RequireFunction(std::move(requireFunction)); lua_rawgeti(m_state, LUA_REGISTRYINDEX, m_requireFunctionMetatableRegistryId); lua_setmetatable(m_state, -2); lua_pushvalue(m_state, -2); auto invokeRequire = [](lua_State* state) { try { lua_checkstack(state, 2); auto require = (LuaContext::RequireFunction*)lua_touserdata(state, lua_upvalueindex(1)); auto self = luaEnginePtr(state); auto moduleName = self->luaTo(self->popLuaValue(state)); lua_pushvalue(state, lua_upvalueindex(2)); LuaContext context(LuaDetail::LuaHandle(RefPtr(self), self->popHandle(state))); (*require)(context, moduleName); return 0; } catch (LuaInstructionLimitReached const&) { lua_pushlightuserdata(state, &s_luaInstructionLimitExceptionKey); return lua_error(state); } catch (LuaRecursionLimitReached const&) { lua_pushlightuserdata(state, &s_luaRecursionLimitExceptionKey); return lua_error(state); } catch (std::exception const& e) { luaL_where(state, 1); lua_pushstring(state, printException(e, true).c_str()); lua_concat(state, 2); return lua_error(state); } }; lua_pushcclosure(m_state, invokeRequire, 2); LuaDetail::rawSetField(m_state, -2, "require"); lua_pop(m_state, 1); } void LuaEngine::contextLoad(int handleIndex, char const* contents, size_t size, char const* name) { lua_checkstack(m_state, 2); // First load the script... handleError(m_state, luaL_loadbuffer(m_state, contents, size, name)); // Then set the _ENV upvalue for the newly loaded chunk to our context env so // we load the scripts into the right environment. pushHandle(m_state, handleIndex); lua_setupvalue(m_state, -2, 1); incrementRecursionLevel(); int res = pcallWithTraceback(m_state, 0, 0); decrementRecursionLevel(); handleError(m_state, res); } LuaDetail::LuaFunctionReturn LuaEngine::contextEval(int handleIndex, String const& lua) { int stackSize = lua_gettop(m_state); lua_checkstack(m_state, 2); // First, try interpreting the lua as an expression by adding "return", then // as a statement. This is the same thing the actual lua repl does. int loadRes = luaL_loadstring(m_state, ("return " + lua).utf8Ptr()); if (loadRes == LUA_ERRSYNTAX) { lua_pop(m_state, 1); loadRes = luaL_loadstring(m_state, lua.utf8Ptr()); } handleError(m_state, loadRes); pushHandle(m_state, handleIndex); lua_setupvalue(m_state, -2, 1); incrementRecursionLevel(); int callRes = pcallWithTraceback(m_state, 0, LUA_MULTRET); decrementRecursionLevel(); handleError(m_state, callRes); int returnValues = lua_gettop(m_state) - stackSize; if (returnValues == 0) { return LuaDetail::LuaFunctionReturn(); } else if (returnValues == 1) { return LuaDetail::LuaFunctionReturn(popLuaValue(m_state)); } else { LuaVariadic ret(returnValues); for (int i = returnValues - 1; i >= 0; --i) ret[i] = popLuaValue(m_state); return LuaDetail::LuaFunctionReturn(ret); } } LuaValue LuaEngine::contextGetPath(int handleIndex, String path) { lua_checkstack(m_state, 2); pushHandle(m_state, handleIndex); std::string utf8Path = path.takeUtf8(); char* utf8Ptr = &utf8Path[0]; size_t utf8Size = utf8Path.size(); size_t subPathStart = 0; for (size_t i = 0; i < utf8Size; ++i) { if (utf8Path[i] == '.') { utf8Path[i] = '\0'; lua_getfield(m_state, -1, utf8Ptr + subPathStart); lua_remove(m_state, -2); if (lua_type(m_state, -1) != LUA_TTABLE) { lua_pop(m_state, 1); return LuaNil; } subPathStart = i + 1; } } lua_getfield(m_state, -1, utf8Ptr + subPathStart); lua_remove(m_state, -2); return popLuaValue(m_state); } void LuaEngine::contextSetPath(int handleIndex, String path, LuaValue const& value) { lua_checkstack(m_state, 3); pushHandle(m_state, handleIndex); std::string utf8Path = path.takeUtf8(); char* utf8Ptr = &utf8Path[0]; size_t utf8Size = utf8Path.size(); size_t subPathStart = 0; for (size_t i = 0; i < utf8Size; ++i) { if (utf8Path[i] == '.') { utf8Path[i] = '\0'; int type = lua_getfield(m_state, -1, utf8Ptr + subPathStart); if (type == LUA_TNIL) { lua_pop(m_state, 1); lua_newtable(m_state); lua_pushvalue(m_state, -1); lua_setfield(m_state, -3, utf8Ptr + subPathStart); lua_remove(m_state, -2); } else if (type == LUA_TTABLE) { lua_remove(m_state, -2); } else { lua_pop(m_state, 2); throw LuaException("Sub-path in setPath is not nil and is not a table"); } subPathStart = i + 1; } } pushLuaValue(m_state, value); lua_setfield(m_state, -2, utf8Ptr + subPathStart); lua_pop(m_state, 1); } int LuaEngine::popHandle(lua_State* state) { lua_xmove(state, m_handleThread, 1); return placeHandle(); } void LuaEngine::pushHandle(lua_State* state, int handleIndex) { lua_pushvalue(m_handleThread, handleIndex); lua_xmove(m_handleThread, state, 1); } int LuaEngine::copyHandle(int handleIndex) { lua_pushvalue(m_handleThread, handleIndex); return placeHandle(); } int LuaEngine::placeHandle() { if (auto free = m_handleFree.maybeTakeLast()) { lua_replace(m_handleThread, *free); return *free; } else { if (m_handleStackMax >= m_handleStackSize) { if (!lua_checkstack(m_handleThread, m_handleStackSize)) { throw LuaException("Exhausted the size of the handle thread stack"); } m_handleStackSize *= 2; } m_handleStackMax += 1; return m_handleStackMax; } } LuaFunction LuaEngine::createWrappedFunction(LuaDetail::LuaWrappedFunction function) { lua_checkstack(m_state, 2); auto funcUserdata = (LuaDetail::LuaWrappedFunction*)lua_newuserdata(m_state, sizeof(LuaDetail::LuaWrappedFunction)); new (funcUserdata) LuaDetail::LuaWrappedFunction(std::move(function)); lua_rawgeti(m_state, LUA_REGISTRYINDEX, m_wrappedFunctionMetatableRegistryId); lua_setmetatable(m_state, -2); auto invokeFunction = [](lua_State* state) { auto func = (LuaDetail::LuaWrappedFunction*)lua_touserdata(state, lua_upvalueindex(1)); auto self = luaEnginePtr(state); int argumentCount = lua_gettop(state); try { // For speed, if the argument count is less than some pre-defined // value, use a stack array. int const MaxArrayArgs = 8; LuaDetail::LuaFunctionReturn res; if (argumentCount <= MaxArrayArgs) { Array args; for (int i = argumentCount - 1; i >= 0; --i) args[i] = self->popLuaValue(state); res = (*func)(*self, argumentCount, args.ptr()); } else { List args(argumentCount); for (int i = argumentCount - 1; i >= 0; --i) args[i] = self->popLuaValue(state); res = (*func)(*self, argumentCount, args.ptr()); } if (auto val = res.ptr()) { self->pushLuaValue(state, *val); return 1; } else if (auto vec = res.ptr>()) { for (auto const& r : *vec) self->pushLuaValue(state, r); return (int)vec->size(); } else { return 0; } } catch (LuaInstructionLimitReached const&) { lua_pushlightuserdata(state, &s_luaInstructionLimitExceptionKey); return lua_error(state); } catch (LuaRecursionLimitReached const&) { lua_pushlightuserdata(state, &s_luaRecursionLimitExceptionKey); return lua_error(state); } catch (std::exception const& e) { luaL_where(state, 1); lua_pushstring(state, printException(e, true).c_str()); lua_concat(state, 2); return lua_error(state); } }; lua_pushcclosure(m_state, invokeFunction, 1); return LuaFunction(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } LuaFunction LuaEngine::createRawFunction(lua_CFunction function) { lua_checkstack(m_state, 2); lua_pushcfunction(m_state, function); return LuaFunction(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } LuaFunction LuaEngine::createFunctionFromSource(int handleIndex, char const* contents, size_t size, char const* name) { lua_checkstack(m_state, 2); handleError(m_state, luaL_loadbufferx(m_state, contents, size, name, "t")); pushHandle(m_state, handleIndex); lua_setupvalue(m_state, -2, 1); return LuaFunction(LuaDetail::LuaHandle(RefPtr(this), popHandle(m_state))); } void LuaEngine::pushLuaValue(lua_State* state, LuaValue const& luaValue) { lua_checkstack(state, 1); struct Pusher { LuaEngine* engine; lua_State* state; void operator()(LuaNilType const&) { lua_pushnil(state); } void operator()(LuaBoolean const& b) { lua_pushboolean(state, b); } void operator()(LuaInt const& i) { lua_pushinteger(state, i); } void operator()(LuaFloat const& f) { lua_pushnumber(state, f); } void operator()(LuaReference const& ref) { if (&ref.engine() != engine) throw LuaException("lua reference values cannot be shared between engines"); engine->pushHandle(state, ref.handleIndex()); } }; luaValue.call(Pusher{this, state}); } LuaValue LuaEngine::popLuaValue(lua_State* state) { lua_checkstack(state, 1); LuaValue result; starAssert(!lua_isnone(state, -1)); switch (lua_type(state, -1)) { case LUA_TNIL: { lua_pop(state, 1); break; } case LUA_TBOOLEAN: { result = lua_toboolean(state, -1) != 0; lua_pop(state, 1); break; } case LUA_TNUMBER: { if (lua_isinteger(state, -1)) { result = lua_tointeger(state, -1); lua_pop(state, 1); } else { result = lua_tonumber(state, -1); lua_pop(state, 1); } break; } case LUA_TSTRING: { result = LuaString(LuaDetail::LuaHandle(RefPtr(this), popHandle(state))); break; } case LUA_TTABLE: { result = LuaTable(LuaDetail::LuaHandle(RefPtr(this), popHandle(state))); break; } case LUA_TFUNCTION: { result = LuaFunction(LuaDetail::LuaHandle(RefPtr(this), popHandle(state))); break; } case LUA_TTHREAD: { result = LuaThread(LuaDetail::LuaHandle(RefPtr(this), popHandle(state))); break; } case LUA_TUSERDATA: { if (lua_getmetatable(state, -1) == 0) { lua_pop(state, 1); throw LuaException("Userdata in popLuaValue missing metatable"); } lua_pop(state, 1); result = LuaUserData(LuaDetail::LuaHandle(RefPtr(this), popHandle(state))); break; } default: { lua_pop(state, 1); throw LuaException("Unsupported type in popLuaValue"); } } return result; } void LuaEngine::incrementRecursionLevel() { // We reset the instruction count and profiling timing only on the *top // level* function entrance, not on recursive entrances. if (m_recursionLevel == 0) { m_instructionCount = 0; } if (m_recursionLimit != 0 && m_recursionLevel == m_recursionLimit) throw LuaRecursionLimitReached(); ++m_recursionLevel; } void LuaEngine::decrementRecursionLevel() { starAssert(m_recursionLevel != 0); --m_recursionLevel; } void LuaEngine::updateCountHook() { if (m_instructionLimit || m_profilingEnabled) lua_sethook(m_state, &LuaEngine::countHook, LUA_MASKCOUNT, m_instructionMeasureInterval); else lua_sethook(m_state, &LuaEngine::countHook, 0, 0); } int LuaEngine::s_luaInstructionLimitExceptionKey = 0; int LuaEngine::s_luaRecursionLimitExceptionKey = 0; void LuaDetail::rawSetField(lua_State* state, int index, char const* key) { lua_checkstack(state, 1); int absTableIndex = lua_absindex(state, index); lua_pushstring(state, key); // Move the newly pushed key to the secont to top spot, leaving the value in // the top spot. lua_insert(state, -2); // Pops the value and the key lua_rawset(state, absTableIndex); } void LuaDetail::rawGetField(lua_State* state, int index, char const* key) { lua_checkstack(state, 2); int absTableIndex = lua_absindex(state, index); lua_pushstring(state, key); // Pops the key lua_rawget(state, absTableIndex); } void LuaDetail::shallowCopy(lua_State* state, int sourceIndex, int targetIndex) { lua_checkstack(state, 3); int absSourceIndex = lua_absindex(state, sourceIndex); int absTargetIndex = lua_absindex(state, targetIndex); lua_pushnil(state); while (lua_next(state, absSourceIndex) != 0) { lua_pushvalue(state, -2); lua_insert(state, -2); lua_rawset(state, absTargetIndex); } } LuaTable LuaDetail::insertJsonMetatable(LuaEngine& engine, LuaTable const& table, Json::Type type) { auto newIndexMetaMethod = [](LuaTable const& table, LuaValue const& key, LuaValue const& value) { auto mt = table.getMetatable(); auto nils = mt->rawGet("__nils"); // If we are setting an entry to nil, need to add a bogus integer entry // to the __nils table, otherwise need to set the entry *in* the __nils // table to nil and remove it. if (value == LuaNil) nils.rawSet(key, 0); else nils.rawSet(key, LuaNil); table.rawSet(key, value); }; auto mt = engine.createTable(); auto nils = engine.createTable(); mt.rawSet("__nils", nils); mt.rawSet("__newindex", engine.createFunction(newIndexMetaMethod)); mt.rawSet("__typehint", type == Json::Type::Array ? 1 : 2); return nils; } LuaTable LuaDetail::jsonContainerToTable(LuaEngine& engine, Json const& container) { if (!container.isType(Json::Type::Array) && !container.isType(Json::Type::Object)) throw LuaException("jsonContainerToTable called on improper json type"); auto table = engine.createTable(); auto nils = insertJsonMetatable(engine, table, container.type()); if (container.isType(Json::Type::Array)) { auto vlist = container.arrayPtr(); for (size_t i = 0; i < vlist->size(); ++i) { auto const& val = (*vlist)[i]; if (val) table.rawSet(i + 1, val); else nils.rawSet(i + 1, 0); } } else { for (auto const& pair : *container.objectPtr()) { if (pair.second) table.rawSet(pair.first, pair.second); else nils.rawSet(pair.first, 0); } } return table; } Maybe LuaDetail::tableToJsonContainer(LuaTable const& table) { JsonObject stringEntries; Map intEntries; int typeHint = 0; if (auto mt = table.getMetatable()) { if (auto th = mt->get>("__typehint")) typeHint = *th; if (auto nils = mt->get>("__nils")) { bool failedConversion = false; // Nil entries just have a garbage integer as their value nils->iterate([&](LuaValue const& key, LuaValue const&) { if (auto i = asInteger(key)) { intEntries[*i] = Json(); } else { if (auto str = table.engine().luaMaybeTo(key)) { stringEntries[str.take()] = Json(); } else { failedConversion = true; return false; } } return true; }); if (failedConversion) return {}; } } bool failedConversion = false; table.iterate([&](LuaValue key, LuaValue value) { auto jsonValue = table.engine().luaMaybeTo(value); if (!jsonValue) { failedConversion = true; return false; } if (auto i = asInteger(key)) { intEntries[*i] = jsonValue.take(); } else { auto stringKey = table.engine().luaMaybeTo(std::move(key)); if (!stringKey) { failedConversion = true; return false; } stringEntries[stringKey.take()] = jsonValue.take(); } return true; }); if (failedConversion) return {}; bool interpretAsList = stringEntries.empty() && (typeHint == 1 || (typeHint != 2 && !intEntries.empty() && prev(intEntries.end())->first == intEntries.size())); if (interpretAsList) { JsonArray list; for (auto& p : intEntries) list.set(p.first - 1, std::move(p.second)); return Json(std::move(list)); } else { for (auto& p : intEntries) stringEntries[toString(p.first)] = std::move(p.second); return Json(std::move(stringEntries)); } } Json LuaDetail::jarrayCreate() { return JsonArray(); } Json LuaDetail::jobjectCreate() { return JsonObject(); } LuaTable LuaDetail::jarray(LuaEngine& engine, Maybe table) { if (auto t = table.ptr()) { insertJsonMetatable(engine, *t, Json::Type::Array); return *t; } else { return jsonContainerToTable(engine, JsonArray()); } } LuaTable LuaDetail::jobject(LuaEngine& engine, Maybe table) { if (auto t = table.ptr()) { insertJsonMetatable(engine, *t, Json::Type::Object); return *t; } else { return jsonContainerToTable(engine, JsonObject()); } } void LuaDetail::jcontRemove(LuaTable const& table, LuaValue const& key) { if (auto mt = table.getMetatable()) { if (auto nils = mt->rawGet>("__nils")) nils->rawSet(key, LuaNil); } table.rawSet(key, LuaNil); } size_t LuaDetail::jcontSize(LuaTable const& table) { size_t elemCount = 0; size_t highestIndex = 0; bool hintList = false; if (auto mt = table.getMetatable()) { if (mt->rawGet>("__typehint") == 1) hintList = true; if (auto nils = mt->rawGet>("__nils")) { nils->iterate([&](LuaValue const& key, LuaValue const&) { auto i = asInteger(key); if (i && *i >= 0) highestIndex = max(*i, highestIndex); else hintList = false; ++elemCount; }); } } table.iterate([&](LuaValue const& key, LuaValue const&) { auto i = asInteger(key); if (i && *i >= 0) highestIndex = max(*i, highestIndex); else hintList = false; ++elemCount; }); if (hintList) return highestIndex; else return elemCount; } void LuaDetail::jcontResize(LuaTable const& table, size_t targetSize) { if (auto mt = table.getMetatable()) { if (auto nils = mt->rawGet>("__nils")) { nils->iterate([&](LuaValue const& key, LuaValue const&) { auto i = asInteger(key); if (i && *i > 0 && (size_t)*i > targetSize) nils->rawSet(key, LuaNil); }); } } table.iterate([&](LuaValue const& key, LuaValue const&) { auto i = asInteger(key); if (i && *i > 0 && (size_t)*i > targetSize) table.rawSet(key, LuaNil); }); table.set(targetSize, table.get(targetSize)); } Maybe LuaDetail::asInteger(LuaValue const& v) { if (v.is()) return v.get(); if (v.is()) { auto f = v.get(); if ((LuaFloat)(LuaInt)f == f) return (LuaInt)f; return {}; } /*// Kae: This prevents 1-1 conversion between Lua and Star::Json. if (v.is()) return maybeLexicalCast(v.get().ptr()); //*/ return {}; } }