Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
*
*
* 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<PromiseObject>();
}
void setPromisingPromise(Handle<PromiseObject*> 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