summaryrefslogtreecommitdiffstats
path: root/js/src/wasm/WasmPI.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:29 +0000
commit59203c63bb777a3bacec32fb8830fba33540e809 (patch)
tree58298e711c0ff0575818c30485b44a2f21bf28a0 /js/src/wasm/WasmPI.cpp
parentAdding upstream version 126.0.1. (diff)
downloadfirefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz
firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/wasm/WasmPI.cpp')
-rw-r--r--js/src/wasm/WasmPI.cpp1743
1 files changed, 1743 insertions, 0 deletions
diff --git a/js/src/wasm/WasmPI.cpp b/js/src/wasm/WasmPI.cpp
new file mode 100644
index 0000000000..d11db9022d
--- /dev/null
+++ b/js/src/wasm/WasmPI.cpp
@@ -0,0 +1,1743 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ *
+ * Copyright 2016 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "wasm/WasmPI.h"
+
+#include "builtin/Promise.h"
+#include "jit/MIRGenerator.h"
+#include "js/CallAndConstruct.h"
+#include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/NativeObject.h"
+#include "vm/PromiseObject.h"
+#include "wasm/WasmConstants.h"
+#include "wasm/WasmContext.h"
+#include "wasm/WasmFeatures.h"
+#include "wasm/WasmGenerator.h"
+#include "wasm/WasmValidate.h"
+
+#include "vm/JSObject-inl.h"
+#include "wasm/WasmGcObject-inl.h"
+#include "wasm/WasmInstance-inl.h"
+
+#ifdef XP_WIN
+# include "util/WindowsWrapper.h"
+#endif
+
+using namespace js;
+using namespace js::jit;
+
+#ifdef ENABLE_WASM_JSPI
+namespace js::wasm {
+
+SuspenderObjectData::SuspenderObjectData(void* stackMemory)
+ : stackMemory_(stackMemory),
+ suspendableFP_(nullptr),
+ suspendableSP_(static_cast<uint8_t*>(stackMemory) +
+ SuspendableStackPlusRedZoneSize),
+ state_(SuspenderState::Initial) {}
+
+# ifdef _WIN64
+// On WIN64, the Thread Information Block stack limits has to be modified to
+// avoid failures on SP checks.
+void SuspenderObjectData::updateTIBStackFields() {
+ _NT_TIB* tib = reinterpret_cast<_NT_TIB*>(::NtCurrentTeb());
+ savedStackBase_ = tib->StackBase;
+ savedStackLimit_ = tib->StackLimit;
+ uintptr_t stack_limit = (uintptr_t)stackMemory_ + SuspendableRedZoneSize;
+ uintptr_t stack_base = stack_limit + SuspendableStackSize;
+ tib->StackBase = (void*)stack_base;
+ tib->StackLimit = (void*)stack_limit;
+}
+
+void SuspenderObjectData::restoreTIBStackFields() {
+ _NT_TIB* tib = reinterpret_cast<_NT_TIB*>(::NtCurrentTeb());
+ tib->StackBase = savedStackBase_;
+ tib->StackLimit = savedStackLimit_;
+}
+# endif
+
+// Slots that used in various JSFunctionExtended below.
+const size_t SUSPENDER_SLOT = 0;
+const size_t WRAPPED_FN_SLOT = 1;
+const size_t CONTINUE_ON_SUSPENDABLE_SLOT = 1;
+
+SuspenderContext::SuspenderContext()
+ : activeSuspender_(nullptr), suspendedStacks_() {}
+
+SuspenderContext::~SuspenderContext() {
+ MOZ_ASSERT(activeSuspender_ == nullptr);
+}
+
+SuspenderObject* SuspenderContext::activeSuspender() {
+ return activeSuspender_;
+}
+
+void SuspenderContext::setActiveSuspender(SuspenderObject* obj) {
+ activeSuspender_.set(obj);
+}
+
+void SuspenderContext::trace(JSTracer* trc) {
+ if (activeSuspender_) {
+ TraceEdge(trc, &activeSuspender_, "suspender");
+ }
+}
+
+void SuspenderContext::traceRoots(JSTracer* trc) {
+ for (const SuspenderObjectData& data : suspendedStacks_) {
+ void* startFP = data.suspendableFP();
+ void* returnAddress = data.suspendedReturnAddress();
+ void* exitFP = data.suspendableExitFP();
+ MOZ_ASSERT(startFP != exitFP);
+
+ WasmFrameIter iter(static_cast<FrameWithInstances*>(startFP),
+ returnAddress);
+ MOZ_ASSERT(iter.stackSwitched());
+ uintptr_t highestByteVisitedInPrevWasmFrame = 0;
+ while (true) {
+ MOZ_ASSERT(!iter.done());
+ uint8_t* nextPC = iter.resumePCinCurrentFrame();
+ Instance* instance = iter.instance();
+ TraceInstanceEdge(trc, instance, "WasmFrameIter instance");
+ highestByteVisitedInPrevWasmFrame = instance->traceFrame(
+ trc, iter, nextPC, highestByteVisitedInPrevWasmFrame);
+ if (iter.frame() == exitFP) {
+ break;
+ }
+ ++iter;
+ if (iter.stackSwitched()) {
+ highestByteVisitedInPrevWasmFrame = 0;
+ }
+ }
+ }
+}
+
+static_assert(JS_STACK_GROWTH_DIRECTION < 0,
+ "JS-PI implemented only for native stacks that grows towards 0");
+
+static void DecrementSuspendableStacksCount(JSContext* cx) {
+ for (;;) {
+ uint32_t currentCount = cx->wasm().suspendableStacksCount;
+ MOZ_ASSERT(currentCount > 0);
+ if (cx->wasm().suspendableStacksCount.compareExchange(currentCount,
+ currentCount - 1)) {
+ break;
+ }
+ // Failed to decrement suspendableStacksCount, repeat.
+ }
+}
+
+class SuspenderObject : public NativeObject {
+ public:
+ static const JSClass class_;
+
+ enum { DataSlot, PromisingPromiseSlot, SuspendingPromiseSlot, SlotCount };
+
+ static SuspenderObject* create(JSContext* cx) {
+ for (;;) {
+ uint32_t currentCount = cx->wasm().suspendableStacksCount;
+ if (currentCount >= SuspendableStacksMaxCount) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_SUSPENDER_LIMIT);
+ return nullptr;
+ }
+ if (cx->wasm().suspendableStacksCount.compareExchange(currentCount,
+ currentCount + 1)) {
+ break;
+ }
+ // Failed to increment suspendableStacksCount, repeat.
+ }
+
+ Rooted<SuspenderObject*> suspender(
+ cx, NewBuiltinClassInstance<SuspenderObject>(cx));
+ if (!suspender) {
+ DecrementSuspendableStacksCount(cx);
+ return nullptr;
+ }
+
+ void* stackMemory = js_malloc(SuspendableStackPlusRedZoneSize);
+ if (!stackMemory) {
+ DecrementSuspendableStacksCount(cx);
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ SuspenderObjectData* data = js_new<SuspenderObjectData>(stackMemory);
+ if (!data) {
+ js_free(stackMemory);
+ DecrementSuspendableStacksCount(cx);
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_RELEASE_ASSERT(data->state() != SuspenderState::Moribund);
+
+ suspender->initReservedSlot(DataSlot, PrivateValue(data));
+ suspender->initReservedSlot(PromisingPromiseSlot, NullValue());
+ suspender->initReservedSlot(SuspendingPromiseSlot, NullValue());
+ return suspender;
+ }
+
+ PromiseObject* promisingPromise() const {
+ return &getReservedSlot(PromisingPromiseSlot)
+ .toObject()
+ .as<PromiseObject>();
+ }
+
+ void setPromisingPromise(Handle<PromiseObject*> promise) {
+ setReservedSlot(PromisingPromiseSlot, ObjectOrNullValue(promise));
+ }
+
+ PromiseObject* suspendingPromise() const {
+ return &getReservedSlot(SuspendingPromiseSlot)
+ .toObject()
+ .as<PromiseObject>();
+ }
+
+ void setSuspendingPromise(Handle<PromiseObject*> promise) {
+ setReservedSlot(SuspendingPromiseSlot, ObjectOrNullValue(promise));
+ }
+
+ JS::NativeStackLimit getStackMemoryLimit() {
+ return JS::NativeStackLimit(data()->stackMemory()) + SuspendableRedZoneSize;
+ }
+
+ SuspenderState state() { return data()->state(); }
+
+ inline bool hasData() { return !getReservedSlot(DataSlot).isUndefined(); }
+
+ inline SuspenderObjectData* data() {
+ return static_cast<SuspenderObjectData*>(
+ getReservedSlot(DataSlot).toPrivate());
+ }
+
+ void setMoribund(JSContext* cx);
+ void setActive(JSContext* cx);
+ void setSuspended(JSContext* cx);
+
+ void enter(JSContext* cx);
+ void suspend(JSContext* cx);
+ void resume(JSContext* cx);
+ void leave(JSContext* cx);
+
+ private:
+ static const JSClassOps classOps_;
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+};
+
+static_assert(SuspenderObjectDataSlot == SuspenderObject::DataSlot);
+
+const JSClass SuspenderObject::class_ = {
+ "SuspenderObject",
+ JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_BACKGROUND_FINALIZE,
+ &SuspenderObject::classOps_};
+
+const JSClassOps SuspenderObject::classOps_ = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+/* static */
+void SuspenderObject::finalize(JS::GCContext* gcx, JSObject* obj) {
+ SuspenderObject& suspender = obj->as<SuspenderObject>();
+ if (!suspender.hasData()) {
+ return;
+ }
+ SuspenderObjectData* data = suspender.data();
+ MOZ_RELEASE_ASSERT(data->state() == SuspenderState::Moribund);
+ js_free(data->stackMemory());
+ js_free(data);
+}
+
+void SuspenderObject::setMoribund(JSContext* cx) {
+ // TODO make sense to free stackMemory at this point to reduce memory usage?
+ MOZ_ASSERT(state() == SuspenderState::Active);
+ ResetInstanceStackLimits(cx);
+# ifdef _WIN64
+ data()->restoreTIBStackFields();
+# endif
+ SuspenderObjectData* data = this->data();
+ data->setState(SuspenderState::Moribund);
+ DecrementSuspendableStacksCount(cx);
+ MOZ_ASSERT(
+ !cx->wasm().promiseIntegration.suspendedStacks_.ElementProbablyInList(
+ data));
+}
+
+void SuspenderObject::setActive(JSContext* cx) {
+ data()->setState(SuspenderState::Active);
+ UpdateInstanceStackLimitsForSuspendableStack(cx, getStackMemoryLimit());
+# ifdef _WIN64
+ data()->updateTIBStackFields();
+# endif
+}
+
+void SuspenderObject::setSuspended(JSContext* cx) {
+ data()->setState(SuspenderState::Suspended);
+ ResetInstanceStackLimits(cx);
+# ifdef _WIN64
+ data()->restoreTIBStackFields();
+# endif
+}
+
+void SuspenderObject::enter(JSContext* cx) {
+ MOZ_ASSERT(state() == SuspenderState::Initial);
+ cx->wasm().promiseIntegration.setActiveSuspender(this);
+ setActive(cx);
+}
+
+void SuspenderObject::suspend(JSContext* cx) {
+ MOZ_ASSERT(state() == SuspenderState::Active);
+ setSuspended(cx);
+ cx->wasm().promiseIntegration.suspendedStacks_.pushFront(data());
+ cx->wasm().promiseIntegration.setActiveSuspender(nullptr);
+}
+
+void SuspenderObject::resume(JSContext* cx) {
+ MOZ_ASSERT(state() == SuspenderState::Suspended);
+ cx->wasm().promiseIntegration.setActiveSuspender(this);
+ setActive(cx);
+ cx->wasm().promiseIntegration.suspendedStacks_.remove(data());
+}
+
+void SuspenderObject::leave(JSContext* cx) {
+ cx->wasm().promiseIntegration.setActiveSuspender(nullptr);
+ // We are exiting alternative stack if state is active,
+ // otherwise the stack was just suspended.
+ switch (state()) {
+ case SuspenderState::Active:
+ setMoribund(cx);
+ break;
+ case SuspenderState::Suspended:
+ break;
+ case SuspenderState::Initial:
+ case SuspenderState::Moribund:
+ MOZ_CRASH();
+ }
+}
+
+bool ParseSuspendingPromisingString(JSContext* cx, HandleValue val,
+ SuspenderArgPosition& result) {
+ if (val.isNullOrUndefined()) {
+ result = SuspenderArgPosition::None;
+ return true;
+ }
+
+ RootedString str(cx, ToString(cx, val));
+ if (!str) {
+ return false;
+ }
+ Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx));
+ if (!linear) {
+ return false;
+ }
+
+ if (StringEqualsLiteral(linear, "first")) {
+ result = SuspenderArgPosition::First;
+ } else if (StringEqualsLiteral(linear, "last")) {
+ result = SuspenderArgPosition::Last;
+ } else {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_ARG_POSITION);
+ return false;
+ }
+ return true;
+}
+
+using CallImportData = Instance::WasmJSPICallImportData;
+
+/*static*/
+bool CallImportData::Call(CallImportData* data) {
+ Instance* instance = data->instance;
+ JSContext* cx = instance->cx();
+ return instance->callImport(cx, data->funcImportIndex, data->argc,
+ data->argv);
+}
+
+bool CallImportOnMainThread(JSContext* cx, Instance* instance,
+ int32_t funcImportIndex, int32_t argc,
+ uint64_t* argv) {
+ CallImportData data = {instance, funcImportIndex, argc, argv};
+ Rooted<SuspenderObject*> suspender(
+ cx, cx->wasm().promiseIntegration.activeSuspender());
+ SuspenderObjectData* stacks = suspender->data();
+
+ cx->wasm().promiseIntegration.setActiveSuspender(nullptr);
+
+ MOZ_ASSERT(suspender->state() == SuspenderState::Active);
+ suspender->setSuspended(cx);
+
+ // The platform specific code below inserts offsets as strings into inline
+ // assembly. CHECK_OFFSETS verifies the specified literals in macros below.
+# define CHECK_OFFSETS(MAIN_FP_OFFSET, MAIN_SP_OFFSET, SUSPENDABLE_FP_OFFSET, \
+ SUSPENDABLE_SP_OFFSET) \
+ static_assert((MAIN_FP_OFFSET) == SuspenderObjectData::offsetOfMainFP() && \
+ (MAIN_SP_OFFSET) == SuspenderObjectData::offsetOfMainSP() && \
+ (SUSPENDABLE_FP_OFFSET) == \
+ SuspenderObjectData::offsetOfSuspendableFP() && \
+ (SUSPENDABLE_SP_OFFSET) == \
+ SuspenderObjectData::offsetOfSuspendableSP());
+
+ // The following assembly code temporarily switches FP/SP pointers to be on
+ // main stack, while maintaining frames linking. After
+ // `CallImportData::Call` execution suspendable stack FP/SP will be restored.
+ //
+ // Because the assembly sequences contain a call, the trashed-register list
+ // must contain all the caller saved registers. They must also contain "cc"
+ // and "memory" since both of those state elements could be modified by the
+ // call. They also need a "volatile" qualifier to ensure that the they don't
+ // get optimised out or otherwise messed with by clang/gcc.
+ //
+ // `Registers::VolatileMask` (in the assembler complex) is useful in that it
+ // lists the caller-saved registers.
+
+ uintptr_t res;
+
+ // clang-format off
+#if defined(_M_ARM64) || defined(__aarch64__)
+# define CALLER_SAVED_REGS \
+ "x0", "x1", "x2", "x3","x4", "x5", "x6", "x7", "x8", "x9", "x10", \
+ "x11", "x12", "x13", "x14", "x15", "x16", "x17", /* "x18", */ \
+ "x19", "x20", /* it's unclear who saves these two, so be safe */ \
+ /* claim that all the vector regs are caller-saved, for safety */ \
+ "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", \
+ "v11", "v12", "v13", "v14", "v15", "v16", "v17", "v18", "v19", \
+ "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", \
+ "v29", "v30", "v31"
+# define INLINED_ASM(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP) \
+ CHECK_OFFSETS(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP); \
+ asm volatile( \
+ "\n mov x0, %1" \
+ "\n mov x27, sp " \
+ "\n str x29, [x0, #" #SUSPENDABLE_FP "] " \
+ "\n str x27, [x0, #" #SUSPENDABLE_SP "] " \
+ \
+ "\n ldr x29, [x0, #" #MAIN_FP "] " \
+ "\n ldr x27, [x0, #" #MAIN_SP "] " \
+ "\n mov sp, x27 " \
+ \
+ "\n stp x0, x27, [sp, #-16]! " \
+ \
+ "\n mov x0, %3" \
+ "\n blr %2 " \
+ \
+ "\n ldp x3, x27, [sp], #16 " \
+ \
+ "\n mov x27, sp " \
+ "\n str x29, [x3, #" #MAIN_FP "] " \
+ "\n str x27, [x3, #" #MAIN_SP "] " \
+ \
+ "\n ldr x29, [x3, #" #SUSPENDABLE_FP "] " \
+ "\n ldr x27, [x3, #" #SUSPENDABLE_SP "] " \
+ "\n mov sp, x27 " \
+ "\n mov %0, x0" \
+ : "=r"(res) \
+ : "r"(stacks), "r"(CallImportData::Call), "r"(&data) \
+ : "x0", "x3", "x27", CALLER_SAVED_REGS, "cc", "memory")
+ INLINED_ASM(24, 32, 40, 48);
+
+# elif defined(_WIN64) && defined(_M_X64)
+# define INLINED_ASM(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP) \
+ CHECK_OFFSETS(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP); \
+ asm("\n mov %1, %%rcx" \
+ "\n mov %%rbp, " #SUSPENDABLE_FP "(%%rcx)" \
+ "\n mov %%rsp, " #SUSPENDABLE_SP "(%%rcx)" \
+ \
+ "\n mov " #MAIN_FP "(%%rcx), %%rbp" \
+ "\n mov " #MAIN_SP "(%%rcx), %%rsp" \
+ \
+ "\n push %%rcx" \
+ "\n push %%rdx" \
+ \
+ "\n mov %3, %%rcx" \
+ "\n call *%2" \
+ \
+ "\n pop %%rdx" \
+ "\n pop %%rcx" \
+ \
+ "\n mov %%rbp, " #MAIN_FP "(%%rcx)" \
+ "\n mov %%rsp, " #MAIN_SP "(%%rcx)" \
+ \
+ "\n mov " #SUSPENDABLE_FP "(%%rcx), %%rbp" \
+ "\n mov " #SUSPENDABLE_SP "(%%rcx), %%rsp" \
+ \
+ "\n movq %%rax, %0" \
+ : "=r"(res) \
+ : "r"(stacks), "r"(CallImportData::Call), "r"(&data) \
+ : "rcx", "rax")
+ INLINED_ASM(24, 32, 40, 48);
+
+# elif defined(__x86_64__)
+# define INLINED_ASM(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP) \
+ CHECK_OFFSETS(MAIN_FP, MAIN_SP, SUSPENDABLE_FP, SUSPENDABLE_SP); \
+ asm("\n mov %1, %%rdi" \
+ "\n mov %%rbp, " #SUSPENDABLE_FP "(%%rdi)" \
+ "\n mov %%rsp, " #SUSPENDABLE_SP "(%%rdi)" \
+ \
+ "\n mov " #MAIN_FP "(%%rdi), %%rbp" \
+ "\n mov " #MAIN_SP "(%%rdi), %%rsp" \
+ \
+ "\n push %%rdi" \
+ "\n push %%rdx" \
+ \
+ "\n mov %3, %%rdi" \
+ "\n call *%2" \
+ \
+ "\n pop %%rdx" \
+ "\n pop %%rdi" \
+ \
+ "\n mov %%rbp, " #MAIN_FP "(%%rdi)" \
+ "\n mov %%rsp, " #MAIN_SP "(%%rdi)" \
+ \
+ "\n mov " #SUSPENDABLE_FP "(%%rdi), %%rbp" \
+ "\n mov " #SUSPENDABLE_SP "(%%rdi), %%rsp" \
+ \
+ "\n movq %%rax, %0" \
+ : "=r"(res) \
+ : "r"(stacks), "r"(CallImportData::Call), "r"(&data) \
+ : "rdi", "rax")
+ INLINED_ASM(24, 32, 40, 48);
+
+# else
+ MOZ_CRASH("Not supported for this platform");
+# endif
+ // clang-format on
+
+ bool ok = res;
+ suspender->setActive(cx);
+ cx->wasm().promiseIntegration.setActiveSuspender(suspender);
+
+# undef INLINED_ASM
+# undef CHECK_OFFSETS
+# undef CALLER_SAVED_REGS
+
+ return ok;
+}
+
+void UnwindStackSwitch(JSContext* cx) {
+ SuspenderObject* suspender = cx->wasm().promiseIntegration.activeSuspender();
+ MOZ_ASSERT(suspender);
+ cx->wasm().promiseIntegration.setActiveSuspender(nullptr);
+ suspender->setMoribund(cx);
+}
+
+// Suspending
+
+// Builds a wasm module with following structure:
+// (module
+// (type $params (struct (field ..)*)))
+// (type $results (struct (field ..)*)))
+// (import "" "" (func $suspending.wrappedfn ..))
+// (import "" "" (func $suspending.add-promise-reactions ..))
+// (import "" "" (func $suspending.get-suspending-promise-result ..))
+// (func $suspending.exported .. )
+// (func $suspending.trampoline ..)
+// (func $suspending.continue-on-suspendable ..)
+// (export "" (func $suspending.exported))
+// )
+class SuspendingFunctionModuleFactory {
+ public:
+ enum TypeIdx {
+ ParamsTypeIndex,
+ ResultsTypeIndex,
+ };
+
+ enum FnIdx {
+ WrappedFnIndex,
+ GetSuspendingResultsFnIndex,
+ ExportedFnIndex,
+ TrampolineFnIndex,
+ ContinueOnSuspendableFnIndex
+ };
+
+ private:
+ // Builds function that will be imported to wasm module:
+ // (func $suspending.exported
+ // (param $suspender externref)? (param ..)* (param $suspender externref)?
+ // (result ..)*
+ // (local $suspender externref)?
+ // (local $results (ref $results))
+ // ;; #if checkSuspender
+ // local.get $suspender
+ // call $builtin.check-suspender
+ // ;; #else
+ // call $builtin.current-suspender
+ // local.tee $suspender
+ // ;; #endif
+ // ref.func $suspending.trampoline
+ // local.get $i*
+ // stuct.new $param-type
+ // stack-switch SwitchToMain ;; <- (suspender,fn,data)
+ // local.get $suspender
+ // call $builtin.get-suspending-promise-result
+ // ref.cast $results-type
+ // local.set $results
+ // (struct.get $results (local.get $results))*
+ // )
+ bool encodeExportedFunction(ModuleEnvironment& moduleEnv, uint32_t paramsSize,
+ uint32_t resultSize, uint32_t paramsOffset,
+ uint32_t suspenderIndex, bool checkSuspender,
+ RefType resultType, Bytes& bytecode) {
+ Encoder encoder(bytecode, *moduleEnv.types);
+ ValTypeVector locals;
+ if (!checkSuspender && !locals.emplaceBack(RefType::extern_())) {
+ return false;
+ }
+ if (!locals.emplaceBack(resultType)) {
+ return false;
+ }
+ if (!EncodeLocalEntries(encoder, locals)) {
+ return false;
+ }
+
+ if (checkSuspender) {
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(suspenderIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(MozOp::CallBuiltinModuleFunc) ||
+ !encoder.writeVarU32((uint32_t)BuiltinModuleFuncId::CheckSuspender)) {
+ return false;
+ }
+ } else {
+ if (!encoder.writeOp(Op::I32Const) || !encoder.writeVarU32(0)) {
+ return false;
+ }
+ if (!encoder.writeOp(MozOp::CallBuiltinModuleFunc) ||
+ !encoder.writeVarU32(
+ (uint32_t)BuiltinModuleFuncId::CurrentSuspender)) {
+ return false;
+ }
+ if (!encoder.writeOp(Op::LocalTee) ||
+ !encoder.writeVarU32(suspenderIndex)) {
+ return false;
+ }
+ }
+
+ // Results local is located after all params and suspender.
+ // Adding 1 to paramSize for checkSuspender and ! cases.
+ int resultsIndex = paramsSize + 1;
+
+ if (!encoder.writeOp(Op::RefFunc) ||
+ !encoder.writeVarU32(TrampolineFnIndex)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < paramsSize; i++) {
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(i + paramsOffset)) {
+ return false;
+ }
+ }
+ if (!encoder.writeOp(GcOp::StructNew) ||
+ !encoder.writeVarU32(ParamsTypeIndex)) {
+ return false;
+ }
+
+ if (!encoder.writeOp(MozOp::StackSwitch) ||
+ !encoder.writeVarU32(uint32_t(StackSwitchKind::SwitchToMain))) {
+ return false;
+ }
+
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(suspenderIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(MozOp::CallBuiltinModuleFunc) ||
+ !encoder.writeVarU32(
+ (uint32_t)BuiltinModuleFuncId::GetSuspendingPromiseResult)) {
+ return false;
+ }
+ if (!encoder.writeOp(GcOp::RefCast) ||
+ !encoder.writeVarU32(ResultsTypeIndex) ||
+ !encoder.writeOp(Op::LocalSet) || !encoder.writeVarU32(resultsIndex)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < resultSize; i++) {
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(resultsIndex) ||
+ !encoder.writeOp(GcOp::StructGet) ||
+ !encoder.writeVarU32(ResultsTypeIndex) || !encoder.writeVarU32(i)) {
+ return false;
+ }
+ }
+ return encoder.writeOp(Op::End);
+ }
+
+ // Builds function that is called on main stack:
+ // (func $suspending.trampoline
+ // (param $params (ref $suspender) (ref $param-type))
+ // (result externref)
+ // local.get $suspender ;; for call $process-promise
+ // (struct.get $param-type $i (local.get $param))*
+ // call $suspending.wrappedfn
+ // ref.func $suspending.continue-on-suspendable
+ // call $suspending.add-promise-reactions
+ // )
+ // The function calls suspending import and returns into the
+ // $promising.exported function because that was the top function
+ // on the main stack.
+ bool encodeTrampolineFunction(ModuleEnvironment& moduleEnv,
+ uint32_t paramsSize, Bytes& bytecode) {
+ Encoder encoder(bytecode, *moduleEnv.types);
+ if (!EncodeLocalEntries(encoder, ValTypeVector())) {
+ return false;
+ }
+ const uint32_t SuspenderIndex = 0;
+ const uint32_t ParamsIndex = 1;
+
+ // For GetSuspendingResultsFnIndex call below.
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(SuspenderIndex)) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < paramsSize; i++) {
+ if (!encoder.writeOp(Op::LocalGet) || !encoder.writeVarU32(ParamsIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(GcOp::StructGet) ||
+ !encoder.writeVarU32(ParamsTypeIndex) || !encoder.writeVarU32(i)) {
+ return false;
+ }
+ }
+ if (!encoder.writeOp(Op::Call) || !encoder.writeVarU32(WrappedFnIndex)) {
+ return false;
+ }
+
+ if (!encoder.writeOp(Op::RefFunc) ||
+ !encoder.writeVarU32(ContinueOnSuspendableFnIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(Op::Call) ||
+ !encoder.writeVarU32(GetSuspendingResultsFnIndex)) {
+ return false;
+ }
+
+ return encoder.writeOp(Op::End);
+ }
+
+ // Builds function that is called on main stack:
+ // (func $suspending.continue-on-suspendable
+ // (param $params (ref $suspender))
+ // (result externref)
+ // local.get $suspender
+ // ref.null funcref
+ // ref.null anyref
+ // stack-switch ContinueOnSuspendable
+ // )
+ bool encodeContinueOnSuspendableFunction(ModuleEnvironment& moduleEnv,
+ uint32_t resultsSize,
+ Bytes& bytecode) {
+ Encoder encoder(bytecode, *moduleEnv.types);
+ if (!EncodeLocalEntries(encoder, ValTypeVector())) {
+ return false;
+ }
+
+ const uint32_t SuspenderIndex = 0;
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(SuspenderIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(Op::RefNull) ||
+ !encoder.writeValType(ValType(RefType::func()))) {
+ return false;
+ }
+ if (!encoder.writeOp(Op::RefNull) ||
+ !encoder.writeValType(ValType(RefType::any()))) {
+ return false;
+ }
+
+ if (!encoder.writeOp(MozOp::StackSwitch) ||
+ !encoder.writeVarU32(
+ uint32_t(StackSwitchKind::ContinueOnSuspendable))) {
+ return false;
+ }
+
+ return encoder.writeOp(Op::End);
+ }
+
+ public:
+ SharedModule build(JSContext* cx, HandleObject func, ValTypeVector&& params,
+ ValTypeVector&& results,
+ SuspenderArgPosition argPosition) {
+ FeatureOptions options;
+ options.isBuiltinModule = true;
+ options.requireGC = true;
+ options.requireTailCalls = true;
+
+ ScriptedCaller scriptedCaller;
+ SharedCompileArgs compileArgs =
+ CompileArgs::buildAndReport(cx, std::move(scriptedCaller), options);
+ if (!compileArgs) {
+ return nullptr;
+ }
+
+ ModuleEnvironment moduleEnv(compileArgs->features);
+ MOZ_ASSERT(IonAvailable(cx));
+ CompilerEnvironment compilerEnv(CompileMode::Once, Tier::Optimized,
+ DebugEnabled::False);
+ compilerEnv.computeParameters();
+
+ if (!moduleEnv.init()) {
+ return nullptr;
+ }
+
+ RefType suspenderType = RefType::extern_();
+ RefType promiseType = RefType::extern_();
+
+ size_t paramsSize, paramsOffset, suspenderIndex;
+ size_t resultsSize = results.length();
+ bool checkSuspender;
+ ValTypeVector paramsWithoutSuspender;
+ switch (argPosition) {
+ case SuspenderArgPosition::First:
+ paramsSize = params.length() - 1;
+ paramsOffset = 1;
+ suspenderIndex = 0;
+ if (!paramsWithoutSuspender.append(params.begin() + 1, params.end())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ checkSuspender = true;
+ break;
+ case SuspenderArgPosition::Last:
+ paramsSize = params.length() - 1;
+ paramsOffset = 0;
+ suspenderIndex = paramsSize - 1;
+ if (!paramsWithoutSuspender.append(params.begin(), params.end() - 1)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ checkSuspender = true;
+ break;
+ default:
+ paramsSize = params.length();
+ paramsOffset = 0;
+ suspenderIndex = paramsSize;
+ if (!paramsWithoutSuspender.append(params.begin(), params.end())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ checkSuspender = false;
+ break;
+ }
+ ValTypeVector resultsRef;
+ if (!resultsRef.emplaceBack(promiseType)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ StructType boxedParamsStruct;
+ if (!StructType::createImmutable(paramsWithoutSuspender,
+ &boxedParamsStruct)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.types->length() == ParamsTypeIndex);
+ if (!moduleEnv.types->addType(std::move(boxedParamsStruct))) {
+ return nullptr;
+ }
+
+ StructType boxedResultType;
+ if (!StructType::createImmutable(results, &boxedResultType)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.types->length() == ResultsTypeIndex);
+ if (!moduleEnv.types->addType(std::move(boxedResultType))) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(moduleEnv.funcs.length() == WrappedFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsWithoutSuspender),
+ std::move(resultsRef))) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsGetSuspendingResults, resultsGetSuspendingResults;
+ if (!paramsGetSuspendingResults.emplaceBack(suspenderType) ||
+ !paramsGetSuspendingResults.emplaceBack(promiseType) ||
+ !paramsGetSuspendingResults.emplaceBack(RefType::func())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == GetSuspendingResultsFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsGetSuspendingResults),
+ std::move(resultsGetSuspendingResults))) {
+ return nullptr;
+ }
+
+ // Imports names are not important, declare functions above as imports.
+ moduleEnv.numFuncImports = moduleEnv.funcs.length();
+
+ // We will be looking up and using the exports function by index so
+ // the name doesn't matter.
+ MOZ_ASSERT(moduleEnv.funcs.length() == ExportedFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(params), std::move(results),
+ /*declareForRef = */ true,
+ mozilla::Some(CacheableName()))) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsTrampoline, resultsTrampoline;
+ if (!paramsTrampoline.emplaceBack(suspenderType) ||
+ !paramsTrampoline.emplaceBack(RefType::fromTypeDef(
+ &(*moduleEnv.types)[ParamsTypeIndex], false))) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == TrampolineFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsTrampoline),
+ std::move(resultsTrampoline),
+ /*declareForRef = */ true)) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsContinueOnSuspendable, resultsContinueOnSuspendable;
+ if (!paramsContinueOnSuspendable.emplaceBack(suspenderType) ||
+ !paramsContinueOnSuspendable.emplaceBack(RefType::extern_())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == ContinueOnSuspendableFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsContinueOnSuspendable),
+ std::move(resultsContinueOnSuspendable),
+ /*declareForRef = */ true)) {
+ return nullptr;
+ }
+
+ ModuleGenerator mg(*compileArgs, &moduleEnv, &compilerEnv, nullptr, nullptr,
+ nullptr);
+ if (!mg.init(nullptr)) {
+ return nullptr;
+ }
+ // Build functions and keep bytecodes around until the end.
+ Bytes bytecode;
+ if (!encodeExportedFunction(
+ moduleEnv, paramsSize, resultsSize, paramsOffset, suspenderIndex,
+ checkSuspender,
+ RefType::fromTypeDef(&(*moduleEnv.types)[ResultsTypeIndex], false),
+ bytecode)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!mg.compileFuncDef(ExportedFnIndex, 0, bytecode.begin(),
+ bytecode.begin() + bytecode.length())) {
+ return nullptr;
+ }
+ Bytes bytecode2;
+ if (!encodeTrampolineFunction(moduleEnv, paramsSize, bytecode2)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!mg.compileFuncDef(TrampolineFnIndex, 0, bytecode2.begin(),
+ bytecode2.begin() + bytecode2.length())) {
+ return nullptr;
+ }
+ Bytes bytecode3;
+ if (!encodeContinueOnSuspendableFunction(moduleEnv, paramsSize,
+ bytecode3)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!mg.compileFuncDef(ContinueOnSuspendableFnIndex, 0, bytecode3.begin(),
+ bytecode3.begin() + bytecode3.length())) {
+ return nullptr;
+ }
+ if (!mg.finishFuncDefs()) {
+ return nullptr;
+ }
+
+ SharedBytes shareableBytes = js_new<ShareableBytes>();
+ if (!shareableBytes) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ return mg.finishModule(*shareableBytes);
+ }
+};
+
+// Reaction on resolved/rejected suspending promise.
+static bool WasmPISuspendTaskContinue(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<JSFunction*> callee(cx, &args.callee().as<JSFunction>());
+ RootedValue suspender(cx, callee->getExtendedSlot(SUSPENDER_SLOT));
+
+ // Convert result of the promise into the parameters/arguments for the
+ // $suspending.continue-on-suspendable.
+ RootedFunction continueOnSuspendable(
+ cx, &callee->getExtendedSlot(CONTINUE_ON_SUSPENDABLE_SLOT)
+ .toObject()
+ .as<JSFunction>());
+ RootedValueVector argv(cx);
+ if (!argv.emplaceBack(suspender)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ MOZ_ASSERT(args.length() > 0);
+ if (!argv.emplaceBack(args[0])) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ JS::Rooted<JS::Value> rval(cx);
+ if (Call(cx, UndefinedHandleValue, continueOnSuspendable, argv, &rval)) {
+ return true;
+ }
+
+ if (cx->isThrowingOutOfMemory()) {
+ return false;
+ }
+ Rooted<PromiseObject*> promise(
+ cx, suspender.toObject().as<SuspenderObject>().promisingPromise());
+ return RejectPromiseWithPendingError(cx, promise);
+}
+
+// Collects returned suspending promising, and registers callbacks to
+// react on it using WasmPISuspendTaskContinue.
+// Seen as $suspending.add-promise-reactions in wasm.
+static bool WasmPIAddPromiseReactions(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ Rooted<SuspenderObject*> suspenderObject(
+ cx, &args[0].toObject().as<SuspenderObject>());
+ RootedValue rval(cx, args[1]);
+ RootedFunction fn(cx, &args[2].toObject().as<JSFunction>());
+
+ MOZ_ASSERT(rval.toObject().is<PromiseObject>(),
+ "WasmPIWrapSuspendingImport always returning a promise");
+ Rooted<PromiseObject*> promise(cx, &rval.toObject().as<PromiseObject>());
+ suspenderObject->setSuspendingPromise(promise);
+
+ // Pass fn here
+ RootedFunction then_(
+ cx, NewNativeFunction(cx, WasmPISuspendTaskContinue, 1, nullptr,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ then_->initExtendedSlot(SUSPENDER_SLOT, ObjectValue(*suspenderObject));
+ then_->initExtendedSlot(CONTINUE_ON_SUSPENDABLE_SLOT, ObjectValue(*fn));
+ return AddPromiseReactions(cx, promise, then_, then_);
+}
+
+// Wraps original import to catch all exceptions and convert result to a
+// promise.
+// Seen as $suspending.wrappedfn in wasm.
+static bool WasmPIWrapSuspendingImport(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<JSFunction*> callee(cx, &args.callee().as<JSFunction>());
+ RootedFunction originalImportFunc(
+ cx,
+ &callee->getExtendedSlot(WRAPPED_FN_SLOT).toObject().as<JSFunction>());
+
+ // Catching exceptions here.
+ RootedValue rval(cx);
+ if (Call(cx, UndefinedHandleValue, originalImportFunc, args, &rval)) {
+ // Convert the result to a resolved promise.
+ RootedObject promiseConstructor(cx, GetPromiseConstructor(cx));
+ RootedObject promiseObj(cx, PromiseResolve(cx, promiseConstructor, rval));
+ if (!promiseObj) {
+ return false;
+ }
+ args.rval().setObject(*promiseObj);
+ return true;
+ }
+
+ if (cx->isThrowingOutOfMemory()) {
+ return false;
+ }
+
+ // Convert failure to a rejected promise.
+ RootedObject promiseObject(cx, NewPromiseObject(cx, nullptr));
+ if (!promiseObject) {
+ return false;
+ }
+ args.rval().setObject(*promiseObject);
+
+ Rooted<PromiseObject*> promise(cx, &promiseObject->as<PromiseObject>());
+ return RejectPromiseWithPendingError(cx, promise);
+}
+
+JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func,
+ ValTypeVector&& params,
+ ValTypeVector&& results,
+ SuspenderArgPosition argPosition) {
+ MOZ_ASSERT(IsCallable(ObjectValue(*func)) &&
+ !IsCrossCompartmentWrapper(func));
+
+ if (argPosition != SuspenderArgPosition::None) {
+ if (params.length() < 1 ||
+ params[argPosition == SuspenderArgPosition::Last ? params.length() - 1
+ : 0] !=
+ RefType::extern_()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_EXPECTED_SUSPENDER);
+ return nullptr;
+ }
+ }
+
+ SuspendingFunctionModuleFactory moduleFactory;
+ SharedModule module = moduleFactory.build(cx, func, std::move(params),
+ std::move(results), argPosition);
+ if (!module) {
+ return nullptr;
+ }
+
+ // Instantiate the module.
+ Rooted<ImportValues> imports(cx);
+
+ // Add $suspending.wrappedfn to imports.
+ RootedFunction funcWrapper(
+ cx, NewNativeFunction(cx, WasmPIWrapSuspendingImport, 0, nullptr,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ if (!funcWrapper) {
+ return nullptr;
+ }
+ funcWrapper->initExtendedSlot(WRAPPED_FN_SLOT, ObjectValue(*func));
+ if (!imports.get().funcs.append(funcWrapper)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ // Add $suspending.add-promise-reactions to imports.
+ RootedFunction addPromiseReactions(
+ cx, NewNativeFunction(cx, WasmPIAddPromiseReactions, 3, nullptr,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ if (!addPromiseReactions) {
+ return nullptr;
+ }
+ if (!imports.get().funcs.append(addPromiseReactions)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ Rooted<WasmInstanceObject*> instance(cx);
+ if (!module->instantiate(cx, imports.get(), nullptr, &instance)) {
+ // Can also trap on invalid input function.
+ return nullptr;
+ }
+
+ // Returns the $suspending.exported function.
+ RootedFunction wasmFunc(cx);
+ if (!WasmInstanceObject::getExportedFunction(
+ cx, instance, SuspendingFunctionModuleFactory::ExportedFnIndex,
+ &wasmFunc)) {
+ return nullptr;
+ }
+ return wasmFunc;
+}
+
+JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func,
+ const FuncType& type) {
+ ValTypeVector params, results;
+ if (!params.append(type.args().begin(), type.args().end()) ||
+ !results.append(type.results().begin(), type.results().end())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ return WasmSuspendingFunctionCreate(cx, func, std::move(params),
+ std::move(results),
+ SuspenderArgPosition::None);
+}
+
+// Promising
+
+// Builds a wasm module with following structure:
+// (module
+// (type $params (struct (field ..)*)))
+// (type $results (struct (field ..)*)))
+// (import "" "" (func $promising.wrappedfn ..))
+// (import "" "" (func $promising.create-suspender ..))
+// (func $promising.exported .. )
+// (func $promising.trampoline ..)
+// (export "" (func $promising.exported))
+// )
+class PromisingFunctionModuleFactory {
+ public:
+ enum TypeIdx {
+ ParamsTypeIndex,
+ ResultsTypeIndex,
+ };
+
+ enum FnIdx {
+ WrappedFnIndex,
+ CreateSuspenderFnIndex,
+ ExportedFnIndex,
+ TrampolineFnIndex,
+ };
+
+ private:
+ // Builds function that will be exported for JS:
+ // (func $promising.exported
+ // (param ..)* (result externref)
+ // (local $promise externref)
+ // call $promising.create-suspender ;; -> (suspender,promise)
+ // local.set $promise
+ // ref.func $promising.trampoline
+ // local.get $i*
+ // stuct.new $param-type
+ // stack-switch SwitchToSuspendable ;; <- (suspender,fn,data)
+ // local.get $promise
+ // )
+ bool encodeExportedFunction(ModuleEnvironment& moduleEnv, uint32_t paramsSize,
+ Bytes& bytecode) {
+ Encoder encoder(bytecode, *moduleEnv.types);
+ ValTypeVector locals;
+ if (!locals.emplaceBack(RefType::extern_())) {
+ return false;
+ }
+ if (!EncodeLocalEntries(encoder, locals)) {
+ return false;
+ }
+
+ const uint32_t PromiseIndex = paramsSize;
+ if (!encoder.writeOp(Op::Call) ||
+ !encoder.writeVarU32(CreateSuspenderFnIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(Op::LocalSet) || !encoder.writeVarU32(PromiseIndex)) {
+ return false;
+ }
+
+ if (!encoder.writeOp(Op::RefFunc) ||
+ !encoder.writeVarU32(TrampolineFnIndex)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < paramsSize; i++) {
+ if (!encoder.writeOp(Op::LocalGet) || !encoder.writeVarU32(i)) {
+ return false;
+ }
+ }
+ if (!encoder.writeOp(GcOp::StructNew) ||
+ !encoder.writeVarU32(ParamsTypeIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(MozOp::StackSwitch) ||
+ !encoder.writeVarU32(uint32_t(StackSwitchKind::SwitchToSuspendable))) {
+ return false;
+ }
+
+ if (!encoder.writeOp(Op::LocalGet) || !encoder.writeVarU32(PromiseIndex)) {
+ return false;
+ }
+ return encoder.writeOp(Op::End);
+ }
+
+ // Builds function that is called on alternative stack:
+ // (func $promising.trampoline
+ // (param $suspender externref) (param $params (ref $param-type))
+ // (result externref)
+ // local.get $suspender ;; for call $set-results
+ // (local.get $suspender)?
+ // (struct.get $param-type $i (local.get $param))*
+ // (local.get $suspender)?
+ // call $promising.wrappedfn
+ // struct.new $result-type
+ // call $builtin.set-promising-promise-results
+ // )
+ bool encodeTrampolineFunction(ModuleEnvironment& moduleEnv,
+ uint32_t paramsSize,
+ SuspenderArgPosition argPosition,
+ Bytes& bytecode) {
+ Encoder encoder(bytecode, *moduleEnv.types);
+ if (!EncodeLocalEntries(encoder, ValTypeVector())) {
+ return false;
+ }
+ const uint32_t SuspenderIndex = 0;
+ const uint32_t ParamsIndex = 1;
+
+ // Reserved for SetResultsFnIndex call at the end
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(SuspenderIndex)) {
+ return false;
+ }
+
+ if (argPosition == SuspenderArgPosition::First) {
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(SuspenderIndex)) {
+ return false;
+ }
+ }
+ for (uint32_t i = 0; i < paramsSize; i++) {
+ if (!encoder.writeOp(Op::LocalGet) || !encoder.writeVarU32(ParamsIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(GcOp::StructGet) ||
+ !encoder.writeVarU32(ParamsTypeIndex) || !encoder.writeVarU32(i)) {
+ return false;
+ }
+ }
+ if (argPosition == SuspenderArgPosition::Last) {
+ if (!encoder.writeOp(Op::LocalGet) ||
+ !encoder.writeVarU32(SuspenderIndex)) {
+ return false;
+ }
+ }
+ if (!encoder.writeOp(Op::Call) || !encoder.writeVarU32(WrappedFnIndex)) {
+ return false;
+ }
+
+ if (!encoder.writeOp(GcOp::StructNew) ||
+ !encoder.writeVarU32(ResultsTypeIndex)) {
+ return false;
+ }
+ if (!encoder.writeOp(MozOp::CallBuiltinModuleFunc) ||
+ !encoder.writeVarU32(
+ (uint32_t)BuiltinModuleFuncId::SetPromisingPromiseResults)) {
+ return false;
+ }
+
+ return encoder.writeOp(Op::End);
+ }
+
+ public:
+ SharedModule build(JSContext* cx, HandleFunction fn, ValTypeVector&& params,
+ ValTypeVector&& results,
+ SuspenderArgPosition argPosition) {
+ const FuncType& fnType = fn->wasmTypeDef()->funcType();
+ size_t paramsSize = params.length();
+
+ RefType suspenderType = RefType::extern_();
+ RefType promiseType = RefType::extern_();
+
+ FeatureOptions options;
+ options.isBuiltinModule = true;
+ options.requireGC = true;
+ options.requireTailCalls = true;
+
+ ScriptedCaller scriptedCaller;
+ SharedCompileArgs compileArgs =
+ CompileArgs::buildAndReport(cx, std::move(scriptedCaller), options);
+ if (!compileArgs) {
+ return nullptr;
+ }
+
+ ModuleEnvironment moduleEnv(compileArgs->features);
+ MOZ_ASSERT(IonAvailable(cx));
+ CompilerEnvironment compilerEnv(CompileMode::Once, Tier::Optimized,
+ DebugEnabled::False);
+ compilerEnv.computeParameters();
+
+ if (!moduleEnv.init()) {
+ return nullptr;
+ }
+
+ StructType boxedParamsStruct;
+ if (!StructType::createImmutable(params, &boxedParamsStruct)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.types->length() == ParamsTypeIndex);
+ if (!moduleEnv.types->addType(std::move(boxedParamsStruct))) {
+ return nullptr;
+ }
+
+ StructType boxedResultType;
+ if (!StructType::createImmutable(fnType.results(), &boxedResultType)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.types->length() == ResultsTypeIndex);
+ if (!moduleEnv.types->addType(std::move(boxedResultType))) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsForWrapper, resultsForWrapper;
+ if (!paramsForWrapper.append(fnType.args().begin(), fnType.args().end()) ||
+ !resultsForWrapper.append(fnType.results().begin(),
+ fnType.results().end())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == WrappedFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsForWrapper),
+ std::move(resultsForWrapper))) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsCreateSuspender, resultsCreateSuspender;
+ if (!resultsCreateSuspender.emplaceBack(suspenderType) ||
+ !resultsCreateSuspender.emplaceBack(promiseType)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == CreateSuspenderFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsCreateSuspender),
+ std::move(resultsCreateSuspender))) {
+ return nullptr;
+ }
+
+ // Imports names are not important, declare functions above as imports.
+ moduleEnv.numFuncImports = moduleEnv.funcs.length();
+
+ // We will be looking up and using the exports function by index so
+ // the name doesn't matter.
+ MOZ_ASSERT(moduleEnv.funcs.length() == ExportedFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(params), std::move(results),
+ /* declareFoRef = */ true,
+ mozilla::Some(CacheableName()))) {
+ return nullptr;
+ }
+
+ ValTypeVector paramsTrampoline, resultsTrampoline;
+ if (!paramsTrampoline.emplaceBack(suspenderType) ||
+ !paramsTrampoline.emplaceBack(RefType::fromTypeDef(
+ &(*moduleEnv.types)[ParamsTypeIndex], false))) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ MOZ_ASSERT(moduleEnv.funcs.length() == TrampolineFnIndex);
+ if (!moduleEnv.addDefinedFunc(std::move(paramsTrampoline),
+ std::move(resultsTrampoline),
+ /* declareFoRef = */ true)) {
+ return nullptr;
+ }
+
+ ModuleGenerator mg(*compileArgs, &moduleEnv, &compilerEnv, nullptr, nullptr,
+ nullptr);
+ if (!mg.init(nullptr)) {
+ return nullptr;
+ }
+ // Build functions and keep bytecodes around until the end.
+ Bytes bytecode;
+ if (!encodeExportedFunction(moduleEnv, paramsSize, bytecode)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!mg.compileFuncDef(ExportedFnIndex, 0, bytecode.begin(),
+ bytecode.begin() + bytecode.length())) {
+ return nullptr;
+ }
+ Bytes bytecode2;
+ if (!encodeTrampolineFunction(moduleEnv, paramsSize, argPosition,
+ bytecode2)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!mg.compileFuncDef(TrampolineFnIndex, 0, bytecode2.begin(),
+ bytecode2.begin() + bytecode2.length())) {
+ return nullptr;
+ }
+ if (!mg.finishFuncDefs()) {
+ return nullptr;
+ }
+
+ SharedBytes shareableBytes = js_new<ShareableBytes>();
+ if (!shareableBytes) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ return mg.finishModule(*shareableBytes);
+ }
+};
+
+// Creates a suspender and promise (that will be returned to JS code).
+// Seen as $promising.create-suspender to wasm.
+static bool WasmPICreateSuspender(JSContext* cx, unsigned argc, Value* vp) {
+ Rooted<SuspenderObject*> suspenderObject(cx, SuspenderObject::create(cx));
+ RootedObject promiseObject(cx, NewPromiseObject(cx, nullptr));
+ if (!promiseObject) {
+ return false;
+ }
+
+ Rooted<PromiseObject*> promise(cx, &promiseObject->as<PromiseObject>());
+ suspenderObject->setPromisingPromise(promise);
+
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<ArrayObject*> results(cx, NewDenseEmptyArray(cx));
+ if (!NewbornArrayPush(cx, results, ObjectValue(*suspenderObject))) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, results, ObjectValue(*promise))) {
+ return false;
+ }
+ args.rval().setObject(*results);
+ return true;
+}
+
+// Wraps call to wasm $promising.exported function to catch an exception and
+// return a promise instead.
+static bool WasmPIPromisingFunction(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<JSFunction*> callee(cx, &args.callee().as<JSFunction>());
+ RootedFunction fn(
+ cx,
+ &callee->getExtendedSlot(WRAPPED_FN_SLOT).toObject().as<JSFunction>());
+
+ // Catching exceptions here.
+ if (Call(cx, UndefinedHandleValue, fn, args, args.rval())) {
+ return true;
+ }
+
+ if (cx->isThrowingOutOfMemory()) {
+ return false;
+ }
+
+ RootedObject promiseObject(cx, NewPromiseObject(cx, nullptr));
+ if (!promiseObject) {
+ return false;
+ }
+ args.rval().setObject(*promiseObject);
+
+ Rooted<PromiseObject*> promise(cx, &promiseObject->as<PromiseObject>());
+ return RejectPromiseWithPendingError(cx, promise);
+}
+
+JSFunction* WasmPromisingFunctionCreate(JSContext* cx, HandleObject func,
+ ValTypeVector&& params,
+ ValTypeVector&& results,
+ SuspenderArgPosition argPosition) {
+ RootedFunction wrappedWasmFunc(cx, &func->as<JSFunction>());
+ MOZ_ASSERT(wrappedWasmFunc->isWasm());
+ const FuncType& wrappedWasmFuncType =
+ wrappedWasmFunc->wasmTypeDef()->funcType();
+
+ if (argPosition != SuspenderArgPosition::None) {
+ if (results.length() != 1 || results[0] != RefType::extern_()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_EXPECTED_PROMISE);
+ return nullptr;
+ }
+
+ size_t paramsOffset, suspenderIndex;
+ switch (argPosition) {
+ case SuspenderArgPosition::First:
+ paramsOffset = 1;
+ suspenderIndex = 0;
+ break;
+ case SuspenderArgPosition::Last:
+ paramsOffset = 0;
+ suspenderIndex = params.length();
+ break;
+ default:
+ MOZ_CRASH();
+ }
+
+ if (wrappedWasmFuncType.args().length() != params.length() + 1 ||
+ wrappedWasmFuncType.args()[suspenderIndex] != RefType::extern_()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_EXPECTED_SUSPENDER);
+ return nullptr;
+ }
+ for (size_t i = 0; i < params.length(); i++) {
+ if (params[i] != wrappedWasmFuncType.args()[i + paramsOffset]) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_SIGNATURE_MISMATCH);
+ return nullptr;
+ }
+ }
+ } else {
+ MOZ_ASSERT(results.length() == 0 && params.length() == 0);
+ if (!results.append(RefType::extern_())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ if (!params.append(wrappedWasmFuncType.args().begin(),
+ wrappedWasmFuncType.args().end())) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ }
+
+ PromisingFunctionModuleFactory moduleFactory;
+ SharedModule module = moduleFactory.build(
+ cx, wrappedWasmFunc, std::move(params), std::move(results), argPosition);
+ // Instantiate the module.
+ Rooted<ImportValues> imports(cx);
+
+ // Add wrapped function ($promising.wrappedfn) to imports.
+ if (!imports.get().funcs.append(func)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ // Add $promising.create-suspender to imports.
+ RootedFunction createSuspenderFunc(
+ cx, NewNativeFunction(cx, WasmPICreateSuspender, 0, nullptr,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ if (!createSuspenderFunc) {
+ return nullptr;
+ }
+ if (!imports.get().funcs.append(createSuspenderFunc)) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+
+ Rooted<WasmInstanceObject*> instance(cx);
+ if (!module->instantiate(cx, imports.get(), nullptr, &instance)) {
+ MOZ_ASSERT(cx->isThrowingOutOfMemory());
+ return nullptr;
+ }
+
+ // Wrap $promising.exported function for exceptions/traps handling.
+ RootedFunction wasmFunc(cx);
+ if (!WasmInstanceObject::getExportedFunction(
+ cx, instance, PromisingFunctionModuleFactory::ExportedFnIndex,
+ &wasmFunc)) {
+ return nullptr;
+ }
+
+ RootedFunction wasmFuncWrapper(
+ cx, NewNativeFunction(cx, WasmPIPromisingFunction, 0, nullptr,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ if (!wasmFuncWrapper) {
+ return nullptr;
+ }
+ wasmFuncWrapper->initExtendedSlot(WRAPPED_FN_SLOT, ObjectValue(*wasmFunc));
+ return wasmFuncWrapper;
+}
+
+// Gets active suspender.
+// The reserved parameter is a workaround for limitation in the
+// WasmBuiltinModule.yaml generator to always have params.
+SuspenderObject* CurrentSuspender(Instance* instance, int32_t reserved) {
+ MOZ_ASSERT(SASigCurrentSuspender.failureMode == FailureMode::FailOnNullPtr);
+ JSContext* cx = instance->cx();
+ SuspenderObject* suspender = cx->wasm().promiseIntegration.activeSuspender();
+ if (!suspender) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_INVALID_STATE);
+ return nullptr;
+ }
+ return suspender;
+}
+
+// Checks suspender value.
+SuspenderObject* CheckSuspender(Instance* instance, JSObject* maybeSuspender) {
+ MOZ_ASSERT(SASigCheckSuspender.failureMode == FailureMode::FailOnNullPtr);
+ JSContext* cx = instance->cx();
+ if (!maybeSuspender || !maybeSuspender->is<SuspenderObject>() ||
+ &maybeSuspender->as<SuspenderObject>() !=
+ cx->wasm().promiseIntegration.activeSuspender()) {
+ // Wrong suspender
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_INVALID_SUSPENDER);
+ return nullptr;
+ }
+ SuspenderObject* suspenderObject = &maybeSuspender->as<SuspenderObject>();
+ if (suspenderObject->state() != SuspenderState::Active) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSPI_INVALID_STATE);
+ return nullptr;
+ }
+ return suspenderObject;
+}
+
+// Converts promise results into actual function result, or exception/trap
+// if rejected.
+JSObject* GetSuspendingPromiseResult(Instance* instance,
+ SuspenderObject* suspender) {
+ MOZ_ASSERT(SASigGetSuspendingPromiseResult.failureMode ==
+ FailureMode::FailOnNullPtr);
+ JSContext* cx = instance->cx();
+ Rooted<SuspenderObject*> suspenderObject(cx, suspender);
+
+ Rooted<PromiseObject*> promise(cx, suspenderObject->suspendingPromise());
+
+ if (promise->state() == JS::PromiseState::Rejected) {
+ RootedValue reason(cx, promise->reason());
+ // Result is also the reason of promise rejection.
+ cx->setPendingException(reason, ShouldCaptureStack::Maybe);
+ return nullptr;
+ }
+
+ // Construct the results object.
+ Rooted<WasmStructObject*> results(
+ cx, instance->constantStructNewDefault(
+ cx, SuspendingFunctionModuleFactory::ResultsTypeIndex));
+ const StructFieldVector& fields = results->typeDef().structType().fields_;
+
+ MOZ_ASSERT(fields.length() <= 1);
+ if (fields.length() == 1) {
+ RootedValue jsValue(cx, promise->value());
+
+ // The struct object is constructed based on returns of exported function.
+ // It is the only way we can get ValType for Val::fromJSValue call.
+ auto bestTier = instance->code().bestTier();
+ const wasm::FuncExport& funcExport =
+ instance->metadata(bestTier).lookupFuncExport(
+ SuspendingFunctionModuleFactory::ExportedFnIndex);
+ const wasm::FuncType& sig =
+ instance->metadata().getFuncExportType(funcExport);
+
+ RootedVal val(cx);
+ MOZ_ASSERT(sig.result(0).storageType() == fields[0].type);
+ if (!Val::fromJSValue(cx, sig.result(0), jsValue, &val)) {
+ return nullptr;
+ }
+ results->storeVal(val, 0);
+ }
+ return results;
+}
+
+// Resolves the promise using results packed by wasm.
+int32_t SetPromisingPromiseResults(Instance* instance,
+ SuspenderObject* suspender,
+ WasmStructObject* results) {
+ MOZ_ASSERT(SASigSetPromisingPromiseResults.failureMode ==
+ FailureMode::FailOnNegI32);
+ JSContext* cx = instance->cx();
+ Rooted<WasmStructObject*> res(cx, results);
+ Rooted<SuspenderObject*> suspenderObject(cx, suspender);
+ RootedObject promise(cx, suspenderObject->promisingPromise());
+
+ const StructType& resultType = res->typeDef().structType();
+ RootedValue val(cx);
+ // Unbox the result value from the struct, if any.
+ if (resultType.fields_.length() > 0) {
+ MOZ_RELEASE_ASSERT(resultType.fields_.length() == 1);
+ if (!res->getField(cx, /*index=*/0, &val)) {
+ return -1;
+ }
+ }
+ ResolvePromise(cx, promise, val);
+ return 0;
+}
+
+void UpdateSuspenderState(Instance* instance, SuspenderObject* suspender,
+ UpdateSuspenderStateAction action) {
+ MOZ_ASSERT(SASigUpdateSuspenderState.failureMode == FailureMode::Infallible);
+
+ JSContext* cx = instance->cx();
+ switch (action) {
+ case UpdateSuspenderStateAction::Enter:
+ suspender->enter(cx);
+ break;
+ case UpdateSuspenderStateAction::Suspend:
+ suspender->suspend(cx);
+ break;
+ case UpdateSuspenderStateAction::Resume:
+ suspender->resume(cx);
+ break;
+ case UpdateSuspenderStateAction::Leave:
+ suspender->leave(cx);
+ break;
+ default:
+ MOZ_CRASH();
+ }
+}
+
+} // namespace js::wasm
+#endif // ENABLE_WASM_JSPI