diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index a4f235ea5600..77ab7851ef7d 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1138,6 +1138,7 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/ArrayStorage.h runtime/ArrayStorageInlines.h runtime/AssertInvariants.h + runtime/AsyncContextSwapScope.h runtime/AsyncDisposableStackConstructor.h runtime/AsyncDisposableStackPrototype.h runtime/AsyncDisposableStackPrototypeInlines.h diff --git a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h new file mode 100644 index 000000000000..9d9a33c00687 --- /dev/null +++ b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2026 Codeblog Corp. 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 THE COPYRIGHT HOLDER ``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 THE COPYRIGHT HOLDER 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. + */ + +#pragma once + +#if USE(BUN_JSC_ADDITIONS) + +#include "InternalFieldTuple.h" +#include "JSCast.h" +#include "JSGlobalObject.h" +#include +#include + +namespace JSC { + +// RAII helper for Bun's AsyncLocalStorage: swaps an async context value into +// JSGlobalObject::m_asyncContextData field 0 for the lifetime of the scope and +// restores the previous value on destruction. A no-op when the supplied context +// is empty or undefined, so the common path (no async context active) costs a +// single branch. Also provides helpers for the snapshot side (capturing the +// current context and wrapping it into an InternalFieldTuple alongside a user +// context) and for unwrapping such a tuple on the restore side. +class AsyncContextSwapScope { + WTF_MAKE_NONCOPYABLE(AsyncContextSwapScope); + WTF_FORBID_HEAP_ALLOCATION; +public: + ALWAYS_INLINE AsyncContextSwapScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) + : m_vm(vm) + { + if (asyncContext.isEmpty() || asyncContext.isUndefined()) + return; + m_asyncContextData = globalObject->m_asyncContextData.get(); + if (!m_asyncContextData) + return; + m_restoreAsyncContext = m_asyncContextData->getInternalField(0); + m_asyncContextData->putInternalField(vm, 0, asyncContext); + } + + ALWAYS_INLINE ~AsyncContextSwapScope() + { + restoreEarly(); + } + + // Restore the previous async context before the scope's natural end. Later + // destruction becomes a no-op. Use this when the tail of a case must run + // with the caller's context restored (e.g. resolving a promise whose + // resolution may itself capture the current async context). + ALWAYS_INLINE void restoreEarly() + { + if (m_asyncContextData) { + m_asyncContextData->putInternalField(m_vm, 0, m_restoreAsyncContext); + m_asyncContextData = nullptr; + } + } + + // If contextArg is an InternalFieldTuple [userContext, asyncContext], + // overwrite contextArg with field 0 and return field 1. Otherwise leave + // contextArg untouched and return jsUndefined(). Empty contextArg is + // tolerated (dynamicDowncast(JSValue) is not empty-safe on its own). + static ALWAYS_INLINE JSValue unwrapContextTuple(JSValue& contextArg) + { + if (contextArg.isEmpty()) + return jsUndefined(); + if (auto* tuple = dynamicDowncast(contextArg)) { + contextArg = tuple->getInternalField(0); + return tuple->getInternalField(1); + } + return jsUndefined(); + } + + // Read the current async context (field 0 of m_asyncContextData), or + // jsUndefined() when tracking has not been enabled on this global. + static ALWAYS_INLINE JSValue current(JSGlobalObject* globalObject) + { + if (auto* asyncContextData = globalObject->m_asyncContextData.get()) + return asyncContextData->getInternalField(0); + return jsUndefined(); + } + + // Snapshot the current async context alongside userContext in an + // InternalFieldTuple [userContext, asyncContext]. When no async context is + // active, returns userContext unchanged so the caller keeps using the + // allocation-free inline/slim reaction fast paths. + static ALWAYS_INLINE JSValue wrapWithCurrent(VM& vm, JSGlobalObject* globalObject, JSValue userContext) + { + JSValue asyncContext = current(globalObject); + if (asyncContext.isUndefined()) + return userContext; + return InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), userContext, asyncContext); + } + +private: + VM& m_vm; + InternalFieldTuple* m_asyncContextData { nullptr }; + JSValue m_restoreAsyncContext; +}; + +} // namespace JSC + +#endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index ba76b494c688..51d300d2ded5 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -64,7 +64,7 @@ #include "TopExceptionScope.h" #include "VMTrapsInlines.h" #if USE(BUN_JSC_ADDITIONS) -#include "InternalFieldTuple.h" +#include "AsyncContextSwapScope.h" extern "C" __attribute__((weak)) void Bun__reportUnhandledError(JSC::JSGlobalObject*, JSC::EncodedJSValue); #endif #if ENABLE(WEBASSEMBLY) @@ -1612,34 +1612,16 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::PromiseResolveThenableJobFast: { auto* promise = uncheckedDowncast(arguments[0]); auto* promiseToResolve = uncheckedDowncast(arguments[1]); -#if USE(BUN_JSC_ADDITIONS) - JSValue asyncContext = arguments[2]; -#endif if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]] RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve)); #if USE(BUN_JSC_ADDITIONS) - // Set up async context for promise resolution - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } + AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]); #endif scope.release(); promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, promiseToResolve, jsUndefined()); - -#if USE(BUN_JSC_ADDITIONS) - // Restore async context - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1660,28 +1642,11 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas JSValue then = arguments[1]; JSPromise* promiseToResolve = uncheckedDowncast(arguments[2]); #if USE(BUN_JSC_ADDITIONS) - JSValue asyncContext = arguments[3]; - - // Set up async context for thenable resolution - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } + AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[3]); #endif auto [resolve, reject] = promiseToResolve->createResolvingFunctions(vm, globalObject); promiseResolveThenableJob(globalObject, promise, then, resolve, reject); - -#if USE(BUN_JSC_ADDITIONS) - // Restore async context after calling thenable's then method - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1692,29 +1657,14 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas JSValue context = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - // Extract async context from the context tuple and set it up before calling thenable's then method - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (auto* tuple = dynamicDowncast(context)) { - JSValue asyncContext = tuple->getInternalField(1); - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - } + // context may be an InternalFieldTuple [userContext, asyncContext]; the resolving + // functions keep the tuple as-is, so only peek at field 1 for the swap. + JSValue peek = context; + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(peek)); #endif auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, task, context); promiseResolveThenableJob(globalObject, promise, then, resolve, reject); - -#if USE(BUN_JSC_ADDITIONS) - // Restore async context after calling thenable's then method - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1775,34 +1725,12 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas JSValue promiseOrCapability = arguments[0]; JSValue handler = arguments[1]; #if USE(BUN_JSC_ADDITIONS) - // Extract userContext and asyncContext from arguments[3] - // If it's an InternalFieldTuple: [userContext, asyncContext] - // Otherwise: it's userContext directly (legacy behavior) - JSValue contextArg = arguments[3]; - JSValue userContext = jsUndefined(); - JSValue asyncContext = jsUndefined(); - - if (!contextArg.isEmpty() && contextArg.isCell()) { - if (auto* tuple = dynamicDowncast(contextArg)) { - userContext = tuple->getInternalField(0); - asyncContext = tuple->getInternalField(1); - } else { - userContext = contextArg; - } - } else if (!contextArg.isEmpty() && !contextArg.isUndefinedOrNull()) { - userContext = contextArg; - } - - // Set up async context before calling handler - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } + // arguments[3] is either an InternalFieldTuple [userContext, asyncContext] + // or userContext directly (legacy behavior). The scope stays active through + // resolvePromise/rejectPromise so thenables returned from the handler + // capture the correct async context, and restores on every return. + JSValue userContext = arguments[3]; + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(userContext)); #endif JSValue result; @@ -1822,29 +1750,17 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #endif if (catchScope.exception()) { if (promiseOrCapability.isUndefinedOrNull()) { -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif scope.release(); return; } error = catchScope.exception()->value(); if (!catchScope.clearExceptionExceptTermination()) [[unlikely]] { -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif scope.release(); return; } } if (promiseOrCapability.isUndefinedOrNull()) { -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif scope.release(); return; } @@ -1852,17 +1768,10 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas ASSERT(result || error); } - // Note: Keep async context active during resolvePromise/rejectPromise - // so that any thenables returned from the handler can capture the correct async context - if (error) { if (auto* promise = dynamicDowncast(promiseOrCapability)) { scope.release(); promise->rejectPromise(vm, error); -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1874,20 +1783,12 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas ASSERT(!arguments.hasOverflowed()); scope.release(); call(globalObject, reject, jsUndefined(), arguments, "reject is not a function"_s); -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } if (auto* promise = dynamicDowncast(promiseOrCapability)) { scope.release(); promise->resolvePromise(promise->realm(), vm, result); -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1899,10 +1800,6 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas ASSERT(!arguments.hasOverflowed()); scope.release(); call(globalObject, resolve, jsUndefined(), arguments, "resolve is not a function"_s); -#if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } @@ -1918,30 +1815,10 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas JSValue contextArg = arguments[2]; #if USE(BUN_JSC_ADDITIONS) - // Extract generator and async context from InternalFieldTuple if wrapped - JSAsyncFunctionGenerator* generator; - JSValue asyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - generator = uncheckedDowncast(tuple->getInternalField(0)); - asyncContext = tuple->getInternalField(1); - } else { - generator = uncheckedDowncast(contextArg); - asyncContext = jsUndefined(); - } - - // Set up Bun's async context before resuming the async function - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } -#else - auto* generator = uncheckedDowncast(contextArg); + // contextArg may be an InternalFieldTuple [generator, asyncContext]. + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); #endif + auto* generator = uncheckedDowncast(contextArg); JSGlobalObject* generatorGlobalObject = generator->realm(); JSGenerator::ResumeMode resumeMode = JSGenerator::ResumeMode::NormalMode; switch (static_cast(payload)) { @@ -1973,11 +1850,6 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas if (catchScope.exception()) { error = catchScope.exception()->value(); if (!catchScope.clearExceptionExceptTermination()) [[unlikely]] { -#if USE(BUN_JSC_ADDITIONS) - // Restore async context before returning - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif scope.release(); return; } @@ -1992,8 +1864,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas scope.release(); promise->reject(vm, error); #if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + asyncContextScope.restoreEarly(); #endif return; } @@ -2003,29 +1874,21 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas scope.release(); promise->resolve(generatorGlobalObject, vm, value); #if USE(BUN_JSC_ADDITIONS) - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + asyncContextScope.restoreEarly(); #endif return; } scope.release(); JSPromise::resolveWithInternalMicrotaskForAsyncAwait(generatorGlobalObject, vm, value, InternalMicrotask::AsyncFunctionResume, generator); -#if USE(BUN_JSC_ADDITIONS) - // Restore async context after capturing it for the next await iteration - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#endif return; } case InternalMicrotask::AsyncFromSyncIteratorContinue: case InternalMicrotask::AsyncFromSyncIteratorDone: { #if USE(BUN_JSC_ADDITIONS) - // Extract context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - if (auto* tuple = dynamicDowncast(contextArg)) - contextArg = tuple->getInternalField(0); + AsyncContextSwapScope::unwrapContextTuple(contextArg); auto* promise = uncheckedDowncast(asObject(contextArg)->getDirect(vm, vm.propertyNames->builtinNames().promisePrivateName())); RELEASE_AND_RETURN(scope, asyncFromSyncIteratorContinueOrDone(promise->realm(), vm, promise, contextArg, arguments[1], static_cast(payload), task == InternalMicrotask::AsyncFromSyncIteratorDone)); #else @@ -2035,119 +1898,47 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::AsyncGeneratorYieldAwaited: { -#if USE(BUN_JSC_ADDITIONS) - // Extract generator and async context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - contextArg = tuple->getInternalField(0); - JSValue asyncContext = tuple->getInternalField(1); - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - } +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif auto* generator = uncheckedDowncast(contextArg); scope.release(); asyncGeneratorYieldAwaited(generator->realm(), generator, arguments[1], static_cast(payload)); - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); return; -#else - auto* generator = uncheckedDowncast(arguments[2]); - RELEASE_AND_RETURN(scope, asyncGeneratorYieldAwaited(generator->realm(), generator, arguments[1], static_cast(payload))); -#endif } case InternalMicrotask::AsyncGeneratorBodyCallNormal: { -#if USE(BUN_JSC_ADDITIONS) - // Extract generator and async context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - contextArg = tuple->getInternalField(0); - JSValue asyncContext = tuple->getInternalField(1); - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - } +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif auto* generator = uncheckedDowncast(contextArg); scope.release(); asyncGeneratorBodyCallNormal(generator->realm(), generator, arguments[1], static_cast(payload)); - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); return; -#else - auto* generator = uncheckedDowncast(arguments[2]); - RELEASE_AND_RETURN(scope, asyncGeneratorBodyCallNormal(generator->realm(), generator, arguments[1], static_cast(payload))); -#endif } case InternalMicrotask::AsyncGeneratorBodyCallReturn: { -#if USE(BUN_JSC_ADDITIONS) - // Extract generator and async context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - contextArg = tuple->getInternalField(0); - JSValue asyncContext = tuple->getInternalField(1); - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - } +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif auto* generator = uncheckedDowncast(contextArg); scope.release(); asyncGeneratorBodyCallReturn(generator->realm(), generator, arguments[1], static_cast(payload)); - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); return; -#else - auto* generator = uncheckedDowncast(arguments[2]); - RELEASE_AND_RETURN(scope, asyncGeneratorBodyCallReturn(generator->realm(), generator, arguments[1], static_cast(payload))); -#endif } case InternalMicrotask::AsyncGeneratorAwaitReturn: { -#if USE(BUN_JSC_ADDITIONS) - // Extract generator and async context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - contextArg = tuple->getInternalField(0); - JSValue asyncContext = tuple->getInternalField(1); - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - } +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif auto* generator = uncheckedDowncast(contextArg); scope.release(); asyncGeneratorAwaitReturnContinuation(generator->realm(), generator, arguments[1], static_cast(payload)); - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); return; -#else - auto* generator = uncheckedDowncast(arguments[2]); - RELEASE_AND_RETURN(scope, asyncGeneratorAwaitReturnContinuation(generator->realm(), generator, arguments[1], static_cast(payload))); -#endif } case InternalMicrotask::PromiseFinallyReactionJob: { @@ -2157,54 +1948,17 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas // arguments[2] = context (JSSlimPromiseReaction: promise=resultPromise, handlerOrContext=onFinally) // OR InternalFieldTuple: [context, asyncContext] when Bun async context is present // payload = Fulfilled/Rejected status -#if USE(BUN_JSC_ADDITIONS) - // Extract context and async context from InternalFieldTuple if wrapped JSValue contextArg = arguments[2]; - JSSlimPromiseReaction* context; - JSValue asyncContext = jsUndefined(); - - if (contextArg.isCell()) { - if (auto* tuple = dynamicDowncast(contextArg)) { - context = uncheckedDowncast(tuple->getInternalField(0)); - asyncContext = tuple->getInternalField(1); - } else { - context = uncheckedDowncast(contextArg); - } - } else { - context = uncheckedDowncast(contextArg); - } - - // Set up async context before calling onFinally - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - - auto* resultPromise = uncheckedDowncast(arguments[0]); - scope.release(); - promiseFinallyReactionJob(resultPromise->realm(), vm, - resultPromise, - arguments[1], - context, - static_cast(payload)); - - // Restore async context - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); -#else +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif auto* resultPromise = uncheckedDowncast(arguments[0]); scope.release(); promiseFinallyReactionJob(resultPromise->realm(), vm, resultPromise, arguments[1], - uncheckedDowncast(arguments[2]), + uncheckedDowncast(contextArg), static_cast(payload)); -#endif return; } @@ -2239,17 +1993,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas if (callData.type == CallData::Type::None) return; - // Save and set async context - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - JSValue asyncContext = arguments[1]; - if (!asyncContext.isEmpty() && !asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } + AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[1]); { auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); @@ -2264,8 +2008,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas callMicrotask(globalObject, job, jsUndefined(), jobCell, "performMicrotask is not a function"_s, microtaskCall); // Restore async context before error reporting - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); + asyncContextScope.restoreEarly(); if (auto* exception = catchScope.exception()) { catchScope.clearException(); @@ -2301,43 +2044,16 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::AsyncModuleExecutionResume: { -#if USE(BUN_JSC_ADDITIONS) // resolveWithInternalMicrotaskForAsyncAwait wraps the module together // with Bun's async context in an InternalFieldTuple when an async - // context is active at await time. Unwrap it and restore the context - // across the resumption, mirroring InternalMicrotask::AsyncFunctionResume. + // context is active at await time. JSValue contextArg = arguments[2]; - JSModuleRecord* module; - JSValue asyncContext; - if (auto* tuple = dynamicDowncast(contextArg)) { - module = uncheckedDowncast(tuple->getInternalField(0)); - asyncContext = tuple->getInternalField(1); - } else { - module = uncheckedDowncast(contextArg); - asyncContext = jsUndefined(); - } - - InternalFieldTuple* asyncContextData = nullptr; - JSValue restoreAsyncContext; - if (!asyncContext.isUndefined()) { - asyncContextData = globalObject->m_asyncContextData.get(); - if (asyncContextData) { - restoreAsyncContext = asyncContextData->getInternalField(0); - asyncContextData->putInternalField(vm, 0, asyncContext); - } - } - - asyncModuleExecutionResume(module->realm(), vm, scope, module, arguments[1], static_cast(payload)); - - // Restore async context after capturing it for the next await iteration. - if (asyncContextData) - asyncContextData->putInternalField(vm, 0, restoreAsyncContext); - return; -#else - auto* module = uncheckedDowncast(arguments[2]); +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif + auto* module = uncheckedDowncast(contextArg); asyncModuleExecutionResume(module->realm(), vm, scope, module, arguments[1], static_cast(payload)); return; -#endif } case InternalMicrotask::ModuleRegistryFetchSettled: { diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index b7f8a3ebfd3a..06dd376e2be1 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -43,7 +43,7 @@ #include "TopExceptionScope.h" #include "VMInlines.h" #if USE(BUN_JSC_ADDITIONS) -#include "InternalFieldTuple.h" +#include "AsyncContextSwapScope.h" #endif namespace JSC { @@ -344,18 +344,8 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue bool rejectedCallable = onRejected.isCallable(); #if USE(BUN_JSC_ADDITIONS) - // Capture async context for promise reaction - // Wrap in InternalFieldTuple: [userContext (undefined), asyncContext] - JSValue context = jsUndefined(); - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { - JSValue asyncContext = asyncContextData->getInternalField(0); - if (!asyncContext.isUndefined()) { - auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); - tuple->putInternalField(vm, 0, jsUndefined()); // userContext - tuple->putInternalField(vm, 1, asyncContext); // asyncContext - context = tuple; - } - } + // Capture async context for promise reaction as [userContext (undefined), asyncContext]. + JSValue context = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, jsUndefined()); #endif switch (status()) { @@ -435,18 +425,15 @@ void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObje bool fulfilledCallable = onFulfilled.isCallable(); bool rejectedCallable = onRejected.isCallable(); - // Wrap userContext and asyncContext in InternalFieldTuple: [userContext, asyncContext] + // Wrap userContext and asyncContext in InternalFieldTuple: [userContext, asyncContext]. + // Unlike wrapWithCurrent, this wraps whenever userContext is defined even if + // asyncContext is not: callers may pass an InternalFieldTuple as userContext + // (e.g. ReadableStream async iterator), which PromiseReactionJob would otherwise + // unwrap as if it were [_, asyncContext]. JSValue context = userContext; - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { - JSValue asyncContext = asyncContextData->getInternalField(0); - // Always create a tuple if there's a user context or async context - if (!userContext.isUndefinedOrNull() || !asyncContext.isUndefined()) { - auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); - tuple->putInternalField(vm, 0, userContext); // userContext - tuple->putInternalField(vm, 1, asyncContext); // asyncContext - context = tuple; - } - } + JSValue asyncContext = AsyncContextSwapScope::current(globalObject); + if (!userContext.isUndefinedOrNull() || !asyncContext.isUndefined()) + context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), userContext, asyncContext); switch (status()) { case JSPromise::Status::Pending: { @@ -688,11 +675,7 @@ void JSPromise::resolvePromise(JSGlobalObject* globalObject, VM& vm, JSValue res auto* promise = uncheckedDowncast(resolutionObject); if (promise->isThenFastAndNonObservable()) { #if USE(BUN_JSC_ADDITIONS) - // Capture async context for thenable resolution - JSValue asyncContext = jsUndefined(); - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) - asyncContext = asyncContextData->getInternalField(0); - return promise->realm()->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJobFast, 0, resolutionObject, this, asyncContext); + return promise->realm()->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJobFast, 0, resolutionObject, this, AsyncContextSwapScope::current(globalObject)); #else return promise->realm()->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJobFast, 0, resolutionObject, this, jsUndefined()); #endif @@ -720,11 +703,7 @@ void JSPromise::resolvePromise(JSGlobalObject* globalObject, VM& vm, JSValue res return fulfillPromise(vm, resolutionObject); #if USE(BUN_JSC_ADDITIONS) - // Capture async context for thenable resolution - JSValue asyncContext = jsUndefined(); - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) - asyncContext = asyncContextData->getInternalField(0); - return globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJob, 0, resolutionObject, then, this, asyncContext); + return globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJob, 0, resolutionObject, then, this, AsyncContextSwapScope::current(globalObject)); #else return globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveThenableJob, 0, resolutionObject, then, this); #endif @@ -988,14 +967,7 @@ void JSPromise::resolveWithInternalMicrotaskForAsyncAwait(JSGlobalObject* global // Capture Bun's async context at the point of await and wrap it with the generator context. // This allows AsyncFunctionResume and related microtasks to restore the async context when // resuming the async function. - JSValue wrappedContext = context; - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { - JSValue asyncContext = asyncContextData->getInternalField(0); - if (!asyncContext.isUndefined()) { - auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), context, asyncContext); - wrappedContext = tuple; - } - } + JSValue wrappedContext = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, context); #define BUN_CONTEXT wrappedContext #else #define BUN_CONTEXT context diff --git a/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp b/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp index 158fb0f6f262..f87ad42ec007 100644 --- a/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp +++ b/Source/JavaScriptCore/runtime/JSPromisePrototype.cpp @@ -33,7 +33,7 @@ #include "JSPromise.h" #include "JSPromiseReaction.h" #if USE(BUN_JSC_ADDITIONS) -#include "InternalFieldTuple.h" +#include "AsyncContextSwapScope.h" #endif namespace JSC { @@ -293,18 +293,7 @@ JSC_DEFINE_HOST_FUNCTION(promiseProtoFuncFinally, (JSGlobalObject* globalObject, JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); auto* context = JSSlimPromiseReaction::create(vm, resultPromise, onFinally, /* isFulfill */ false, /* next */ nullptr); #if USE(BUN_JSC_ADDITIONS) - // Wrap context with async context in InternalFieldTuple: [context, asyncContext] - JSValue contextValue = context; - if (auto* asyncContextData = globalObject->m_asyncContextData.get()) { - JSValue asyncContext = asyncContextData->getInternalField(0); - if (!asyncContext.isUndefined()) { - auto* tuple = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure()); - tuple->putInternalField(vm, 0, context); - tuple->putInternalField(vm, 1, asyncContext); - contextValue = tuple; - } - } - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyReactionJob, resultPromise, contextValue); + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyReactionJob, resultPromise, AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, context)); #else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyReactionJob, resultPromise, context); #endif