From ce01bdddf69ac50714ee076240720b7dc4e59632 Mon Sep 17 00:00:00 2001 From: phosit Date: Wed, 15 Jan 2025 21:01:23 +0100 Subject: [PATCH] Allow to check whether modules finished evaluating --- .../module/delayed_blabbermouth.js | 3 + .../module/top_level_await_finite.js | 2 + .../module/top_level_await_infinite.js | 1 + source/scriptinterface/ModuleLoader.cpp | 78 +++++++++++++- source/scriptinterface/ModuleLoader.h | 33 +++++- source/scriptinterface/tests/test_Module.h | 101 ++++++++++++++++-- 6 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 binaries/data/mods/_test.scriptinterface/module/delayed_blabbermouth.js create mode 100644 binaries/data/mods/_test.scriptinterface/module/top_level_await_finite.js create mode 100644 binaries/data/mods/_test.scriptinterface/module/top_level_await_infinite.js diff --git a/binaries/data/mods/_test.scriptinterface/module/delayed_blabbermouth.js b/binaries/data/mods/_test.scriptinterface/module/delayed_blabbermouth.js new file mode 100644 index 0000000000..49a726bd92 --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/module/delayed_blabbermouth.js @@ -0,0 +1,3 @@ +await undefined; +// `import "blabbermouth.js";` would be hoisted before the await resulting in it not being delayed. +log("blah blah blah"); diff --git a/binaries/data/mods/_test.scriptinterface/module/top_level_await_finite.js b/binaries/data/mods/_test.scriptinterface/module/top_level_await_finite.js new file mode 100644 index 0000000000..1381466aec --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/module/top_level_await_finite.js @@ -0,0 +1,2 @@ +await undefined; +await undefined; diff --git a/binaries/data/mods/_test.scriptinterface/module/top_level_await_infinite.js b/binaries/data/mods/_test.scriptinterface/module/top_level_await_infinite.js new file mode 100644 index 0000000000..231a8cd634 --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/module/top_level_await_infinite.js @@ -0,0 +1 @@ +await new Promise(() => {}); diff --git a/source/scriptinterface/ModuleLoader.cpp b/source/scriptinterface/ModuleLoader.cpp index 73b23ae867..b42320760f 100644 --- a/source/scriptinterface/ModuleLoader.cpp +++ b/source/scriptinterface/ModuleLoader.cpp @@ -22,6 +22,7 @@ #include "ps/CStr.h" #include "ps/Filesystem.h" #include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" @@ -63,7 +64,7 @@ namespace return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject; } -void Evaluate(const ScriptRequest& rq, JS::HandleObject mod) +[[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod) { if (!JS::ModuleLink(rq.cx, mod)) { @@ -72,12 +73,33 @@ void Evaluate(const ScriptRequest& rq, JS::HandleObject mod) } JS::RootedValue val{rq.cx}; - if (!JS::ModuleEvaluate(rq.cx, mod, &val)) + if (!JS::ModuleEvaluate(rq.cx, mod, &val) || !val.isObject()) { ScriptException::CatchPending(rq); throw std::invalid_argument{"Unable to evaluate module."}; } + + return &val.toObject(); } + +bool Call(JSContext* cx, const unsigned argc, JS::Value* vp) +{ + JS::CallArgs args{JS::CallArgsFromVp(argc, vp)}; + const ScriptRequest rq{cx}; + + const auto statusPtr{JS::GetMaybePtrFromReservedSlot( + &args.callee(), 0)}; + if (!statusPtr) + return true; + + (*statusPtr) = ModuleLoader::Future::Fulfilled{}; + return true; +} + +constexpr JSClassOps callbackClassOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + /*call =*/Call, nullptr, nullptr}; + +constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps}; } // anonymous namespace ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsPath& filePath): @@ -103,10 +125,56 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP } } -void ModuleLoader::LoadModule(const ScriptRequest& rq, const VfsPath& modulePath) +ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath): + m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass)}}} { - JS::RootedObject mod{rq.cx, CompileModule(rq, m_Registry, modulePath)}; - Evaluate(rq, mod); + JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)}; + JS::RootedObject promise{rq.cx, Evaluate(rq, mod)}; + + SetReservedSlot(JS::PrivateValue(static_cast(&m_Status))); + + if (!JS::AddPromiseReactions(rq.cx, promise, std::get(m_Status).fulfill, nullptr)) + throw std::runtime_error{"Failed adding promise reaction."}; +} + +ModuleLoader::Future::Future(Future&& other) noexcept: + m_Status{std::exchange(other.m_Status, Invalid{})} +{ + SetReservedSlot(JS::PrivateValue(static_cast(&m_Status))); +} + +ModuleLoader::Future& ModuleLoader::Future::operator=(Future&& other) noexcept +{ + SetReservedSlot(JS::UndefinedValue()); + m_Status = std::exchange(other.m_Status, Invalid{}); + SetReservedSlot(JS::PrivateValue(static_cast(&m_Status))); + + return *this; +} + +ModuleLoader::Future::~Future() +{ + SetReservedSlot(JS::UndefinedValue()); +} + +[[nodiscard]] bool ModuleLoader::Future::IsDone() const noexcept +{ + return std::holds_alternative(m_Status); +} + +void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept +{ + Evaluating* evaluatingStatus{std::get_if(&m_Status)}; + if (!evaluatingStatus) + return; + if (evaluatingStatus->fulfill) + JS::SetReservedSlot(evaluatingStatus->fulfill, 0, privateValue); +} + +[[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq, + const VfsPath& modulePath) +{ + return Future{rq, *this, modulePath}; } [[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue, diff --git a/source/scriptinterface/ModuleLoader.h b/source/scriptinterface/ModuleLoader.h index 314cb8c5cc..f114f13018 100644 --- a/source/scriptinterface/ModuleLoader.h +++ b/source/scriptinterface/ModuleLoader.h @@ -22,6 +22,7 @@ #include "scriptinterface/ScriptTypes.h" #include +#include class ScriptContext; class ScriptRequest; @@ -42,13 +43,43 @@ public: using RegistryType = std::unordered_map; + class Future + { + public: + struct Evaluating + { + JS::PersistentRootedObject fulfill; + }; + struct Fulfilled {}; + struct Invalid {}; + using Status = std::variant; + + explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath); + Future() = default; + Future(const Future&) = delete; + Future& operator=(const Future&) = delete; + Future(Future&& other) noexcept; + Future& operator=(Future&& other) noexcept; + ~Future(); + + [[nodiscard]] bool IsDone() const noexcept; + + private: + // It's save to not require a `JS::HandleValue` here. + void SetReservedSlot(JS::Value privateValue) noexcept; + + Status m_Status{Invalid{}}; + }; + /** * Load the specified module and all module it imports recursively. * * @param rq @c globalThis is taken from this @c ScriptRequest. * @param modulePath The path to the file which should be loaded as a module. + * @return A future that is fulfilled when the evaluation of the module + * completes. */ - void LoadModule(const ScriptRequest& rq, const VfsPath& modulePath); + [[nodiscard]] Future LoadModule(const ScriptRequest& rq, const VfsPath& modulePath); private: // Functions used by the `ScriptContext`. diff --git a/source/scriptinterface/tests/test_Module.h b/source/scriptinterface/tests/test_Module.h index ad63974c4a..88e015b3e6 100644 --- a/source/scriptinterface/tests/test_Module.h +++ b/source/scriptinterface/tests/test_Module.h @@ -20,6 +20,7 @@ #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "scriptinterface/ModuleLoader.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" class TestScriptModule : public CxxTest::TestSuite @@ -43,7 +44,7 @@ public: const ScriptRequest rq{script}; TestLogger logger; - script.GetModuleLoader().LoadModule(rq, "include/entry.js"); + std::ignore = script.GetModuleLoader().LoadModule(rq, "include/entry.js"); // This test does not rely on export to the engine. So use the logger to check if it succeeded. TS_ASSERT_STR_CONTAINS(logger.GetOutput(),"Test succeeded"); @@ -54,12 +55,12 @@ public: { ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - script.GetModuleLoader().LoadModule(rq, "empty.js"); + std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js"); } { ScriptInterface script{"Test", "Test", g_ScriptContext}; const ScriptRequest rq{script}; - script.GetModuleLoader().LoadModule(rq, "empty.js"); + std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js"); } } @@ -70,9 +71,9 @@ public: { ScriptInterface scriptInner{"Test", "Test", g_ScriptContext}; const ScriptRequest rqInner{scriptInner}; - scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js"); + std::ignore = scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js"); } - scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js"); + std::ignore = scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js"); } void test_ImportInFunction() @@ -81,8 +82,8 @@ public: const ScriptRequest rq{script}; TestLogger logger; - TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "import_inside_function.js"), - const std::invalid_argument&); + TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq, + "import_inside_function.js"), const std::invalid_argument&); const std::string log{logger.GetOutput()}; TS_ASSERT_STR_CONTAINS(log, "import_inside_function.js line 3"); TS_ASSERT_STR_CONTAINS(log, "import declarations may only appear at top level of a module"); @@ -94,7 +95,7 @@ public: const ScriptRequest rq{script}; const TestLogger _; - TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "nonexistent.js"), + TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq, "nonexistent.js"), const std::runtime_error&); } @@ -105,13 +106,93 @@ public: { TestLogger logger; - script.GetModuleLoader().LoadModule(rq, "blabbermouth.js"); + std::ignore = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js"); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); } { TestLogger logger; - script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js"); + std::ignore = script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js"); TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah"); } } + + void test_TopLevelAwaitFinite() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js"); + + TS_ASSERT(!future.IsDone()); + g_ScriptContext->RunJobs(); + TS_ASSERT(future.IsDone()); + } + + void test_TopLevelAwaitInfinite() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js"); + + g_ScriptContext->RunJobs(); + TS_ASSERT(!future.IsDone()); + } + + void test_MoveFulfilledFuture() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + Script::ModuleLoader::Future future0{ + script.GetModuleLoader().LoadModule(rq, "empty.js")}; + + g_ScriptContext->RunJobs(); + TS_ASSERT(future0.IsDone()); + + Script::ModuleLoader::Future future1{std::move(future0)}; + Script::ModuleLoader::Future future2; + future2 = std::move(future1); + + TS_ASSERT(!future0.IsDone()); + TS_ASSERT(!future1.IsDone()); + TS_ASSERT(future2.IsDone()); + } + + void test_MoveEvaluatingFuture() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + Script::ModuleLoader::Future future0{ + script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")}; + Script::ModuleLoader::Future future1{std::move(future0)}; + Script::ModuleLoader::Future future2; + future2 = std::move(future1); + + TS_ASSERT(!future0.IsDone()); + TS_ASSERT(!future1.IsDone()); + TS_ASSERT(!future2.IsDone()); + g_ScriptContext->RunJobs(); + TS_ASSERT(!future0.IsDone()); + TS_ASSERT(!future1.IsDone()); + TS_ASSERT(future2.IsDone()); + } + + void test_EvaluateReplacedFuture() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + TestLogger logger; + auto future{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")}; + TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah"); + TS_ASSERT(!future.IsDone()); + + future = script.GetModuleLoader().LoadModule(rq, "empty.js"); + TS_ASSERT(!future.IsDone()); + + g_ScriptContext->RunJobs(); + TS_ASSERT(future.IsDone()); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); + } };