From ebcf686cf7a0bf5ce0f81c1d271fc00ea90f2dfd Mon Sep 17 00:00:00 2001 From: asarium Date: Sun, 17 May 2020 13:22:05 +0200 Subject: [PATCH] Add Lua API for promises and async coroutines This adds a Lua API for representing asynchronous operations and running coroutines based on those operations comfortably. This is very similar to features other languages have introduced such as C# or JavaScript. In fact, the whole "promise" thing is derived from how JavaScript represents these operations. This is the initial and very basic system for supporting async coroutines in Lua. There is no native support for running these operations at specific points (they run whenever the awaited promise resolved) or making sure they do not leak through different game states. --- .clang-format | 4 +- code/scripting/ade_args.cpp | 17 +-- code/scripting/api/LuaCoroutineRunner.cpp | 82 +++++++++++ code/scripting/api/LuaCoroutineRunner.h | 22 +++ code/scripting/api/LuaPromise.cpp | 120 ++++++++++++++++ code/scripting/api/LuaPromise.h | 72 ++++++++++ code/scripting/api/libs/async.cpp | 136 ++++++++++++++++++ code/scripting/api/libs/async.h | 11 ++ code/scripting/api/objs/promise.cpp | 93 ++++++++++++ code/scripting/api/objs/promise.h | 12 ++ code/scripting/lua.cpp | 7 + code/scripting/lua/LuaFunction.cpp | 88 ++++++++++-- code/scripting/lua/LuaFunction.h | 23 ++- code/scripting/lua/LuaReference.cpp | 29 ++-- code/scripting/lua/LuaReference.h | 3 +- code/scripting/lua/LuaTable.cpp | 12 +- code/scripting/lua/LuaTable.h | 4 +- code/scripting/lua/LuaThread.cpp | 120 ++++++++++++++++ code/scripting/lua/LuaThread.h | 76 ++++++++++ code/scripting/lua/LuaTypes.h | 7 + code/scripting/lua/LuaUtil.cpp | 49 +++++-- code/scripting/lua/LuaUtil.h | 16 ++- code/scripting/lua/LuaValue.cpp | 50 +++---- code/scripting/lua/LuaValue.h | 66 +++++---- code/scripting/scripting.h | 4 +- code/source_groups.cmake | 11 ++ test/src/scripting/ade_args.cpp | 4 +- test/src/scripting/api/async.cpp | 37 +++++ test/src/scripting/lua/Function.cpp | 31 +++- test/src/scripting/lua/Reference.cpp | 4 +- test/src/scripting/lua/Table.cpp | 2 +- test/src/scripting/lua/TestUtil.h | 4 +- test/src/scripting/lua/Thread.cpp | 63 ++++++++ test/src/source_groups.cmake | 2 + .../callWithNonFunction/data/scripts/test.lua | 2 + .../resolveWithValues/data/scripts/test.lua | 42 ++++++ .../run/runWithAwait/data/scripts/test.lua | 66 +++++++++ .../data/scripts/test.lua | 42 ++++++ .../run/runWithoutAwait/data/scripts/test.lua | 13 ++ .../scripting/data/scripts/assert.lua | 19 +++ 40 files changed, 1342 insertions(+), 123 deletions(-) create mode 100644 code/scripting/api/LuaCoroutineRunner.cpp create mode 100644 code/scripting/api/LuaCoroutineRunner.h create mode 100644 code/scripting/api/LuaPromise.cpp create mode 100644 code/scripting/api/LuaPromise.h create mode 100644 code/scripting/api/libs/async.cpp create mode 100644 code/scripting/api/libs/async.h create mode 100644 code/scripting/api/objs/promise.cpp create mode 100644 code/scripting/api/objs/promise.h create mode 100644 code/scripting/lua/LuaThread.cpp create mode 100644 code/scripting/lua/LuaThread.h create mode 100644 code/scripting/lua/LuaTypes.h create mode 100644 test/src/scripting/api/async.cpp create mode 100644 test/src/scripting/lua/Thread.cpp create mode 100644 test/test_data/scripting/async/promise/callWithNonFunction/data/scripts/test.lua create mode 100644 test/test_data/scripting/async/promise/resolveWithValues/data/scripts/test.lua create mode 100644 test/test_data/scripting/async/run/runWithAwait/data/scripts/test.lua create mode 100644 test/test_data/scripting/async/run/runWithResolvedAwait/data/scripts/test.lua create mode 100644 test/test_data/scripting/async/run/runWithoutAwait/data/scripts/test.lua create mode 100644 test/test_data/scripting/data/scripts/assert.lua 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