diff --git a/binaries/data/mods/_test.scriptinterface/promises/simple.js b/binaries/data/mods/_test.scriptinterface/promises/simple.js new file mode 100644 index 0000000000..7f4dcd4a92 --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/promises/simple.js @@ -0,0 +1,30 @@ +let test = 0; + +function incrementTest() +{ + test += 1; +} + +async function waitAndIncrement(promise) +{ + await promise; + incrementTest(); +} + +{ + let resolve; + const promise = new Promise(res => { + incrementTest(); + resolve = res; + }); + waitAndIncrement(promise); + TS_ASSERT_EQUALS(test, 1); + resolve(); + // At this point, waitAndIncrement is still not run, but is now free to run. + TS_ASSERT_EQUALS(test, 1); +} + +function endTest() +{ + TS_ASSERT_EQUALS(test, 2); +} diff --git a/source/scriptinterface/Promises.cpp b/source/scriptinterface/Promises.cpp new file mode 100644 index 0000000000..a871bc5c39 --- /dev/null +++ b/source/scriptinterface/Promises.cpp @@ -0,0 +1,117 @@ +/* Copyright (C) 2024 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "Promises.h" + +#include "lib/debug.h" +#include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptRequest.h" + +namespace Script +{ + +void UnhandledRejectedPromise(JSContext* cx, bool, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, void*) +{ + if (state == JS::PromiseRejectionHandlingState::Handled) + return; + + const ScriptRequest rq{cx}; + JS::RootedValue reason(cx, JS::GetPromiseResult(promise)); + + std::string asString; + ScriptFunction::Call(rq, reason, "toString", asString); + std::string stack; + Script::GetProperty(rq, reason, "stack", stack); + LOGERROR("An unhandled promise got rejected:\n%s\n%s", asString, stack); +} + +void JobQueue::runJobs(JSContext*) +{ + while (!m_Jobs.empty()) + { + QueueElement& element = m_Jobs.front(); + ScriptRequest rq{element.scriptInterface}; + JS::RootedObject localJob{rq.cx, element.job}; + m_Jobs.pop(); + + JS::RootedValue globV{rq.cx, rq.globalValue()}; + JS::RootedValue rval{rq.cx}; + JS::Call(rq.cx, globV, localJob, JS::HandleValueArray::empty(), &rval); + } +} + +JSObject* JobQueue::getIncumbentGlobal(JSContext* cx) +{ + return JS::CurrentGlobalOrNull(cx); +} + +bool JobQueue::enqueuePromiseJob(JSContext* cx, JS::HandleObject, JS::HandleObject job, JS::HandleObject, + JS::HandleObject) +{ + try + { + m_Jobs.push({ScriptRequest{cx}.GetScriptInterface(), JS::PersistentRootedObject{cx, job}}); + return true; + } + catch (...) + { + return false; + } +} + +bool JobQueue::empty() const +{ + return m_Jobs.empty(); +} + +js::UniquePtr JobQueue::saveJobQueue(JSContext*) +{ + class SavedJobQueue : public JS::JobQueue::SavedJobQueue + { + public: + SavedJobQueue(QueueType& queue) : + externQueue{queue}, + internQueue{std::move(queue)} + {} + + ~SavedJobQueue() final + { + ENSURE(externQueue.empty()); + externQueue = std::move(internQueue); + } + + private: + QueueType& externQueue; + QueueType internQueue; + }; + + try + { + return js::MakeUnique(m_Jobs); + } + catch (...) + { + return nullptr; + } +} + +} // namespace Script diff --git a/source/scriptinterface/Promises.h b/source/scriptinterface/Promises.h new file mode 100644 index 0000000000..6e64da5d54 --- /dev/null +++ b/source/scriptinterface/Promises.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2024 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_SCRIPTINTERFACE_JOBQUEUE +#define INCLUDED_SCRIPTINTERFACE_JOBQUEUE + +#if MSC_VERSION +# pragma warning(push, 1) +#endif +#include "js/Promise.h" +#if MSC_VERSION +# pragma warning(pop) +#endif + +#include + +class ScriptInterface; + +namespace Script +{ +void UnhandledRejectedPromise(JSContext* cx, bool, JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, void*); + +class JobQueue : public JS::JobQueue +{ +public: + ~JobQueue() final = default; + + void runJobs(JSContext*) final; + +private: + JSObject* getIncumbentGlobal(JSContext* cx) final; + + bool enqueuePromiseJob(JSContext* cx, JS::HandleObject, JS::HandleObject job, JS::HandleObject, + JS::HandleObject) final; + + bool empty() const final; + + js::UniquePtr saveJobQueue(JSContext*) final; + + struct QueueElement + { + const ScriptInterface& scriptInterface; + JS::PersistentRootedObject job; + }; + using QueueType = std::queue; + QueueType m_Jobs; +}; +} + +#endif // INCLUDED_SCRIPTINTERFACE_JOBQUEUE diff --git a/source/scriptinterface/tests/test_Promises.h b/source/scriptinterface/tests/test_Promises.h new file mode 100644 index 0000000000..2b8aa716a9 --- /dev/null +++ b/source/scriptinterface/tests/test_Promises.h @@ -0,0 +1,55 @@ +/* Copyright (C) 2024 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/Promises.h" +#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptContext.h" +#include "scriptinterface/StructuredClone.h" + +class TestPromises : public CxxTest::TestSuite +{ +public: + void test_simple_promises() + { + ScriptInterface script("Engine", "Test", g_ScriptContext); + ScriptTestSetup(script); + TS_ASSERT(script.LoadGlobalScriptFile(L"promises/simple.js")); + g_ScriptContext->RunJobs(); + + ScriptRequest rq(script); + JS::RootedValue global(rq.cx, rq.globalValue()); + ScriptFunction::CallVoid(rq, global, "endTest"); + } + + void setUp() + { + g_VFS = CreateVfs(); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.scriptinterface" / "", VFS_MOUNT_MUST_EXIST)); + } + + void tearDown() + { + g_VFS.reset(); + } +};