WasmPI.h (13881B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * 4 * Copyright 2016 Mozilla Foundation 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 #ifndef wasm_pi_h 20 #define wasm_pi_h 21 22 #include "js/TypeDecls.h" 23 #include "vm/NativeObject.h" 24 #include "vm/PromiseObject.h" 25 #include "wasm/WasmAnyRef.h" 26 #include "wasm/WasmTypeDef.h" 27 28 // [SMDOC] JS Promise Integration 29 // 30 // The API provides relatively efficient and relatively ergonomic interop 31 // between JavaScript promises and WebAssembly but works under the constraint 32 // that the only changes are to the JS API and not to the core wasm. 33 // 34 // Secondary (suspendable) stacks are introduced at the entrance into the wasm 35 // code -- a promising function. A suspendable stack can contain/store only 36 // wasm frames and be part of one activation. If there is a need to execute 37 // a JS script, the stack must be switched back (to the main stack). 38 // 39 // There is a special exit from the suspendable stack where it is expected to 40 // receive a promise from a JS script -- a suspending function/import. If wasm 41 // code calls such import, the suspendable stack will be unlinked from 42 // the current activation allowing the main stack to continue returning to 43 // the event loop. 44 // 45 // Here is a small example that uses JS Promise Integration API: 46 // 47 // const suspending = new WebAssembly.Suspending(async () => 42) 48 // const ins = wasmTextEval(`(module 49 // (import "" "suspending" (func $imp (result i32))) 50 // (func (export "entry") (result i32) (call $imp)) 51 // )`, {"": { suspending, }}) 52 // const promising = WebAssembly.promising(ins.exports.entry) 53 // assertEq(await promising(), 42) 54 // 55 // The states transitions can be described by the following diagram: 56 // 57 // Invoke 58 // Promising Promise 59 // +-------+ Export +----------+ Resolved +---------+ 60 // |Initial+----------->|Wasm Logic|<-----------+Suspended| 61 // +-------+ ++-+------++ +---------+ 62 // | | ^ |Invoke ^ Suspending Function 63 // Return from| | | |Suspending | Returns a Promise 64 // +--------+ Wasm Call | | | |Import +----+---+ 65 // |Finished|<-----------+ | | +------------>|JS Logic| 66 // +--------+ | | +----+---+ 67 // +------------+ | | Re-entry 68 // |Invoke Other |Return +------> 69 // |Import +--+-----+ 70 // +---------->|JS Logic| 71 // +--------+ 72 // 73 // The Invoke Promising Export transition creates a suspendable stack, 74 // switches to it, and continues execution of wasm code there. When the callee 75 // frame is popped, the promise is returned to the JS caller. 76 // 77 // The Invoke Suspending Import switches stack to the main one and sets 78 // the suspended stack aside. It is expected that the suspending promise 79 // is returned by JS. The callee returns to the moment the promising call was 80 // instantiated. 81 // 82 // The Return from Wasm Call transition destroys the suspendable stack, 83 // continues execution on the main stack, and resolves the promising promise 84 // with the results of the call. 85 // 86 // The Promise Resolve transition is invoked when the suspending promise is 87 // resolved, which wakes the suspendable stack and extends the main one. 88 // The execution will continue on the suspendable stack that returns the 89 // resolution values to the wasm code as return values. 90 // 91 // The Invoke Other Import transition temporary switches to the main stack and 92 // invokes the JS code. The suspendable stack will not be removed from the 93 // chain of frames. 94 // 95 // Notice that calling wasm and then invoking a suspendable import from 96 // the main stack is not allowed. For example, re-exporting $imp, from 97 // the small example above, and calling it directly from the JS main thread 98 // will fail. 99 // 100 // The `new WebAssembly.Suspending(fn)` logic is implemented in a Wasm module 101 // generated by `SuspendingFunctionModuleFactory` utility (see WasmPI.cpp). 102 // The `WebAssembly.promising(wasmfn)` logic is implemented in a Wasm module 103 // generated by `PromisingFunctionModuleFactory` utility. 104 105 namespace js { 106 107 class WasmStructObject; 108 109 namespace wasm { 110 111 class Context; 112 113 #ifdef ENABLE_WASM_JSPI 114 115 enum SuspenderState : int32_t { 116 // The suspender's has no stack or the stack has finished and has been 117 // destroyed. 118 Moribund, 119 // The suspender's stack hasn't been entered yet. 120 Initial, 121 // The suspender's stack is active and is the currently active stack. 122 Active, 123 // The suspender's stack has been suspended and is no longer the active stack 124 // and isn't linked to the active stack. 125 Suspended, 126 // The suspender's stack has switched back to the main stack for a call. It 127 // is not the active call stack, but is linked to by the active stack. 128 CalledOnMain, 129 }; 130 131 class SuspenderObject : public NativeObject { 132 public: 133 static const JSClass class_; 134 135 enum { 136 StateSlot, 137 138 PromisingPromiseSlot, 139 SuspendingReturnTypeSlot, 140 141 StackMemorySlot, 142 MainFPSlot, 143 MainSPSlot, 144 SuspendableFPSlot, 145 SuspendableSPSlot, 146 SuspendableExitFPSlot, 147 SuspendedRASlot, 148 MainExitFPSlot, 149 150 SlotCount, 151 }; 152 153 enum class ReturnType : int32_t { Unknown, Promise, Exception }; 154 155 static SuspenderObject* create(JSContext* cx); 156 157 SuspenderState state() const { 158 return (SuspenderState)getFixedSlot(StateSlot).toInt32(); 159 } 160 void setState(SuspenderState state) { 161 setFixedSlot(StateSlot, JS::Int32Value((int32_t)state)); 162 } 163 164 // This suspender can be traced if it's not 'Initial' or 'Moribund'. 165 bool isTraceable() const { 166 SuspenderState current = state(); 167 return current == SuspenderState::Active || 168 current == SuspenderState::Suspended || 169 current == SuspenderState::CalledOnMain; 170 } 171 bool isMoribund() const { return state() == SuspenderState::Moribund; } 172 bool isActive() const { return state() == SuspenderState::Active; } 173 bool isSuspended() const { return state() == SuspenderState::Suspended; } 174 bool isCalledOnMain() const { 175 return state() == SuspenderState::CalledOnMain; 176 } 177 178 PromiseObject* promisingPromise() const { 179 return &getFixedSlot(PromisingPromiseSlot).toObject().as<PromiseObject>(); 180 } 181 void setPromisingPromise(Handle<PromiseObject*> promise) { 182 setFixedSlot(PromisingPromiseSlot, ObjectOrNullValue(promise)); 183 } 184 185 ReturnType suspendingReturnType() const { 186 return ReturnType(getFixedSlot(SuspendingReturnTypeSlot).toInt32()); 187 } 188 void setSuspendingReturnType(ReturnType type) { 189 // The SuspendingReturnTypeSlot will change after result is defined, 190 // and becomes invalid after GetSuspendingPromiseResult. The assert is 191 // checking if the result was processed by GetSuspendingPromiseResult. 192 MOZ_ASSERT((type == ReturnType::Unknown) != 193 (suspendingReturnType() == ReturnType::Unknown)); 194 195 setFixedSlot(SuspendingReturnTypeSlot, Int32Value(int32_t(type))); 196 } 197 198 // Pointer to the beginning of the stack memory allocation. 199 void* stackMemory() const { 200 return getFixedSlot(StackMemorySlot).toPrivate(); 201 } 202 void setStackMemory(void* stackMemory) { 203 setFixedSlot(StackMemorySlot, PrivateValue(stackMemory)); 204 } 205 206 // The logical beginning or bottom of the stack, which is the physically 207 // highest memory address in the stack allocation. 208 JS::NativeStackLimit stackMemoryBase() const { 209 return ((uintptr_t)stackMemory()) + SuspendableStackPlusRedZoneSize; 210 } 211 // The logical end or top of the stack for system code, which is the 212 // physically lowest memory address in the stack allocation. This does not 213 // include any 'red zone' space, and so it is not safe to use if a stub 214 // or OS interrupt handler could run on the stack. Use 215 // `stackMemoryLimitForJit` instead. 216 JS::NativeStackLimit stackMemoryLimitForSystem() const { 217 return JS::NativeStackLimit(stackMemory()); 218 } 219 // The logical end or top of the stack for JIT code, which is the 220 // physically lowest memory address in the stack allocation. This does 221 // include 'red zone' space for running stubs or OS interrupt handlers. 222 JS::NativeStackLimit stackMemoryLimitForJit() const { 223 return stackMemoryLimitForSystem() + SuspendableRedZoneSize; 224 } 225 226 bool hasStackAddress(const void* stackAddress) const { 227 MOZ_ASSERT(!isMoribund()); 228 void* base = stackMemory(); 229 return (uintptr_t)base <= (uintptr_t)stackAddress && 230 (uintptr_t)stackAddress < 231 (uintptr_t)base + SuspendableStackPlusRedZoneSize; 232 } 233 234 // Stored main stack FP register. 235 void* mainFP() const { 236 MOZ_ASSERT(isActive()); 237 return getFixedSlot(MainFPSlot).toPrivate(); 238 } 239 // Stored main stack SP register. 240 void* mainSP() const { 241 MOZ_ASSERT(isActive()); 242 return getFixedSlot(MainSPSlot).toPrivate(); 243 } 244 // Stored main stack exit/top frame pointer. 245 void* mainExitFP() const { 246 MOZ_ASSERT(isSuspended()); 247 return getFixedSlot(MainExitFPSlot).toPrivate(); 248 } 249 // Stored suspendable stack FP register. 250 void* suspendableFP() const { 251 MOZ_ASSERT(isSuspended()); 252 return getFixedSlot(SuspendableFPSlot).toPrivate(); 253 } 254 // Stored suspendable stack SP register. 255 void* suspendableSP() const { 256 MOZ_ASSERT(isSuspended()); 257 return getFixedSlot(SuspendableSPSlot).toPrivate(); 258 } 259 // Stored return address for return to suspendable stack. 260 void* suspendedReturnAddress() const { 261 MOZ_ASSERT(isSuspended()); 262 return getFixedSlot(SuspendedRASlot).toPrivate(); 263 } 264 // Stored suspendable stack exit/bottom frame pointer. 265 void* suspendableExitFP() const { 266 // We always have the root frame when we've been entered into, which is 267 // when we're traceable. 268 MOZ_ASSERT(isTraceable()); 269 return getFixedSlot(SuspendableExitFPSlot).toPrivate(); 270 } 271 272 static constexpr size_t offsetOfState() { 273 return getFixedSlotOffset(StateSlot); 274 } 275 static constexpr size_t offsetOfStackMemory() { 276 return getFixedSlotOffset(StackMemorySlot); 277 } 278 static constexpr size_t offsetOfMainFP() { 279 return getFixedSlotOffset(MainFPSlot); 280 } 281 static constexpr size_t offsetOfMainSP() { 282 return getFixedSlotOffset(MainSPSlot); 283 } 284 static constexpr size_t offsetOfSuspendableFP() { 285 return getFixedSlotOffset(SuspendableFPSlot); 286 } 287 static constexpr size_t offsetOfSuspendableSP() { 288 return getFixedSlotOffset(SuspendableSPSlot); 289 } 290 static constexpr size_t offsetOfSuspendableExitFP() { 291 return getFixedSlotOffset(SuspendableExitFPSlot); 292 } 293 static constexpr size_t offsetOfMainExitFP() { 294 return getFixedSlotOffset(MainExitFPSlot); 295 } 296 static constexpr size_t offsetOfSuspendedReturnAddress() { 297 return getFixedSlotOffset(SuspendedRASlot); 298 } 299 300 void setMoribund(JSContext* cx); 301 void setActive(JSContext* cx); 302 void setSuspended(JSContext* cx); 303 304 void enter(JSContext* cx); 305 void suspend(JSContext* cx); 306 void resume(JSContext* cx); 307 void leave(JSContext* cx); 308 void unwind(JSContext* cx); 309 310 void releaseStackMemory(); 311 312 // Modifies frames to inject the suspendable stack back into the main one. 313 void forwardToSuspendable(); 314 315 private: 316 static const JSClassOps classOps_; 317 static const ClassExtension classExt_; 318 319 static void finalize(JS::GCContext* gcx, JSObject* obj); 320 static void trace(JSTracer* trc, JSObject* obj); 321 static size_t moved(JSObject* obj, JSObject* old); 322 }; 323 324 JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func, 325 wasm::ValTypeVector&& params, 326 wasm::ValTypeVector&& results); 327 328 JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func, 329 const FuncType& type); 330 331 JSFunction* WasmPromisingFunctionCreate(JSContext* cx, HandleObject func, 332 wasm::ValTypeVector&& params, 333 wasm::ValTypeVector&& results); 334 335 SuspenderObject* CurrentSuspender(Instance* instance, int reserved); 336 337 SuspenderObject* CreateSuspender(Instance* instance, int reserved); 338 339 PromiseObject* CreatePromisingPromise(Instance* instance, 340 SuspenderObject* suspender); 341 342 JSObject* GetSuspendingPromiseResult(Instance* instance, void* result, 343 SuspenderObject* suspender); 344 345 void* AddPromiseReactions(Instance* instance, SuspenderObject* suspender, 346 void* result, JSFunction* continueOnSuspendable); 347 348 void* ForwardExceptionToSuspended(Instance* instance, 349 SuspenderObject* suspender, void* exception); 350 351 int32_t SetPromisingPromiseResults(Instance* instance, 352 SuspenderObject* suspender, 353 WasmStructObject* results); 354 355 void UpdateSuspenderState(Instance* instance, SuspenderObject* suspender, 356 UpdateSuspenderStateAction action); 357 358 void TraceSuspendableStack(JSTracer* trc, SuspenderObject* suspender); 359 360 #else 361 362 // Provide an empty forward declaration to simplify some conditional 363 // compilation code. 364 class SuspenderObject; 365 366 #endif // ENABLE_WASM_JSPI 367 368 } // namespace wasm 369 } // namespace js 370 371 #endif // wasm_pi_h