| /* |
| * Copyright (C) 2025 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "PinballCompletion.h" |
| |
| #if ENABLE(WEBASSEMBLY) |
| |
| #include "ConservativeRoots.h" |
| #include "EvacuatedStack.h" |
| #include "Exception.h" |
| #include "JSCellInlines.h" |
| #include "JSPIContextInlines.h" |
| #include "JSPromise.h" |
| #include "PinballHandlerContext.h" |
| #include "StackAlignment.h" |
| #include "TopExceptionScope.h" |
| |
| #include <wtf/StdLibExtras.h> |
| |
| namespace JSC { |
| |
| const ClassInfo PinballCompletion::s_info = { "PinballCompletion"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(PinballCompletion) }; |
| |
| Structure* PinballCompletion::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue proto) |
| { |
| return Structure::create(vm, globalObject, proto, TypeInfo(ObjectType, StructureFlags), info()); |
| } |
| |
| PinballCompletion* PinballCompletion::create(VM& vm, Vector<std::unique_ptr<EvacuatedStackSlice>>&& slices, CPURegister* calleeSaves, JSPromise* resultPromise) |
| { |
| Structure* structure = vm.pinballCompletionStructure.get(); |
| auto* instance = new (NotNull, allocateCell<PinballCompletion>(vm)) PinballCompletion(vm, structure, WTF::move(slices), calleeSaves, resultPromise); |
| instance->finishCreation(vm); |
| return instance; |
| } |
| |
| |
| PinballCompletion::PinballCompletion(VM& vm, Structure* structure, Vector<std::unique_ptr<EvacuatedStackSlice>>&& slices, CPURegister* calleeSaves, JSPromise* resultPromise) |
| : Base(vm, structure) |
| , m_slices(WTF::move(slices)) |
| , m_resultPromise(resultPromise, WriteBarrierEarlyInit) |
| { |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN |
| memcpySpan(std::span(m_calleeSaves), std::span(calleeSaves, NUMBER_OF_CALLEE_SAVES_REGISTERS)); |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_END |
| } |
| |
| void PinballCompletion::destroy(JSCell* cell) |
| { |
| SUPPRESS_MEMORY_UNSAFE_CAST // jsCast() is not available here; validity is guaranteed by the destruction mechanism |
| auto* thisObject = static_cast<PinballCompletion*>(cell); |
| thisObject->~PinballCompletion(); |
| } |
| |
| void PinballCompletion::assimilate(PinballCompletion* other) |
| { |
| other->m_slices.appendVector(WTF::move(m_slices)); |
| m_slices = WTF::move(other->m_slices); |
| } |
| |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN |
| |
| void PinballCompletion::gatherConservativeRoots(ConservativeRoots& roots) |
| { |
| for (auto& slice : m_slices) { |
| std::span<Register> slots = slice->slots(); |
| roots.add(slots.data(), slots.data() + slots.size()); |
| } |
| roots.add(m_calleeSaves, m_calleeSaves + NUMBER_OF_CALLEE_SAVES_REGISTERS); |
| } |
| |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_END |
| |
| template<typename Visitor> |
| void PinballCompletion::visitChildrenImpl(JSCell* cell, Visitor& visitor) |
| { |
| auto* thisObject = uncheckedDowncast<PinballCompletion>(cell); |
| ASSERT_GC_OBJECT_INHERITS(thisObject, info()); |
| Base::visitChildren(thisObject, visitor); |
| |
| visitor.append(thisObject->m_resultPromise); |
| // Evacuated stack slices and callee saves are conservatively scanned by the "Pbc" |
| // constraint in Heap::addCoreConstraints() when this cell is marked. |
| } |
| |
| DEFINE_VISIT_CHILDREN(PinballCompletion); |
| |
| extern "C" { |
| // defined in InPlaceInterpreter.asm |
| JSC_DECLARE_HOST_FUNCTION(pinballHandlerFulfillFunction); |
| JSC_DECLARE_HOST_FUNCTION(pinballHandlerRejectFunction); |
| } |
| |
| static JSFunctionWithFields* createHandler(VM& vm, JSGlobalObject* globalObject, PinballCompletion* pinballCompletion, NativeFunction function, const String name) |
| { |
| NativeExecutable* executable = vm.getHostFunction(function, ImplementationVisibility::Private, NoIntrinsic, callHostFunctionAsConstructor, nullptr, name); |
| constexpr unsigned length = 1; |
| JSFunctionWithFields* handler = JSFunctionWithFields::create(vm, globalObject, executable, length, name); |
| handler->setField(vm, JSFunctionWithFields::Field::PromiseHandlerPinballCompletion, pinballCompletion); |
| return handler; |
| } |
| |
| JSFunctionWithFields* createPinballCompletionFulfillHandler(VM& vm, JSGlobalObject* globalObject, PinballCompletion* pinballCompletion) |
| { |
| return createHandler(vm, globalObject, pinballCompletion, pinballHandlerFulfillFunction, "<pinball fulfill handler>"_s); |
| } |
| |
| JSFunctionWithFields* createPinballCompletionRejectHandler(VM& vm, JSGlobalObject* globalObject, PinballCompletion* pinballCompletion) |
| { |
| return createHandler(vm, globalObject, pinballCompletion, pinballHandlerRejectFunction, "<pinball reject handler>"_s); |
| } |
| |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN |
| |
| /* |
| The following C++ functions implement the "normal" part of the logic of reviving |
| and executing a suspended Wasm stack when the suspension promise has been fulfilled. The |
| magical stack and register manipulation is done by the core handler code implemented |
| in offlineasm. |
| */ |
| |
| extern "C" void SYSV_ABI pinballHandlerInitContextForFulfill(JSGlobalObject*, CallFrame*, PinballHandlerContext*); |
| extern "C" void SYSV_ABI pinballHandlerInitContextForReject(JSGlobalObject*, CallFrame*, PinballHandlerContext*); |
| extern "C" void SYSV_ABI pinballHandlerImplantSlice(PinballHandlerContext*, Register*, CallFrame*, CallerFrameAndPC*); |
| extern "C" UCPURegister SYSV_ABI pinballHandlerFulfillFunctionContinue(PinballHandlerContext*); |
| extern "C" void SYSV_ABI pinballHandlerFinishReject(PinballHandlerContext*); |
| |
| void pinballHandlerInitContextForFulfill(JSGlobalObject* globalObject, CallFrame* callFrame, PinballHandlerContext* context) |
| { |
| ASSERT(callFrame->argumentCount() == 1); |
| new (context) PinballHandlerContext(globalObject, callFrame); |
| context->arguments[0] = JSValue::encode(callFrame->argument(0)); |
| } |
| |
| void pinballHandlerInitContextForReject(JSGlobalObject* globalObject, CallFrame* callFrame, PinballHandlerContext* context) |
| { |
| ASSERT(callFrame->argumentCount() == 1); |
| new (context) PinballHandlerContext(globalObject, callFrame); |
| ASSERT(context->pinball->slices().size() == 1); // exceptions are only supported with slab slicing, expecting 1 slice |
| JSValue reason = callFrame->argument(0); |
| |
| context->zombieFrameCallee = globalObject->zombieFrameCallee(); |
| context->exception = Exception::create(globalObject->vm(), reason); |
| } |
| |
| void pinballHandlerImplantSlice(PinballHandlerContext* context, Register *base, CallFrame* sentinelFrame, CallerFrameAndPC* returnFrame) |
| { |
| ASSERT(context->magic == PinballHandlerContext::expectedMagic); |
| |
| auto slice = context->pinball->takeTopSlice(); |
| CallFrame* bottommostImplantedFrame = slice->implant(base, sentinelFrame); |
| returnFrame->callerFrame = bottommostImplantedFrame; |
| auto* originalDiscriminator = saltedDiscriminator(reinterpret_cast<const void*>(slice.get())); |
| auto* newDiscriminator = reinterpret_cast<const void*>(returnFrame + 1); |
| returnFrame->returnPC = relocateReturnPC(const_cast<void*>(slice->entryPC()), originalDiscriminator, newDiscriminator); |
| } |
| |
| // After the execution of a slice returns, determine how to proceed. |
| // The return value is essentially a 'bool', but making it a UCPURegister allows for uniform treatment |
| // in offlineasm. Otherwise we'd need a special case for x86 where a bool is returned as an 8-bit AL register. |
| // True return indicates that the assembly driver should install and execute the next slice, |
| // false means execution completed, the result promise has been resolved, and the driver should exit. |
| UCPURegister pinballHandlerFulfillFunctionContinue(PinballHandlerContext* context) |
| { |
| ASSERT(context->magic == PinballHandlerContext::expectedMagic); |
| |
| VM& vm = *context->vm; |
| auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); |
| JSPIContext& jspiContext = context->jspiContext; |
| PinballCompletion* pinball = context->pinball; |
| |
| if (jspiContext.completion) { |
| // Computation was suspended again; the remainder of this completion should be added to the new one. |
| jspiContext.completion->assimilate(pinball); |
| jspiContext.deactivate(vm); |
| context->arguments[0] = JSValue::encode(jsUndefined()); |
| context->~PinballHandlerContext(); // context is in asm caller frame data, we destruct it from here |
| return 0; |
| } |
| |
| if (pinball->hasSlices()) { |
| RELEASE_ASSERT(!scope.exception()); // multi-slice completion is not yet prepared to handle exceptions; we should never encounter one at this point |
| context->sliceByteSize = pinball->topSlice()->size() * sizeof(Register); |
| return 1; |
| } |
| |
| jspiContext.deactivate(vm); |
| |
| JSPromise* resultPromise = pinball->resultPromise(); |
| if (!scope.exception()) { |
| JSValue arg = JSValue::decode(context->arguments[0]); |
| resultPromise->resolve(context->globalObject, vm, arg); |
| } else { |
| resultPromise->reject(vm, context->globalObject, scope.exception()); |
| scope.clearException(); |
| } |
| |
| context->arguments[0] = JSValue::encode(jsNull()); |
| context->~PinballHandlerContext(); |
| return 0; |
| } |
| |
| void pinballHandlerFinishReject(PinballHandlerContext* context) |
| { |
| ASSERT(context->magic == PinballHandlerContext::expectedMagic); |
| |
| VM& vm = *context->vm; |
| auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); |
| JSPIContext& jspiContext = context->jspiContext; |
| PinballCompletion* pinball = context->pinball; |
| ASSERT(!pinball->hasSlices()); |
| |
| if (jspiContext.completion) { |
| // The exception was apparently caught, execution proceeded, and was resuspended again. |
| |
| jspiContext.completion->assimilate(pinball); |
| jspiContext.deactivate(vm); |
| context->arguments[0] = JSValue::encode(jsUndefined()); |
| context->~PinballHandlerContext(); // context is in asm caller frame data, we destruct it from here |
| return; |
| } |
| |
| jspiContext.deactivate(vm); |
| |
| JSPromise* resultPromise = pinball->resultPromise(); |
| ASSERT(resultPromise); |
| |
| |
| if (!scope.exception()) { |
| JSValue arg = JSValue::decode(context->arguments[0]); |
| resultPromise->resolve(context->globalObject, vm, arg); |
| } else { |
| resultPromise->reject(vm, context->globalObject, scope.exception()); |
| scope.clearException(); |
| } |
| |
| context->arguments[0] = JSValue::encode(jsNull()); |
| context->~PinballHandlerContext(); |
| } |
| |
| WTF_ALLOW_UNSAFE_BUFFER_USAGE_END |
| |
| } // namespace JSC |
| |
| #endif // ENABLE(WEBASSEMBLY) |