/* -*- 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. */ #ifndef wasm_pi_h #define wasm_pi_h #include "js/TypeDecls.h" #include "vm/NativeObject.h" #include "vm/PromiseObject.h" #include "wasm/WasmAnyRef.h" #include "wasm/WasmTypeDef.h" // [SMDOC] JS Promise Integration // // The API provides relatively efficient and relatively ergonomic interop // between JavaScript promises and WebAssembly but works under the constraint // that the only changes are to the JS API and not to the core wasm. // // Secondary (suspendable) stacks are introduced at the entrance into the wasm // code -- a promising function. A suspendable stack can contain/store only // wasm frames and be part of one activation. If there is a need to execute // a JS script, the stack must be switched back (to the main stack). // // There is a special exit from the suspendable stack where it is expected to // receive a promise from a JS script -- a suspending function/import. If wasm // code calls such import, the suspendable stack will be unlinked from // the current activation allowing the main stack to continue returning to // the event loop. // // Here is a small example that uses JS Promise Integration API: // // const suspending = new WebAssembly.Suspending(async () => 42) // const ins = wasmTextEval(`(module // (import "" "suspending" (func $imp (result i32))) // (func (export "entry") (result i32) (call $imp)) // )`, {"": { suspending, }}) // const promising = WebAssembly.promising(ins.exports.entry) // assertEq(await promising(), 42) // // The states transitions can be described by the following diagram: // // Invoke // Promising Promise // +-------+ Export +----------+ Resolved +---------+ // |Initial+----------->|Wasm Logic|<-----------+Suspended| // +-------+ ++-+------++ +---------+ // | | ^ |Invoke ^ Suspending Function // Return from| | | |Suspending | Returns a Promise // +--------+ Wasm Call | | | |Import +----+---+ // |Finished|<-----------+ | | +------------>|JS Logic| // +--------+ | | +----+---+ // +------------+ | | Re-entry // |Invoke Other |Return +------> // |Import +--+-----+ // +---------->|JS Logic| // +--------+ // // The Invoke Promising Export transition creates a suspendable stack, // switches to it, and continues execution of wasm code there. When the callee // frame is popped, the promise is returned to the JS caller. // // The Invoke Suspending Import switches stack to the main one and sets // the suspended stack aside. It is expected that the suspending promise // is returned by JS. The callee returns to the moment the promising call was // instantiated. // // The Return from Wasm Call transition destroys the suspendable stack, // continues execution on the main stack, and resolves the promising promise // with the results of the call. // // The Promise Resolve transition is invoked when the suspending promise is // resolved, which wakes the suspendable stack and extends the main one. // The execution will continue on the suspendable stack that returns the // resolution values to the wasm code as return values. // // The Invoke Other Import transition temporary switches to the main stack and // invokes the JS code. The suspendable stack will not be removed from the // chain of frames. // // Notice that calling wasm and then invoking a suspendable import from // the main stack is not allowed. For example, re-exporting $imp, from // the small example above, and calling it directly from the JS main thread // will fail. // // The `new WebAssembly.Suspending(fn)` logic is implemented in a Wasm module // generated by `SuspendingFunctionModuleFactory` utility (see WasmPI.cpp). // The `WebAssembly.promising(wasmfn)` logic is implemented in a Wasm module // generated by `PromisingFunctionModuleFactory` utility. namespace js { class WasmStructObject; namespace wasm { class Context; #ifdef ENABLE_WASM_JSPI enum SuspenderState : int32_t { // The suspender's has no stack or the stack has finished and has been // destroyed. Moribund, // The suspender's stack hasn't been entered yet. Initial, // The suspender's stack is active and is the currently active stack. Active, // The suspender's stack has been suspended and is no longer the active stack // and isn't linked to the active stack. Suspended, // The suspender's stack has switched back to the main stack for a call. It // is not the active call stack, but is linked to by the active stack. CalledOnMain, }; class SuspenderObject : public NativeObject { public: static const JSClass class_; enum { StateSlot, PromisingPromiseSlot, SuspendingReturnTypeSlot, StackMemorySlot, MainFPSlot, MainSPSlot, SuspendableFPSlot, SuspendableSPSlot, SuspendableExitFPSlot, SuspendedRASlot, MainExitFPSlot, SlotCount, }; enum class ReturnType : int32_t { Unknown, Promise, Exception }; static SuspenderObject* create(JSContext* cx); SuspenderState state() const { return (SuspenderState)getFixedSlot(StateSlot).toInt32(); } void setState(SuspenderState state) { setFixedSlot(StateSlot, JS::Int32Value((int32_t)state)); } // This suspender can be traced if it's not 'Initial' or 'Moribund'. bool isTraceable() const { SuspenderState current = state(); return current == SuspenderState::Active || current == SuspenderState::Suspended || current == SuspenderState::CalledOnMain; } bool isMoribund() const { return state() == SuspenderState::Moribund; } bool isActive() const { return state() == SuspenderState::Active; } bool isSuspended() const { return state() == SuspenderState::Suspended; } bool isCalledOnMain() const { return state() == SuspenderState::CalledOnMain; } PromiseObject* promisingPromise() const { return &getFixedSlot(PromisingPromiseSlot).toObject().as(); } void setPromisingPromise(Handle promise) { setFixedSlot(PromisingPromiseSlot, ObjectOrNullValue(promise)); } ReturnType suspendingReturnType() const { return ReturnType(getFixedSlot(SuspendingReturnTypeSlot).toInt32()); } void setSuspendingReturnType(ReturnType type) { // The SuspendingReturnTypeSlot will change after result is defined, // and becomes invalid after GetSuspendingPromiseResult. The assert is // checking if the result was processed by GetSuspendingPromiseResult. MOZ_ASSERT((type == ReturnType::Unknown) != (suspendingReturnType() == ReturnType::Unknown)); setFixedSlot(SuspendingReturnTypeSlot, Int32Value(int32_t(type))); } // Pointer to the beginning of the stack memory allocation. void* stackMemory() const { return getFixedSlot(StackMemorySlot).toPrivate(); } void setStackMemory(void* stackMemory) { setFixedSlot(StackMemorySlot, PrivateValue(stackMemory)); } // The logical beginning or bottom of the stack, which is the physically // highest memory address in the stack allocation. JS::NativeStackLimit stackMemoryBase() const { return ((uintptr_t)stackMemory()) + SuspendableStackPlusRedZoneSize; } // The logical end or top of the stack for system code, which is the // physically lowest memory address in the stack allocation. This does not // include any 'red zone' space, and so it is not safe to use if a stub // or OS interrupt handler could run on the stack. Use // `stackMemoryLimitForJit` instead. JS::NativeStackLimit stackMemoryLimitForSystem() const { return JS::NativeStackLimit(stackMemory()); } // The logical end or top of the stack for JIT code, which is the // physically lowest memory address in the stack allocation. This does // include 'red zone' space for running stubs or OS interrupt handlers. JS::NativeStackLimit stackMemoryLimitForJit() const { return stackMemoryLimitForSystem() + SuspendableRedZoneSize; } bool hasStackAddress(const void* stackAddress) const { MOZ_ASSERT(!isMoribund()); void* base = stackMemory(); return (uintptr_t)base <= (uintptr_t)stackAddress && (uintptr_t)stackAddress < (uintptr_t)base + SuspendableStackPlusRedZoneSize; } // Stored main stack FP register. void* mainFP() const { MOZ_ASSERT(isActive()); return getFixedSlot(MainFPSlot).toPrivate(); } // Stored main stack SP register. void* mainSP() const { MOZ_ASSERT(isActive()); return getFixedSlot(MainSPSlot).toPrivate(); } // Stored main stack exit/top frame pointer. void* mainExitFP() const { MOZ_ASSERT(isSuspended()); return getFixedSlot(MainExitFPSlot).toPrivate(); } // Stored suspendable stack FP register. void* suspendableFP() const { MOZ_ASSERT(isSuspended()); return getFixedSlot(SuspendableFPSlot).toPrivate(); } // Stored suspendable stack SP register. void* suspendableSP() const { MOZ_ASSERT(isSuspended()); return getFixedSlot(SuspendableSPSlot).toPrivate(); } // Stored return address for return to suspendable stack. void* suspendedReturnAddress() const { MOZ_ASSERT(isSuspended()); return getFixedSlot(SuspendedRASlot).toPrivate(); } // Stored suspendable stack exit/bottom frame pointer. void* suspendableExitFP() const { // We always have the root frame when we've been entered into, which is // when we're traceable. MOZ_ASSERT(isTraceable()); return getFixedSlot(SuspendableExitFPSlot).toPrivate(); } static constexpr size_t offsetOfState() { return getFixedSlotOffset(StateSlot); } static constexpr size_t offsetOfStackMemory() { return getFixedSlotOffset(StackMemorySlot); } static constexpr size_t offsetOfMainFP() { return getFixedSlotOffset(MainFPSlot); } static constexpr size_t offsetOfMainSP() { return getFixedSlotOffset(MainSPSlot); } static constexpr size_t offsetOfSuspendableFP() { return getFixedSlotOffset(SuspendableFPSlot); } static constexpr size_t offsetOfSuspendableSP() { return getFixedSlotOffset(SuspendableSPSlot); } static constexpr size_t offsetOfSuspendableExitFP() { return getFixedSlotOffset(SuspendableExitFPSlot); } static constexpr size_t offsetOfMainExitFP() { return getFixedSlotOffset(MainExitFPSlot); } static constexpr size_t offsetOfSuspendedReturnAddress() { return getFixedSlotOffset(SuspendedRASlot); } 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); void unwind(JSContext* cx); void releaseStackMemory(); // Modifies frames to inject the suspendable stack back into the main one. void forwardToSuspendable(); private: static const JSClassOps classOps_; static const ClassExtension classExt_; static void finalize(JS::GCContext* gcx, JSObject* obj); static void trace(JSTracer* trc, JSObject* obj); static size_t moved(JSObject* obj, JSObject* old); }; JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func, wasm::ValTypeVector&& params, wasm::ValTypeVector&& results); JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func, const FuncType& type); JSFunction* WasmPromisingFunctionCreate(JSContext* cx, HandleObject func, wasm::ValTypeVector&& params, wasm::ValTypeVector&& results); SuspenderObject* CurrentSuspender(Instance* instance, int reserved); SuspenderObject* CreateSuspender(Instance* instance, int reserved); PromiseObject* CreatePromisingPromise(Instance* instance, SuspenderObject* suspender); JSObject* GetSuspendingPromiseResult(Instance* instance, void* result, SuspenderObject* suspender); void* AddPromiseReactions(Instance* instance, SuspenderObject* suspender, void* result, JSFunction* continueOnSuspendable); void* ForwardExceptionToSuspended(Instance* instance, SuspenderObject* suspender, void* exception); int32_t SetPromisingPromiseResults(Instance* instance, SuspenderObject* suspender, WasmStructObject* results); void UpdateSuspenderState(Instance* instance, SuspenderObject* suspender, UpdateSuspenderStateAction action); void TraceSuspendableStack(JSTracer* trc, SuspenderObject* suspender); #else // Provide an empty forward declaration to simplify some conditional // compilation code. class SuspenderObject; #endif // ENABLE_WASM_JSPI } // namespace wasm } // namespace js #endif // wasm_pi_h