diff --git a/.clang-format b/.clang-format index aa9f9e46a7..6304726d6e 100644 --- a/.clang-format +++ b/.clang-format @@ -3,13 +3,13 @@ Language: Cpp Standard: Cpp11 # BasedOnStyle: LLVM AccessModifierOffset: -2 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: DontAlign AlignConsecutiveAssignments: true AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: true AlignTrailingComments: true -AllowAllParametersOfDeclarationOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false AllowAllArgumentsOnNextLine: false AllowShortBlocksOnASingleLine: Never AllowShortCaseLabelsOnASingleLine: false diff --git a/code/scripting/ade_args.cpp b/code/scripting/ade_args.cpp index a0019df4a8..ba9b8b3b4b 100644 --- a/code/scripting/ade_args.cpp +++ b/code/scripting/ade_args.cpp @@ -1,12 +1,13 @@ #include "scripting/ade_args.h" -#include "scripting/ade.h" -#include "scripting/lua/LuaTable.h" -#include "scripting/lua/LuaFunction.h" -#include "scripting/lua/LuaConvert.h" +#include "scripting.h" #include "mod_table/mod_table.h" +#include "scripting/ade.h" +#include "scripting/lua/LuaConvert.h" +#include "scripting/lua/LuaFunction.h" +#include "scripting/lua/LuaTable.h" #include @@ -113,20 +114,20 @@ void set_single_arg(lua_State* L, char fmt, luacpp::LuaTable* table) { set_single_arg(L, fmt, *table); } -void set_single_arg(lua_State*, char fmt, const luacpp::LuaTable& table) +void set_single_arg(lua_State* L, char fmt, const luacpp::LuaTable& table) { Assertion(fmt == 't', "Invalid format character '%c' for table type!", fmt); - table.pushValue(); + table.pushValue(L); } void set_single_arg(lua_State* L, char fmt, luacpp::LuaFunction* func) { set_single_arg(L, fmt, *func); } -void set_single_arg(lua_State*, char fmt, const luacpp::LuaFunction& func) +void set_single_arg(lua_State* L, char fmt, const luacpp::LuaFunction& func) { Assertion(fmt == 'u', "Invalid format character '%c' for function type!", fmt); - func.pushValue(); + func.pushValue(L); } } // namespace internal diff --git a/code/scripting/api/LuaCoroutineRunner.cpp b/code/scripting/api/LuaCoroutineRunner.cpp new file mode 100644 index 0000000000..91c5b4ffa5 --- /dev/null +++ b/code/scripting/api/LuaCoroutineRunner.cpp @@ -0,0 +1,82 @@ +#include "LuaCoroutineRunner.h" + +#include "scripting/ade_args.h" +#include "scripting/api/objs/promise.h" + +namespace scripting { +namespace api { + +namespace { + +/** + * @brief A run context which resumes a coroutine until it is finished + * + * This will manage the passed coroutine and resume it until it is completed. For every yielded promise it registers + * itself as the continuation which then resumes the coroutine. + */ +class run_resolve_context : public resolve_context, public std::enable_shared_from_this { + public: + run_resolve_context(luacpp::LuaThread coroutine) : _coroutine(std::move(coroutine)) {} + + ~run_resolve_context() override = default; + void setResolver(Resolver resolver) override + { + m_resolver = std::move(resolver); + + // Kick off the coroutine once we know that someone cares about its result + resumeCoroutine(luacpp::LuaValueList()); + } + + private: + void resumeCoroutine(const luacpp::LuaValueList& resumeParams) + { + const auto result = _coroutine.resume(resumeParams); + + if (result.completed) { + // Thread is finished! We can call our resolver + m_resolver(result.returnVals); + return; + } + + // The coroutine suspended so the return value must be a promise + Assertion(result.returnVals.size() == 1, + "Wrong number of yielded values. Should be 1 but is " SIZE_T_ARG, + result.returnVals.size()); + + auto promiseStackStart = lua_gettop(_coroutine.getLuaState()); + result.returnVals.front().pushValue(_coroutine.getLuaState()); + + internal::Ade_get_args_skip = promiseStackStart; + LuaPromise* promise = nullptr; + if (!ade_get_args(_coroutine.getLuaState(), "o", l_Promise.GetPtr(&promise))) { + internal::Ade_get_args_skip = 0; + LuaError(_coroutine.getLuaState(), + "Failed to get promise after coroutine yielded. Make sure you only use async.await in async " + "coroutines."); + return; + } + internal::Ade_get_args_skip = 0; + + // Register ourself to be called when the promise resolves so that we can resume our coroutine + auto self = shared_from_this(); + promise->then([this, self](const luacpp::LuaValueList& resolveVals) { + // Since "self" keeps "this" alive it is safe to access that here + resumeCoroutine(resolveVals); + + return luacpp::LuaValueList(); + }); + } + + Resolver m_resolver; + luacpp::LuaThread _coroutine; +}; + +} // namespace + +LuaPromise runAsyncCoroutine(luacpp::LuaThread luaThread) +{ + return LuaPromise(std::make_shared(std::move(luaThread))); +} + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/LuaCoroutineRunner.h b/code/scripting/api/LuaCoroutineRunner.h new file mode 100644 index 0000000000..a75eeeaadd --- /dev/null +++ b/code/scripting/api/LuaCoroutineRunner.h @@ -0,0 +1,22 @@ +#pragma once + +#include "LuaPromise.h" + +#include "scripting/lua/LuaThread.h" + +namespace scripting { +namespace api { + +/** + * @brief Runs an asynchronous Lua thread and runs it to completion + * + * This takes the specified lua thread and resumes it until it is completed. Every yielded value must be a LuaPromise + * which suspends the coroutine until that promise resolves. + * + * @param luaThread The lua coroutine to run + * @return A LuaPromise that will resolve with the value returned when the coroutine completes + */ +LuaPromise runAsyncCoroutine(luacpp::LuaThread luaThread); + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/LuaPromise.cpp b/code/scripting/api/LuaPromise.cpp new file mode 100644 index 0000000000..7e76f9a9e4 --- /dev/null +++ b/code/scripting/api/LuaPromise.cpp @@ -0,0 +1,120 @@ +#include "LuaPromise.h" + +#include "globalincs/pstypes.h" + +#include "scripting/lua/LuaValue.h" + +namespace scripting { +namespace api { + +enum class State { + Invalid, + Pending, + Resolved, +}; + +resolve_context::~resolve_context() = default; + +class continuation_resolve_context : public resolve_context { + public: + continuation_resolve_context(LuaPromise::ContinuationFunction continuation) : _continuation(std::move(continuation)) + { + } + ~continuation_resolve_context() override = default; + + void setResolver(Resolver resolver) override { _resolver = std::move(resolver); } + + void resolve(const luacpp::LuaValueList& resolveVals) { _resolver(_continuation(resolveVals)); } + + private: + LuaPromise::ContinuationFunction _continuation; + Resolver _resolver; +}; + +struct LuaPromise::internal_state : std::enable_shared_from_this { + State state = State::Invalid; + + luacpp::LuaValueList resolvedValue; + + std::shared_ptr resolveCtx; + + SCP_vector> continuationContexts; + + void registerResolveCallback() + { + // Need a weak pointer here to avoid circular dependency. Lua will keep this object alive as long as needed + std::weak_ptr weak_self = shared_from_this(); + resolveCtx->setResolver([this, weak_self](const luacpp::LuaValueList& resolveVals) { + if (auto _ = weak_self.lock()) { + // Now we know that "this" is still valid + resolveCb(resolveVals); + } + }); + } + + void resolveCb(const luacpp::LuaValueList& resolveVals) + { + resolvedValue = resolveVals; + state = State::Resolved; + + // Call everyone who has registered on our coroutine so that those promises also resolve + for (const auto& cont : continuationContexts) { + cont->resolve(resolveVals); + } + } +}; + +LuaPromise::LuaPromise() : m_state(std::make_shared()) {} + +LuaPromise::LuaPromise(luacpp::LuaValueList resolveValue) : LuaPromise() +{ + m_state->state = State::Resolved; + m_state->resolvedValue = std::move(resolveValue); +} + +LuaPromise::LuaPromise(std::shared_ptr resolveContext) : LuaPromise() +{ + m_state->state = State::Pending; + m_state->resolveCtx = std::move(resolveContext); + + // This executes promises eagerly since registering the callback kicks of the async operation + m_state->registerResolveCallback(); +} + +LuaPromise::LuaPromise(const LuaPromise&) = default; +LuaPromise& LuaPromise::operator=(const LuaPromise&) = default; + +LuaPromise::LuaPromise(LuaPromise&&) noexcept = default; +LuaPromise& LuaPromise::operator=(LuaPromise&&) noexcept = default; + +LuaPromise LuaPromise::then(LuaPromise::ContinuationFunction continuation) +{ + // NOT THREAD SAFE! + if (m_state->state == State::Invalid) { + return LuaPromise(); + } + + // The easy way + if (m_state->state == State::Resolved) { + return LuaPromise(continuation(m_state->resolvedValue)); + } + + // This is the connection between us and the resulting promise. We need the reference to resolve the promise + // and the returned promise uses it to know when to resolve + auto continuationContext = std::make_shared(std::move(continuation)); + m_state->continuationContexts.push_back(continuationContext); + + return LuaPromise(continuationContext); +} +bool LuaPromise::isValid() const { return m_state->state != State::Invalid; } +bool LuaPromise::isResolved() const { return m_state->state == State::Resolved; } + +const luacpp::LuaValueList& LuaPromise::resolveValue() const +{ + Assertion(isResolved(), "Tried to get value from unresolved promise!"); + + return m_state->resolvedValue; +} + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/LuaPromise.h b/code/scripting/api/LuaPromise.h new file mode 100644 index 0000000000..21794f7f19 --- /dev/null +++ b/code/scripting/api/LuaPromise.h @@ -0,0 +1,72 @@ +#pragma once + +#include "scripting/lua/LuaTypes.h" + +namespace scripting { +namespace api { + +/** + * @brief A context that manages an asynchronous operation + * + * This is the interface used by a LuaPromise for running pending promises. It gets passed a callback that should be + * called when a promise has resolved. + */ +class resolve_context { + public: + virtual ~resolve_context(); + + using Resolver = std::function; + + /** + * @brief Sets the function which is called when an asynchronous operation finishes + * + * @note For future compatibility, the operation represented by a subclass of this should not start executing until + * this function is called. + * + * @param resolver The function to call to resolve a coroutine + */ + virtual void setResolver(Resolver resolver) = 0; +}; + +class LuaPromise { + public: + using ContinuationFunction = std::function; + + /** + * @brief Initializes an invalid promise + */ + LuaPromise(); + + /** + * @brief Creates a resolved promise + * @param resolveValue The value with which the promise resolves + */ + explicit LuaPromise(luacpp::LuaValueList resolveValue); + + /** + * @brief Creates a pending promise + * @param resolveContext The context to register on + */ + explicit LuaPromise(std::shared_ptr resolveContext); + + LuaPromise(const LuaPromise&); + LuaPromise& operator=(const LuaPromise&); + + LuaPromise(LuaPromise&&) noexcept; + LuaPromise& operator=(LuaPromise&&) noexcept; + + LuaPromise then(ContinuationFunction continuation); + + bool isValid() const; + + bool isResolved() const; + + const luacpp::LuaValueList& resolveValue() const; + + private: + struct internal_state; + std::shared_ptr m_state; +}; + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/libs/async.cpp b/code/scripting/api/libs/async.cpp new file mode 100644 index 0000000000..b67bf5a5e9 --- /dev/null +++ b/code/scripting/api/libs/async.cpp @@ -0,0 +1,136 @@ + +#include "async.h" + +#include "scripting/api/LuaCoroutineRunner.h" +#include "scripting/api/objs/promise.h" +#include "scripting/lua/LuaThread.h" + +namespace scripting { +namespace api { + +//**********LIBRARY: Async +ADE_LIB(l_Async, "Async", "async", "Support library for asynchronous operations"); + +/** + * @brief A simple run context that allows a promise to be resolved by calling a lua function + * + * This is very similar to JavaScript Promises where a user defined function is passed a special "resolve" function that + * should be called when the coroutine is finished. + */ +class lua_func_resolve_context : public resolve_context { + public: + lua_func_resolve_context(luacpp::LuaFunction callback) : _callback(std::move(callback)) {} + + void setResolver(Resolver resolver) override + { + const auto resolveFunc = luacpp::LuaFunction::createFromStdFunction(_callback.getLuaState(), + [resolver](lua_State*, const luacpp::LuaValueList& resolveVals) { + resolver(resolveVals); + return luacpp::LuaValueList(); + }); + + _callback({resolveFunc}); + } + + private: + luacpp::LuaFunction _callback; +}; + +ADE_FUNC(promise, + l_Async, + "function(function resolve(...))", + "Creates a promise that resolves when the resolve function of the callback is called. The function will be called " + "on it's own.", + "promise", + "The promise or nil on error") +{ + luacpp::LuaFunction callback; + if (!ade_get_args(L, "u", &callback)) { + return ADE_RETURN_NIL; + } + + if (!callback.isValid()) { + LuaError(L, "Invalid function supplied!"); + return ADE_RETURN_NIL; + } + + std::unique_ptr resolveCtx(new lua_func_resolve_context(std::move(callback))); + + return ade_set_args(L, "o", l_Promise.Set(LuaPromise(std::move(resolveCtx)))); +} + +ADE_FUNC(resolved, + l_Async, + "...", + "Creates a resolved promise with the values passed to this function.", + "promise", + "Resolved promise") +{ + auto nargs = lua_gettop(L); + luacpp::LuaValueList values; + values.reserve(nargs); + + for (int i = 1; i <= nargs; ++i) { + luacpp::LuaValue val; + val.setReference(luacpp::UniqueLuaReference::create(L, i)); + values.push_back(val); + } + + return ade_set_args(L, "o", l_Promise.Set(LuaPromise(values))); +} + +ADE_FUNC(run, + l_Async, + "function body()", + "Runs an asynchronous function", + "promise", + "A promise that resolves with the return value of the body when it reaches a return statement") +{ + luacpp::LuaFunction body; + if (!ade_get_args(L, "u", &body)) { + return ADE_RETURN_NIL; + } + + auto coroutine = luacpp::LuaThread::create(L, body); + coroutine.setErrorCallback([](lua_State*, lua_State* thread) { + LuaError(thread); + return true; + }); + + return ade_set_args(L, "o", l_Promise.Set(runAsyncCoroutine(std::move(coroutine)))); +} + +ADE_FUNC(await, + l_Async, + "promise", + "Suspends an asynchronous coroutine until the passed promise resolves.", + "unknown", + "The resolve value of the promise") +{ + LuaPromise* promise = nullptr; + if (!ade_get_args(L, "o", l_Promise.GetPtr(&promise))) { + return ADE_RETURN_NIL; + } + + if (promise == nullptr || !promise->isValid()) { + LuaError(L, "Invalid promise detected. This should not happen. Please contact a developer."); + return ADE_RETURN_NIL; + } + + if (promise->isResolved()) { + // No need to suspend if the promise is already resolved. Just take the resolve values from the promise and + // return them + const auto& retVals = promise->resolveValue(); + for (const auto& retVal : retVals) { + retVal.pushValue(L); // Push onto this stack to ensure it is actually returned from this function + } + return static_cast(retVals.size()); + } + + // Return the promise via the yield so that the resumer can register themself on the promise to resume when that + // resolves + return lua_yield(L, 1); +} + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/libs/async.h b/code/scripting/api/libs/async.h new file mode 100644 index 0000000000..6349e3f487 --- /dev/null +++ b/code/scripting/api/libs/async.h @@ -0,0 +1,11 @@ +#pragma once + +#include "scripting/ade_api.h" + +namespace scripting { +namespace api { + +DECLARE_ADE_LIB(l_Async); + +} +} // namespace scripting diff --git a/code/scripting/api/objs/promise.cpp b/code/scripting/api/objs/promise.cpp new file mode 100644 index 0000000000..89464ca32c --- /dev/null +++ b/code/scripting/api/objs/promise.cpp @@ -0,0 +1,93 @@ +// +// Created by marius on 14.05.20. +// + +#include "promise.h" + +namespace scripting { +namespace api { + +ADE_OBJ(l_Promise, + LuaPromise, + "promise", + "A promise that represents an operation that will return a value at some point in the future"); + +// Can't call this "then" since that is a Lua keyword +ADE_FUNC(continueWith, + l_Promise, + "function(args...)", + "When the called on promise resolves, this function will be called with the resolved value of the promise.", + "promise", + "A promise that will resolve with the return value of the passed function.") +{ + LuaPromise* promise = nullptr; + luacpp::LuaFunction thenFunc; + if (!ade_get_args(L, "ou", l_Promise.GetPtr(&promise), &thenFunc)) { + return ADE_RETURN_NIL; + } + + if (!thenFunc.isValid()) { + LuaError(L, "'continueWith' function is invalid!"); + return ADE_RETURN_NIL; + } + + if (promise == nullptr || !promise->isValid()) { + LuaError(L, "Invalid promise detected. This should not happen. Please contact a developer."); + return ADE_RETURN_NIL; + } + + return ade_set_args(L, "o", l_Promise.Set(promise->then(thenFunc))); +} + +ADE_FUNC(isResolved, + l_Promise, + nullptr, + "Checks if the promise is already resolved.", + "boolean", + "true if resolved, false if result is still pending.") +{ + LuaPromise* promise = nullptr; + if (!ade_get_args(L, "o", l_Promise.GetPtr(&promise))) { + return ADE_RETURN_NIL; + } + + if (promise == nullptr || !promise->isValid()) { + LuaError(L, "Invalid promise detected. This should not happen. Please contact a developer."); + return ADE_RETURN_NIL; + } + + return ade_set_args(L, "b", promise->isResolved()); +} + +ADE_FUNC(getValue, + l_Promise, + nullptr, + "Gets the resolved value of this promise. Causes an error when used on an unresolved promise!", + "unknown", + "The resolved values.") +{ + LuaPromise* promise = nullptr; + if (!ade_get_args(L, "o", l_Promise.GetPtr(&promise))) { + return ADE_RETURN_NIL; + } + + if (promise == nullptr || !promise->isValid()) { + LuaError(L, "Invalid promise detected. This should not happen. Please contact a developer."); + return ADE_RETURN_NIL; + } + + if (!promise->isResolved()) { + LuaError(L, "Tried to get the value of a promise that was not resolved!"); + return ADE_RETURN_NIL; + } + + // We can't use our usual functions for returning here since we return a variable amount of values + const auto& retVals = promise->resolveValue(); + for (const auto& retVal : retVals) { + retVal.pushValue(L); + } + return static_cast(retVals.size()); +} + +} // namespace api +} // namespace scripting diff --git a/code/scripting/api/objs/promise.h b/code/scripting/api/objs/promise.h new file mode 100644 index 0000000000..0968addca9 --- /dev/null +++ b/code/scripting/api/objs/promise.h @@ -0,0 +1,12 @@ +#pragma once + +#include "scripting/ade_api.h" +#include "scripting/api/LuaPromise.h" + +namespace scripting { +namespace api { + +DECLARE_ADE_OBJ(l_Promise, LuaPromise); + +} // namespace api +} // namespace scripting diff --git a/code/scripting/lua.cpp b/code/scripting/lua.cpp index d99cb920d4..4fd9ab4260 100644 --- a/code/scripting/lua.cpp +++ b/code/scripting/lua.cpp @@ -1,6 +1,8 @@ #include "scripting/scripting.h" +#include "scripting/lua/LuaUtil.h" + /** * IMPORTANT! * @@ -34,6 +36,7 @@ #include "scripting/api/objs/model_path.h" #include "scripting/api/objs/physics_info.h" #include "scripting/api/objs/player.h" +#include "scripting/api/objs/promise.h" #include "scripting/api/objs/sexpvar.h" #include "scripting/api/objs/shields.h" #include "scripting/api/objs/ship.h" @@ -55,6 +58,7 @@ #include "scripting/api/objs/wing.h" #include "scripting/api/libs/audio.h" +#include "scripting/api/libs/async.h" #include "scripting/api/libs/base.h" #include "scripting/api/libs/bitops.h" #include "scripting/api/libs/cfile.h" @@ -108,6 +112,9 @@ int script_state::CreateLuaState() mprintf(("LUA: Initializing base Lua libraries...\n")); luaL_openlibs(L); + //*****INITIALIZE OUR SUPPORT LIBRARY + luacpp::util::initializeLuaSupportLib(L); + //*****DISABLE DANGEROUS COMMANDS lua_pushstring(L, "os"); lua_rawget(L, LUA_GLOBALSINDEX); diff --git a/code/scripting/lua/LuaFunction.cpp b/code/scripting/lua/LuaFunction.cpp index 8a5e4c747b..5d7afd210f 100644 --- a/code/scripting/lua/LuaFunction.cpp +++ b/code/scripting/lua/LuaFunction.cpp @@ -4,14 +4,62 @@ #include "LuaHeaders.h" namespace luacpp { -LuaFunction LuaFunction::createFromCFunction(lua_State* L, lua_CFunction function, const LuaValueList& upvalues) { +namespace { + +int stdFunctionDestructor(lua_State* L) +{ + // The userdata is the function object so we call the destructor here to free up the internal resources + const auto luaPtr = lua_touserdata(L, 1); + const auto funcPtr = static_cast(luaPtr); + + funcPtr->~LuaFunctionObject(); + + return 0; +} + +int stdFunctionCaller(lua_State* L) +{ + const auto numArgs = lua_gettop(L); + + // Push the actual function object on the stack + lua_pushvalue(L, lua_upvalueindex(1)); + + const auto funcObj = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); // We have the pointer so we can remove this from the stack again + + LuaValueList params; + params.reserve(numArgs); + + for (int i = 1; i <= numArgs; ++i) { + LuaValue val; + val.setReference(UniqueLuaReference::create(L, i)); + params.push_back(val); + } + + const auto ret = (*funcObj)(L, params); + + // Prepare to return values. Clear the stack + lua_settop(L, 0); + for (const auto& retVal : ret) + { + retVal.pushValue(L); + } + + // Done! + return static_cast(ret.size()); +} + +} + +LuaFunction LuaFunction::createFromCFunction(lua_State* L, lua_CFunction function, const LuaValueList& upvalues) +{ LuaFunction func; for (auto& val : upvalues) { - val.pushValue(); + val.pushValue(L); } - lua_pushcclosure(L, function, (int) upvalues.size()); + lua_pushcclosure(L, function, (int)upvalues.size()); func.setReference(UniqueLuaReference::create(L)); @@ -19,6 +67,23 @@ LuaFunction LuaFunction::createFromCFunction(lua_State* L, lua_CFunction functio return func; } +LuaFunction LuaFunction::createFromStdFunction(lua_State* L, LuaFunctionObject function) { + auto userdataMetatable = LuaTable::create(L); + userdataMetatable.addValue("__gc", stdFunctionDestructor); + + // Create storage for the function object in Lua and initialize our data there via placement new + auto functionStorage = lua_newuserdata(L, sizeof(function)); + new (functionStorage) LuaFunctionObject(std::move(function)); + + userdataMetatable.pushValue(L); + lua_setmetatable(L, -2); + + LuaValue funcValue; + funcValue.setReference(UniqueLuaReference::create(L)); + lua_pop(L, 1); // Value is referenced. We can remove it from the stack now + + return LuaFunction::createFromCFunction(L, stdFunctionCaller, { funcValue }); +} LuaFunction LuaFunction::createFromCode(lua_State* L, std::string const& code, std::string const& name) { int load_err = luaL_loadbuffer(L, code.c_str(), code.length(), name.c_str()); @@ -48,6 +113,9 @@ LuaFunction::LuaFunction() : LuaValue(), _errorFunction(nullptr) { LuaFunction::LuaFunction(const LuaFunction&) = default; LuaFunction& LuaFunction::operator=(const LuaFunction&) = default; +LuaFunction::LuaFunction(LuaFunction&&) noexcept = default; +LuaFunction& LuaFunction::operator=(LuaFunction&&) noexcept = default; + LuaFunction::~LuaFunction() = default; bool LuaFunction::setEnvironment(const LuaTable& table) { @@ -55,8 +123,8 @@ bool LuaFunction::setEnvironment(const LuaTable& table) { throw LuaException("Table reference is not valid!"); } - this->pushValue(); - table.pushValue(); + this->pushValue(_luaState); + table.pushValue(_luaState); bool ret = lua_setfenv(_luaState, -2) != 0; @@ -78,10 +146,10 @@ void LuaFunction::setReference(const LuaReference& ref) { return; } - ref->pushValue(); - lua_State* L = ref->getState(); + ref->pushValue(L); + if (lua_type(L, -1) != LUA_TFUNCTION) { lua_pop(L, 1); throw LuaException("Reference does not refere to a function!"); @@ -97,7 +165,7 @@ LuaValueList LuaFunction::call(const LuaValueList& args) const { if (_errorFunction) { // push the error function - _errorFunction->pushValue(); + _errorFunction->pushValue(_luaState); err_idx = lua_gettop(_luaState); stackTop = err_idx; } else { @@ -105,11 +173,11 @@ LuaValueList LuaFunction::call(const LuaValueList& args) const { } // Push the function onto the stack - this->pushValue(); + this->pushValue(_luaState); // Push the arguments onto the stack for (const auto& arg : args) { - arg.pushValue(); + arg.pushValue(_luaState); } // actually call the function now! diff --git a/code/scripting/lua/LuaFunction.h b/code/scripting/lua/LuaFunction.h index d0fbe724d2..ece1509ffd 100644 --- a/code/scripting/lua/LuaFunction.h +++ b/code/scripting/lua/LuaFunction.h @@ -3,18 +3,19 @@ #pragma once #include "LuaConvert.h" -#include "LuaValue.h" #include "LuaTable.h" +#include "LuaTypes.h" +#include "LuaValue.h" -#include -#include #include - +#include +#include namespace luacpp { -typedef std::vector LuaValueList; class LuaFunction; +using LuaFunctionObject = std::function; + /** * @brief A reference to lua code. * @@ -25,6 +26,7 @@ class LuaFunction; */ class LuaFunction: public LuaValue { public: + /** * @brief Compile lua code and return the function. * The specified code is compiled into a lua functions which can then be called with the returned class. @@ -47,6 +49,14 @@ class LuaFunction: public LuaValue { */ static LuaFunction createFromCFunction(lua_State* L, lua_CFunction function, const LuaValueList& upvalues = LuaValueList()); + /** + * @brief Creates a lua function from a C++ function object + * @param L The lua state to create the function in + * @param function The function to wrap + * @return A handle of a Lua function that can be used for calling that function + */ + static LuaFunction createFromStdFunction(lua_State* L, LuaFunctionObject function); + /** * @brief Default constructor */ @@ -62,6 +72,9 @@ class LuaFunction: public LuaValue { LuaFunction(const LuaFunction& other); LuaFunction& operator=(const LuaFunction& other); + LuaFunction(LuaFunction&&) noexcept; + LuaFunction& operator=(LuaFunction&&) noexcept; + /** * @brief Frees the reference to the function if it exists. */ diff --git a/code/scripting/lua/LuaReference.cpp b/code/scripting/lua/LuaReference.cpp index d497b8f1bb..7bd4cefd61 100644 --- a/code/scripting/lua/LuaReference.cpp +++ b/code/scripting/lua/LuaReference.cpp @@ -1,39 +1,45 @@ #include "LuaReference.h" -#include "LuaException.h" +#include "globalincs/pstypes.h" + +#include "LuaException.h" #include "LuaHeaders.h" +#include "LuaUtil.h" namespace luacpp { -LuaReference UniqueLuaReference::create(lua_State* state, int position) { +LuaReference UniqueLuaReference::create(lua_State* state, int position) +{ if (state == nullptr) { throw LuaException("Need a valid lua state!"); } lua_pushvalue(state, position); - LuaReference ref = std::make_shared(state, luaL_ref(state, LUA_REGISTRYINDEX)); + // Always store the main thread here to ensure that we do not store a possible invalid thread reference + LuaReference ref = + std::make_shared(util::getMainThread(state), luaL_ref(state, LUA_REGISTRYINDEX)); return ref; } LuaReference UniqueLuaReference::copy(const LuaReference& other) { - other->pushValue(); + other->pushValue(other->_luaState); return create(other->_luaState); } -UniqueLuaReference::UniqueLuaReference(lua_State* state, int reference) : _luaState(state), _reference(reference) { +UniqueLuaReference::UniqueLuaReference(lua_State* state, int reference) : _luaState(state), _reference(reference) +{ if (state == nullptr) { throw LuaException("Need a valid lua state!"); } - if (reference < 0) { + if (reference < 0 && reference != LUA_REFNIL) { throw LuaException("Reference must be greater than or equal to zero!"); } } -UniqueLuaReference::UniqueLuaReference() : _luaState(nullptr), _reference(LUA_NOREF) { -} +UniqueLuaReference::UniqueLuaReference() : _luaState(nullptr), _reference(LUA_NOREF) {} UniqueLuaReference::UniqueLuaReference(UniqueLuaReference&& other) SCP_NOEXCEPT : _luaState(nullptr), _reference(LUA_NOREF) { *this = std::move(other); @@ -74,9 +80,12 @@ bool UniqueLuaReference::isValid() const { return true; } -void UniqueLuaReference::pushValue() const { +void UniqueLuaReference::pushValue(lua_State* thread) const +{ + Assertion(thread != nullptr, "Valid thread state must be specified!"); + if (this->isValid()) { - lua_rawgeti(_luaState, LUA_REGISTRYINDEX, this->getReference()); + lua_rawgeti(thread, LUA_REGISTRYINDEX, this->getReference()); } } } diff --git a/code/scripting/lua/LuaReference.h b/code/scripting/lua/LuaReference.h index 40c9a1c544..0c5cb70c30 100644 --- a/code/scripting/lua/LuaReference.h +++ b/code/scripting/lua/LuaReference.h @@ -101,8 +101,9 @@ class UniqueLuaReference { /** * @brief Pushes the referenced value onto the stack. + * @param thread A specific thread state to push the value to. nullptr for the default state of this reference */ - void pushValue() const; + void pushValue(lua_State* thread) const; }; } diff --git a/code/scripting/lua/LuaTable.cpp b/code/scripting/lua/LuaTable.cpp index 3c9d3d9b64..27d5fb12e8 100644 --- a/code/scripting/lua/LuaTable.cpp +++ b/code/scripting/lua/LuaTable.cpp @@ -28,8 +28,8 @@ bool LuaTable::setMetatable(const LuaTable& table) { throw LuaException("Meta table reference is not valid!"); } - this->pushValue(); - table.pushValue(); + this->pushValue(_luaState); + table.pushValue(_luaState); lua_setmetatable(_luaState, -2); @@ -39,10 +39,10 @@ bool LuaTable::setMetatable(const LuaTable& table) { } void LuaTable::setReference(const LuaReference& ref) { - ref->pushValue(); - lua_State* L = ref->getState(); + ref->pushValue(L); + if (lua_type(L, -1) != LUA_TTABLE) { lua_pop(L, 1); throw LuaException("Reference does not refere to a table!"); @@ -53,7 +53,7 @@ void LuaTable::setReference(const LuaReference& ref) { } size_t LuaTable::getLength() { - this->pushValue(); + this->pushValue(_luaState); size_t length = lua_objlen(_luaState, -1); @@ -100,7 +100,7 @@ LuaTable::iterator LuaTable::end() { LuaTableIterator::LuaTableIterator(const LuaTable& t) : _luaState(t.getLuaState()) { _stackTop = lua_gettop(_luaState); - t.pushValue(); + t.pushValue(_luaState); lua_pushnil(_luaState); toNextElement(); diff --git a/code/scripting/lua/LuaTable.h b/code/scripting/lua/LuaTable.h index b0c042208f..afcbbd4bab 100644 --- a/code/scripting/lua/LuaTable.h +++ b/code/scripting/lua/LuaTable.h @@ -138,7 +138,7 @@ class LuaTable: public LuaValue { template void addValue(IndexType&& index, ValueType&& value) { // Push the table onto the stack by using the reference - this->pushValue(); + this->pushValue(_luaState); // Push the index and value onto the stac by using the template functions convert::pushValue(_luaState, std::forward(index)); @@ -160,7 +160,7 @@ class LuaTable: public LuaValue { */ template bool getValue(const IndexType& index, ValueType& target) { - this->pushValue(); + this->pushValue(_luaState); convert::pushValue(_luaState, index); diff --git a/code/scripting/lua/LuaThread.cpp b/code/scripting/lua/LuaThread.cpp new file mode 100644 index 0000000000..8ca07fd2af --- /dev/null +++ b/code/scripting/lua/LuaThread.cpp @@ -0,0 +1,120 @@ +#include "LuaThread.h" + +#include "LuaException.h" + +#include + +namespace luacpp { +LuaThread LuaThread::create(lua_State* L, const LuaFunction& func) +{ + auto luaThread = lua_newthread(L); + func.pushValue(L); + // Move the main function to the thread (I have no idea what this actually does but the Lua code does the same...) + lua_xmove(L, luaThread, 1); + + LuaThread thread(L, luaThread); + thread.setReference(UniqueLuaReference::create(L)); + + lua_pop(L, 1); + + return thread; +} + +LuaThread::LuaThread() : LuaValue() {} +LuaThread::LuaThread(lua_State* luaState, lua_State* thread) : LuaValue(luaState), _thread(thread) {} + +LuaThread::LuaThread(const LuaThread&) = default; +LuaThread& LuaThread::operator=(const LuaThread&) = default; + +LuaThread::LuaThread(LuaThread&&) noexcept = default; +LuaThread& LuaThread::operator=(LuaThread&&) noexcept = default; + +LuaThread::~LuaThread() = default; + +void LuaThread::setReference(const LuaReference& ref) +{ + lua_State* L = ref->getState(); + + ref->pushValue(L); + + if (lua_type(L, -1) != LUA_TTHREAD) { + lua_pop(L, 1); + throw LuaException("Reference does not refer to a thread!"); + } else { + lua_pop(L, 1); + LuaValue::setReference(ref); + } +} +LuaThread::ResumeState LuaThread::resume(const LuaValueList& params) const +{ + int nargs = static_cast(params.size()); + for (const auto& val : params) { + val.pushValue(_luaState); + } + + // Move parameters to the thread + lua_xmove(_luaState, _thread, nargs); + + // now resume + const auto result = lua_resume(_thread, nargs); + + if (result != 0 && result != LUA_YIELD) { + if (_errorCallback) { + if (_errorCallback(_luaState, _thread)) { + // If the error was handled just pretend that we resumed successfully + ResumeState resume; + resume.completed = true; + return resume; + } + } + + throw LuaException("Lua coroutine failed to resume!"); + } + + LuaValueList returnVals; + auto numRet = lua_gettop(_thread); + returnVals.reserve(numRet); + + // Move the values back to the main state so that it uses the right state + auto previousStack = lua_gettop(_luaState); // Keep this for cleaning up later + lua_xmove(_thread, _luaState, numRet); + + auto retValsStart = previousStack + 1; // We need to start at + 1 since that is where the first return val would be + for (int i = retValsStart; i <= previousStack + numRet; ++i) { + LuaValue val; + convert::popValue(_luaState, val, i, false); + returnVals.push_back(std::move(val)); + } + + // Cleanup the stack + lua_settop(_luaState, previousStack); + + ResumeState state; + state.completed = result != LUA_YIELD; + state.returnVals = std::move(returnVals); + return state; +} +void LuaThread::setErrorCallback(LuaThread::ErrorCallback errorCallback) { _errorCallback = std::move(errorCallback); } + +lua_State* LuaThread::getThreadHandle() const { return _thread; } + +bool convert::popValue(lua_State* luaState, LuaThread& target, int stackposition, bool remove) +{ + if (!internal::isValidIndex(luaState, stackposition)) { + return false; + } + + if (!lua_isthread(luaState, stackposition)) { + return false; + } else { + target.setReference(UniqueLuaReference::create(luaState, stackposition)); + + if (remove) { + lua_remove(luaState, stackposition); + } + + return true; + } +} + +} // namespace luacpp diff --git a/code/scripting/lua/LuaThread.h b/code/scripting/lua/LuaThread.h new file mode 100644 index 0000000000..44b5a7fdc6 --- /dev/null +++ b/code/scripting/lua/LuaThread.h @@ -0,0 +1,76 @@ +#pragma once + +#include "LuaConvert.h" +#include "LuaTypes.h" +#include "LuaValue.h" +#include "LuaFunction.h" + +#include + +namespace luacpp { + +/** + * @brief Class to improve handling of lua threads (aka coroutines). + * + * This class provides a high-level interface to lua threads. + * + * @see LuaConvert + */ +class LuaThread : public LuaValue { + public: + using ErrorCallback = std::function; + struct ResumeState + { + bool completed = false; + LuaValueList returnVals; + }; + + /** + * @brief Creates a new empty thread. + */ + static LuaThread create(lua_State* L, const LuaFunction& func); + + /** + * @brief Default constructor + */ + LuaThread(); + + /** + * @brief Copy-constructor + * @param other The other thread. + */ + LuaThread(const LuaThread&); + LuaThread& operator=(const LuaThread&); + + LuaThread(LuaThread&&) noexcept; + LuaThread& operator=(LuaThread&&) noexcept; + + ~LuaThread() override; + + /** + * @brief Sets a new reference. + * This overload checks if the passed reference is a thread + * + * @param ref The new reference + * @return void + */ + void setReference(const LuaReference& ref) override; + + void setErrorCallback(ErrorCallback errorCallback); + + ResumeState resume(const LuaValueList& params) const; + + lua_State* getThreadHandle() const; + private: + LuaThread(lua_State* luaState, lua_State* threadHandle); + + lua_State* _thread = nullptr; + ErrorCallback _errorCallback; +}; + +namespace convert { + +bool popValue(lua_State* luaState, LuaThread& target, int stackposition = -1, bool remove = true); + +} +} // namespace luacpp diff --git a/code/scripting/lua/LuaTypes.h b/code/scripting/lua/LuaTypes.h new file mode 100644 index 0000000000..f2686b8542 --- /dev/null +++ b/code/scripting/lua/LuaTypes.h @@ -0,0 +1,7 @@ +#pragma once + +namespace luacpp { +class LuaValue; + +typedef std::vector LuaValueList; +} // namespace luacpp diff --git a/code/scripting/lua/LuaUtil.cpp b/code/scripting/lua/LuaUtil.cpp index ef0e417a69..1e96762511 100644 --- a/code/scripting/lua/LuaUtil.cpp +++ b/code/scripting/lua/LuaUtil.cpp @@ -1,16 +1,21 @@ #include "LuaUtil.h" +namespace { +const char* mainStateRefName = "_lua_mainthread"; +} + namespace luacpp { namespace util { -const char* getValueName(ValueType type) { +const char* getValueName(ValueType type) +{ switch (type) { - case ValueType::NONE: - return "none"; - case ValueType::NIL: - return "nil"; - case ValueType::BOOLEAN: - return "boolean"; + case ValueType::NONE: + return "none"; + case ValueType::NIL: + return "nil"; + case ValueType::BOOLEAN: + return "boolean"; case ValueType::LIGHTUSERDATA: return "light userdata"; case ValueType::STRING: @@ -27,7 +32,33 @@ const char* getValueName(ValueType type) { return "thread"; default: return "unknown"; + } +} + +void initializeLuaSupportLib(lua_State* L) +{ + auto mainThread = lua_pushthread(L); + Assertion(mainThread, "Must be called with the main thread as the parameter!"); + + lua_setfield(L, LUA_REGISTRYINDEX, mainStateRefName); +} + +lua_State* getMainThread(lua_State* L) +{ + if (lua_pushthread(L)) { + // State is the main thread, just return that + lua_pop(L, 1); + return L; } + + lua_pushstring(L, mainStateRefName); + lua_rawget(L, LUA_REGISTRYINDEX); + + auto state = lua_tothread(L, -1); + lua_pop(L, 1); + + return state; } -} -} + +} // namespace util +} // namespace luacpp diff --git a/code/scripting/lua/LuaUtil.h b/code/scripting/lua/LuaUtil.h index 3046079747..89b20c4e8f 100644 --- a/code/scripting/lua/LuaUtil.h +++ b/code/scripting/lua/LuaUtil.h @@ -15,6 +15,18 @@ class LuaTable; namespace util { +/** + * @brief Initializes this sublibrary in the specified lua state + * @param L The lua state to initialize with. Must be the main thread. + */ +void initializeLuaSupportLib(lua_State* L); + +/** + * @brief From the specified state gets the main thread. + * @param L The state to query + */ +lua_State* getMainThread(lua_State* L); + /** * @brief Fills the given container with all key-value pairs. * Each key-value pair is converted into the type given to the Container and then push_back is used to add it. @@ -35,13 +47,13 @@ void tableListPairs(LuaTable& table, Container& keyValueList) { lua_State* L = table.getLuaState(); for (auto val : table) { - val.first.pushValue(); + val.first.pushValue(L); key_type key; if (!convert::popValue(L, key)) { throw LuaException("Failed to pop key!"); } - val.second.pushValue(); + val.second.pushValue(L); value_type value; if (!convert::popValue(L, value)) { diff --git a/code/scripting/lua/LuaValue.cpp b/code/scripting/lua/LuaValue.cpp index a09134f5a0..c2d9291eb9 100644 --- a/code/scripting/lua/LuaValue.cpp +++ b/code/scripting/lua/LuaValue.cpp @@ -34,7 +34,8 @@ ValueType luaToEnumType(int luaType) { } namespace luacpp { -LuaValue LuaValue::createNil(lua_State* L) { +LuaValue LuaValue::createNil(lua_State* L) +{ lua_pushnil(L); LuaValue val(L); @@ -44,33 +45,34 @@ LuaValue LuaValue::createNil(lua_State* L) { return val; } -LuaValue::LuaValue(lua_State* state) : _luaState(state), _luaType(ValueType::NONE) { +LuaValue::LuaValue() = default; + +LuaValue::LuaValue(lua_State* state) : _luaState(state) +{ Assertion(state != nullptr, "Lua state pointer is not valid!"); } -LuaValue::LuaValue(const LuaValue& other) : _luaState(other._luaState) { - // Just copy the reference object from the other - this->setReference(other._reference); -} +LuaValue::LuaValue(const LuaValue&) = default; +LuaValue& LuaValue::operator=(const LuaValue&) = default; -LuaValue& LuaValue::operator=(const LuaValue& other) { - _luaState = other._luaState; - this->setReference(other._reference); - return *this; -} +LuaValue::LuaValue(LuaValue&&) noexcept = default; +LuaValue& LuaValue::operator=(LuaValue&&) noexcept = default; LuaValue::~LuaValue() = default; -void LuaValue::setReference(const LuaReference& ref) { +void LuaValue::setReference(const LuaReference& ref) +{ this->_reference = ref; + this->_luaState = ref->getState(); if (isValid()) { - this->_reference->pushValue(); + this->_reference->pushValue(_luaState); - _luaState = ref->getState(); this->_luaType = luaToEnumType(lua_type(_luaState, -1)); lua_pop(_luaState, 1); + } else { + _luaType = ValueType::NONE; } } @@ -86,30 +88,24 @@ bool LuaValue::isValid() const { return _reference && _reference->isValid(); } -bool LuaValue::pushValue() const { +bool LuaValue::pushValue(lua_State* thread) const +{ if (this->_reference->isValid()) { - this->_reference->pushValue(); + this->_reference->pushValue(thread); return true; } else { return false; } } -lua_State* LuaValue::getLuaState() const { - return _luaState; -} +lua_State* LuaValue::getLuaState() const { return _luaState; } namespace convert { -void pushValue(lua_State* luaState, const LuaValue& value) { - if (luaState != value.getLuaState()) { - throw LuaException("Lua state mismatch!"); - } +void pushValue(lua_State* luaState, const LuaValue& value) { value.pushValue(luaState); } - value.pushValue(); -} - -bool popValue(lua_State* luaState, LuaValue& target, int stackposition, bool remove) { +bool popValue(lua_State* luaState, LuaValue& target, int stackposition, bool remove) +{ if (!internal::isValidIndex(luaState, stackposition)) { return false; } diff --git a/code/scripting/lua/LuaValue.h b/code/scripting/lua/LuaValue.h index d098032fe3..9618532310 100644 --- a/code/scripting/lua/LuaValue.h +++ b/code/scripting/lua/LuaValue.h @@ -28,7 +28,7 @@ enum class ValueType { TABLE, FUNCTION, USERDATA, - THREAD + THREAD, }; /** @@ -63,37 +63,44 @@ class LuaValue { static LuaValue createNil(lua_State* L); /** - * @brief Default constructor, creates an invalid LuaValue - */ - LuaValue() : _luaState(nullptr), _luaType(ValueType::NONE) {} + * @brief Default constructor, creates an invalid LuaValue + */ + LuaValue(); /** - * @brief Initializes the lua value - * - * The instance does not point to a lua-value after the constructor has finished. To reference - * a value use #setReference(LuaReferencePtr) - * - * @param state The lua state - */ + * @brief Initializes the lua value + * + * The instance does not point to a lua-value after the constructor has finished. To reference + * a value use #setReference(LuaReferencePtr) + * + * @param state The lua state + */ LuaValue(lua_State* state); /** - * @brief Copy-constructor - * @param other The other LuaValue. - */ + * @brief Copy-constructor + * @param other The other LuaValue. + */ LuaValue(const LuaValue& other); LuaValue& operator=(const LuaValue& other); /** - * @brief Releases the reference - */ + * @brief Move-constructor + * @param other The other LuaValue. + */ + LuaValue(LuaValue&& other) noexcept; + LuaValue& operator=(LuaValue&& other) noexcept; + + /** + * @brief Releases the reference + */ virtual ~LuaValue(); /** - * @brief Sets a new LuaReference. - * - * @param ref The new lua reference. - */ + * @brief Sets a new LuaReference. + * + * @param ref The new lua reference. + */ virtual void setReference(const LuaReference& ref); /** @@ -141,7 +148,7 @@ class LuaValue { */ template Type getValue() const { - _reference->pushValue(); + _reference->pushValue(_luaState); Type target; if (!convert::popValue(_luaState, target)) { @@ -160,13 +167,16 @@ class LuaValue { bool isValid() const; /** - * @brief Pushes this lua value onto the stack. - */ - virtual bool pushValue() const; + * @brief Pushes this lua value onto the stack. + * @param thread The thread stack onto which this value should be pushed. May be nullptr for the default state of + * this value + */ + bool pushValue(lua_State* thread) const; lua_State* getLuaState() const; - protected: - lua_State* _luaState; //!< The lua state of this value. + + protected: + lua_State* _luaState{nullptr}; //!< The lua state of this value. LuaReference _reference; @@ -181,7 +191,7 @@ class LuaValue { */ template bool operator==(const LuaValue& lhs, const Type& rhs) { - lhs.pushValue(); + lhs.pushValue(lhs.getLuaState()); convert::pushValue(lhs.getLuaState(), rhs); bool result = lua_equal(lhs.getLuaState(), -2, -1) != 0; @@ -200,7 +210,7 @@ bool operator==(const LuaValue& lhs, const Type& rhs) { */ template bool operator<(const LuaValue& lhs, const Type& rhs) { - lhs.pushValue(); + lhs.pushValue(lhs.getLuaState()); convert::pushValue(lhs.getLuaState(), rhs); bool result = lua_lessthan(lhs.getLuaState(), -2, -1) != 0; diff --git a/code/scripting/scripting.h b/code/scripting/scripting.h index 3bae7b19a6..a3122a333c 100644 --- a/code/scripting/scripting.h +++ b/code/scripting/scripting.h @@ -392,7 +392,7 @@ bool script_state::EvalStringWithReturn(const char* string, const char* format, auto stack_start = lua_gettop(LuaState); auto val = ret.front(); - val.pushValue(); + val.pushValue(LuaState); scripting::internal::Ade_get_args_skip = stack_start; scripting::internal::Ade_get_args_lfunction = true; @@ -430,7 +430,7 @@ int script_state::RunBytecode(script_function& hd, char format, T* data) auto stack_start = lua_gettop(LuaState); auto val = ret.front(); - val.pushValue(); + val.pushValue(LuaState); char fmt[2] = {format, '\0'}; scripting::internal::Ade_get_args_skip = stack_start; diff --git a/code/source_groups.cmake b/code/source_groups.cmake index c68007b5aa..7c12240557 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1060,11 +1060,17 @@ add_file_folder("Scripting" ) add_file_folder("Scripting\\\\Api" + scripting/api/LuaCoroutineRunner.cpp + scripting/api/LuaCoroutineRunner.h scripting/api/LuaEventCallback.cpp scripting/api/LuaEventCallback.h + scripting/api/LuaPromise.cpp + scripting/api/LuaPromise.h ) add_file_folder("Scripting\\\\Api\\\\Libs" + scripting/api/libs/async.cpp + scripting/api/libs/async.h scripting/api/libs/audio.cpp scripting/api/libs/audio.h scripting/api/libs/base.cpp @@ -1158,6 +1164,8 @@ add_file_folder("Scripting\\\\Api\\\\Objs" scripting/api/objs/physics_info.h scripting/api/objs/player.cpp scripting/api/objs/player.h + scripting/api/objs/promise.cpp + scripting/api/objs/promise.h scripting/api/objs/sexpvar.cpp scripting/api/objs/sexpvar.h scripting/api/objs/shields.cpp @@ -1211,6 +1219,9 @@ add_file_folder("Scripting\\\\Lua" scripting/lua/LuaReference.h scripting/lua/LuaTable.cpp scripting/lua/LuaTable.h + scripting/lua/LuaThread.cpp + scripting/lua/LuaThread.h + scripting/lua/LuaTypes.h scripting/lua/LuaUtil.cpp scripting/lua/LuaUtil.h scripting/lua/LuaValue.cpp diff --git a/test/src/scripting/ade_args.cpp b/test/src/scripting/ade_args.cpp index 2d2999c4c3..f925345e95 100644 --- a/test/src/scripting/ade_args.cpp +++ b/test/src/scripting/ade_args.cpp @@ -13,7 +13,7 @@ class AdeArgsTest : public LuaStateTest { TEST_F(AdeArgsTest, GetFunctionArg) { auto stack_func = LuaFunction::createFromCode(L, "return 1"); - stack_func.pushValue(); + stack_func.pushValue(L); scripting::internal::Ade_get_args_lfunction = true; LuaFunction func; @@ -23,7 +23,7 @@ TEST_F(AdeArgsTest, GetFunctionArg) { TEST_F(AdeArgsTest, GetTableArg) { auto stack_table = LuaTable::create(L); - stack_table.pushValue(); + stack_table.pushValue(L); scripting::internal::Ade_get_args_lfunction = true; LuaTable table; diff --git a/test/src/scripting/api/async.cpp b/test/src/scripting/api/async.cpp new file mode 100644 index 0000000000..d41485ac18 --- /dev/null +++ b/test/src/scripting/api/async.cpp @@ -0,0 +1,37 @@ + +#include "scripting/ScriptingTestFixture.h" + +class ScriptingAsyncTest : public test::scripting::ScriptingTestFixture { + public: + ScriptingAsyncTest() : test::scripting::ScriptingTestFixture(INIT_CFILE) { pushModDir("async"); } +}; + +class ScriptingAsyncPromiseTest : public ScriptingAsyncTest { + public: + ScriptingAsyncPromiseTest() { pushModDir("promise"); } +}; + +TEST_F(ScriptingAsyncPromiseTest, callWithNonFunction) +{ + ASSERT_THROW(this->EvalTestScript(), os::dialogs::LuaErrorException); +} + +TEST_F(ScriptingAsyncPromiseTest, resolveWithValues) +{ + this->EvalTestScript(); +} + +class ScriptingAsyncAsyncRunTest : public ScriptingAsyncTest { + public: + ScriptingAsyncAsyncRunTest() { pushModDir("run"); } +}; + +TEST_F(ScriptingAsyncAsyncRunTest, runWithoutAwait) +{ + this->EvalTestScript(); +} + +TEST_F(ScriptingAsyncAsyncRunTest, runWithResolvedAwait) +{ + this->EvalTestScript(); +} diff --git a/test/src/scripting/lua/Function.cpp b/test/src/scripting/lua/Function.cpp index 6ac0428707..7d6c8c5857 100644 --- a/test/src/scripting/lua/Function.cpp +++ b/test/src/scripting/lua/Function.cpp @@ -209,18 +209,18 @@ TEST_F(LuaFunctionTest, ErrorFunctionNoReturnValues) { try { func(); FAIL(); - } - catch (const LuaException& err) { + } catch (const LuaException& err) { ASSERT_STREQ("Invalid lua value on stack!", err.what()); } } -TEST_F(LuaFunctionTest, Upvalues) { +TEST_F(LuaFunctionTest, Upvalues) +{ ScopedLuaStackTest stackTest(L); LuaValue upval = LuaValue::createValue(L, "UpvalueTest"); - LuaFunction func = LuaFunction::createFromCFunction(L, upvalueTest, { upval }); + LuaFunction func = LuaFunction::createFromCFunction(L, upvalueTest, {upval}); auto ret = func(); @@ -228,3 +228,26 @@ TEST_F(LuaFunctionTest, Upvalues) { ASSERT_TRUE(ret.front().is(ValueType::BOOLEAN)); ASSERT_TRUE(ret.front().getValue()); } + +TEST_F(LuaFunctionTest, CreateFromStdFunction) +{ + ScopedLuaStackTest stackTest(L); + + const auto stdFunc = [](lua_State* luaState, const LuaValueList& params) { + EXPECT_EQ(2, static_cast(params.size())); + + EXPECT_EQ(42, params[0].getValue()); + EXPECT_EQ("TestString", params[1].getValue()); + + return LuaValueList{LuaValue::createValue(luaState, "StringTest"), LuaValue::createValue(luaState, 1234)}; + }; + + const auto stdFuncObj = LuaFunction::createFromStdFunction(L, stdFunc); + + const auto testRet = stdFuncObj({LuaValue::createValue(L, 42), LuaValue::createValue(L, "TestString")}); + + EXPECT_EQ(2, static_cast(testRet.size())); + + EXPECT_EQ("StringTest", testRet[0].getValue()); + EXPECT_EQ(1234, testRet[1].getValue()); +} diff --git a/test/src/scripting/lua/Reference.cpp b/test/src/scripting/lua/Reference.cpp index 4462bd5fe7..a68ea3e803 100644 --- a/test/src/scripting/lua/Reference.cpp +++ b/test/src/scripting/lua/Reference.cpp @@ -24,7 +24,7 @@ TEST_F(LuaReferenceTest, Create) { // Remove the two values again lua_pop(L, 2); - ref->pushValue(); + ref->pushValue(L); ASSERT_TRUE(lua_isboolean(L, -1) == 1); ASSERT_TRUE(lua_toboolean(L, -1) == 1); @@ -81,7 +81,7 @@ TEST_F(LuaReferenceTest, PushValue) { lua_pop(L, 1); - refPtr->pushValue(); + refPtr->pushValue(L); ASSERT_TRUE(lua_isboolean(L, -1) == 1); ASSERT_EQ(1, lua_toboolean(L, -1)); diff --git a/test/src/scripting/lua/Table.cpp b/test/src/scripting/lua/Table.cpp index d8aa84ecd4..15552e7aa3 100644 --- a/test/src/scripting/lua/Table.cpp +++ b/test/src/scripting/lua/Table.cpp @@ -15,7 +15,7 @@ TEST_F(LuaTableTest, AddValue) { LuaTable table = LuaTable::create(L); table.addValue("key", "value"); - ASSERT_TRUE(table.pushValue()); + ASSERT_TRUE(table.pushValue(L)); lua_getfield(L, -1, "key"); diff --git a/test/src/scripting/lua/TestUtil.h b/test/src/scripting/lua/TestUtil.h index 07be58a04b..6fb68e73e9 100644 --- a/test/src/scripting/lua/TestUtil.h +++ b/test/src/scripting/lua/TestUtil.h @@ -11,12 +11,12 @@ class LuaStateTest: public testing::Test { public: lua_State* L; - virtual void SetUp() override { + void SetUp() override { L = luaL_newstate(); luaL_openlibs(L); } - virtual void TearDown() override { + void TearDown() override { lua_close(L); } }; diff --git a/test/src/scripting/lua/Thread.cpp b/test/src/scripting/lua/Thread.cpp new file mode 100644 index 0000000000..c6e9b99c18 --- /dev/null +++ b/test/src/scripting/lua/Thread.cpp @@ -0,0 +1,63 @@ + +#include "TestUtil.h" + +#include "scripting/lua/LuaFunction.h" +#include "scripting/lua/LuaThread.h" + +using namespace luacpp; + +class LuaThreadTest : public LuaStateTest { +}; + +TEST_F(LuaThreadTest, CreateYieldFinish) +{ + ScopedLuaStackTest stackTest(L); + + const auto mainFunc = LuaFunction::createFromCode(L, R"( +local yield_val = coroutine.yield("Test") +return yield_val +)"); + + const auto thread = LuaThread::create(L, mainFunc); + + // Initial start + const auto initialResult = thread.resume(LuaValueList()); + + ASSERT_FALSE(initialResult.completed); + + ASSERT_EQ(1, static_cast(initialResult.returnVals.size())); + ASSERT_EQ("Test", initialResult.returnVals.front().getValue()); + + const auto resumeResult = thread.resume({LuaValue::createValue(L, "TestValue")}); + + ASSERT_TRUE(resumeResult.completed); + + ASSERT_EQ(1, static_cast(resumeResult.returnVals.size())); + ASSERT_EQ("TestValue", resumeResult.returnVals.front().getValue()); +} + +TEST_F(LuaThreadTest, OnErrorCallbackIsCalled) +{ + ScopedLuaStackTest stackTest(L); + + const auto mainFunc = LuaFunction::createFromCode(L, R"( +local val = nil +coroutine.yield(val.test) +)"); + + auto thread = LuaThread::create(L, mainFunc); + bool errorCalled = false; + thread.setErrorCallback([&errorCalled](lua_State* /*mainState*/, lua_State* /*thread*/) { + errorCalled = true; + return true; + }); + + // Initial start + const auto initialResult = thread.resume(LuaValueList()); + + ASSERT_TRUE(initialResult.completed); + + ASSERT_EQ(0, static_cast(initialResult.returnVals.size())); + + ASSERT_TRUE(errorCalled); +} diff --git a/test/src/source_groups.cmake b/test/src/source_groups.cmake index fe8bec35ec..4bc3c1afe8 100644 --- a/test/src/source_groups.cmake +++ b/test/src/source_groups.cmake @@ -44,6 +44,7 @@ add_file_folder("Scripting" ) add_file_folder("Scripting\\\\API" + scripting/api/async.cpp scripting/api/base.cpp scripting/api/bitops.cpp scripting/api/enums.cpp @@ -56,6 +57,7 @@ add_file_folder("Scripting\\\\Lua" scripting/lua/Reference.cpp scripting/lua/Table.cpp scripting/lua/TestUtil.h + scripting/lua/Thread.cpp scripting/lua/Util.cpp scripting/lua/Value.cpp ) diff --git a/test/test_data/scripting/async/promise/callWithNonFunction/data/scripts/test.lua b/test/test_data/scripting/async/promise/callWithNonFunction/data/scripts/test.lua new file mode 100644 index 0000000000..eb263e9bfe --- /dev/null +++ b/test/test_data/scripting/async/promise/callWithNonFunction/data/scripts/test.lua @@ -0,0 +1,2 @@ + +async.promise("Test") diff --git a/test/test_data/scripting/async/promise/resolveWithValues/data/scripts/test.lua b/test/test_data/scripting/async/promise/resolveWithValues/data/scripts/test.lua new file mode 100644 index 0000000000..6248a5f09b --- /dev/null +++ b/test/test_data/scripting/async/promise/resolveWithValues/data/scripts/test.lua @@ -0,0 +1,42 @@ + +function pack(...) + return {n = select("#", ...), ...} +end + +local resolve_func; +local lazy_test_val = "Test" + +local promise = async.promise(function (resolve) + lazy_test_val = "NotTest" + resolve_func = resolve +end) + +assert(not promise:isResolved()) + +-- Check the expected behavior that the function is evaluated eagerly +assert(lazy_test_val == "NotTest") + +local continuationCalled = false +local contPromise = promise:continueWith(function(testVal) + assert(testVal == "ResolveValue") + continuationCalled = true + + return 42, "Test" +end) + +assert(not contPromise:isResolved()) + +-- Now kick off the chain +resolve_func("ResolveValue") + +assert(continuationCalled) + +assert(promise:isResolved()) +assert(promise:getValue() == "ResolveValue") + +assert(contPromise:isResolved()) +local vals = pack(contPromise:getValue()) +assert(#vals == 2) +assert(vals[1] == 42) +assert(vals[2] == "Test") + diff --git a/test/test_data/scripting/async/run/runWithAwait/data/scripts/test.lua b/test/test_data/scripting/async/run/runWithAwait/data/scripts/test.lua new file mode 100644 index 0000000000..fda4fae805 --- /dev/null +++ b/test/test_data/scripting/async/run/runWithAwait/data/scripts/test.lua @@ -0,0 +1,66 @@ + +function pack(...) + return {n = select("#", ...), ...} +end + +local assert = require("assert") + +local promise1_cb; +local promise1 = async.promise(function(resolve) + promise1_cb = resolve; +end) + +local promise2_cb; +local promise2 = async.promise(function(resolve) + promise2_cb = resolve; +end) + +local start_reached = false +local promise1_reached = false +local promise2_reached = false + +local asyncPromise = async.run(function() + start_reached = true + + local promise1Val = async.await(promise1) + promise1_reached = true + assert.equals("Promise1", promise1Val) + + local promise2Vals = pack(async.await(promise2)) + promise2_reached = true + assert.equals(3, #promise2Vals) + assert.equals("Test", promise2Vals[1]) + assert.equals(nil, promise2Vals[2]) + assert.equals(1234, promise2Vals[3]) + + return "AsyncReturn", 42 +end) + +-- Check progress status +assert.isTrue(start_reached) +assert.isFalse(promise1_reached) +assert.isFalse(promise2_reached) +assert.isFalse(asyncPromise:isResolved()) + +-- Resolve the first promise +promise1_cb("Promise1") + +-- Check progress status +assert.isTrue(start_reached) +assert.isTrue(promise1_reached) +assert.isFalse(promise2_reached) +assert.isFalse(asyncPromise:isResolved()) + +-- Resolve the second promise +promise2_cb("Test", nil, 1234) + +-- Check progress status +assert.isTrue(start_reached) +assert.isTrue(promise1_reached) +assert.isTrue(promise2_reached) +assert.isTrue(asyncPromise:isResolved()) + +local retVals = pack(asyncPromise:getValue()) +assert.equals(2, #retVals) +assert.equals("AsyncReturn", retVals[1]) +assert.equals(42, retVals[2]) diff --git a/test/test_data/scripting/async/run/runWithResolvedAwait/data/scripts/test.lua b/test/test_data/scripting/async/run/runWithResolvedAwait/data/scripts/test.lua new file mode 100644 index 0000000000..236a5f4895 --- /dev/null +++ b/test/test_data/scripting/async/run/runWithResolvedAwait/data/scripts/test.lua @@ -0,0 +1,42 @@ + +function pack(...) + return {n = select("#", ...), ...} +end + +local assert = require("assert") + +local promise1 = async.resolved("Promise1") + +local promise2 = async.resolved("Test", nil, 1234) + +local start_reached = false +local promise1_reached = false +local promise2_reached = false + +local asyncPromise = async.run(function() + start_reached = true + + local promise1Val = async.await(promise1) + promise1_reached = true + assert.equals("Promise1", promise1Val) + + local promise2Vals = pack(async.await(promise2)) + promise2_reached = true + assert.equals(3, #promise2Vals) + assert.equals("Test", promise2Vals[1]) + assert.equals(nil, promise2Vals[2]) + assert.equals(1234, promise2Vals[3]) + + return "ResolvedReturn", 42 +end) + +-- Check progress status. Since promises are already resolved this will resolve synchronously. +assert.isTrue(start_reached) +assert.isTrue(promise1_reached) +assert.isTrue(promise2_reached) +assert.isTrue(asyncPromise:isResolved()) + +local retVals = pack(asyncPromise:getValue()) +assert.equals(2, #retVals) +assert.equals("ResolvedReturn", retVals[1]) +assert.equals(42, retVals[2]) diff --git a/test/test_data/scripting/async/run/runWithoutAwait/data/scripts/test.lua b/test/test_data/scripting/async/run/runWithoutAwait/data/scripts/test.lua new file mode 100644 index 0000000000..2dc274a01f --- /dev/null +++ b/test/test_data/scripting/async/run/runWithoutAwait/data/scripts/test.lua @@ -0,0 +1,13 @@ + +local assert = require("assert") + +local function_was_run = false + +local asyncPromise = async.run(function() + function_was_run = true + return "SyncReturn" +end) + +assert.isTrue(function_was_run) +assert.isTrue(asyncPromise:isResolved()) +assert.equals("SyncReturn", asyncPromise:getValue()) diff --git a/test/test_data/scripting/data/scripts/assert.lua b/test/test_data/scripting/data/scripts/assert.lua new file mode 100644 index 0000000000..920676eb38 --- /dev/null +++ b/test/test_data/scripting/data/scripts/assert.lua @@ -0,0 +1,19 @@ +local module = {} + +function module.equals(expected, actual) + if (expected == actual) then + return + end + + error(string.format("Expected value of %q but was %q", tostring(expected), tostring(actual))) +end + +function module.isTrue(value) + module.equals(true, value) +end + +function module.isFalse(value) + module.equals(false, value) +end + +return module