Compare commits

...

2 Commits

Author SHA1 Message Date
Sosuke Suzuki
9d36605331 Update preview build 2025-12-28 19:39:00 +09:00
Sosuke Suzuki
b780b96806 refactor(async-context): replace InternalFieldTuple access with accessor methods
- Replace direct m_asyncContextData.get()->getInternalField(0) and putInternalField() calls with cleaner asyncContext() and setAsyncContext() accessor methods
- Add new $getAsyncContext and $setAsyncContext builtin functions in JavaScript
- Update all C++ files to use the new accessors
- Update JavaScript builtins to use the new functions

This refactoring enables NodeVMGlobalObject to delegate async context to its parent Zig::GlobalObject, which is important for proper AsyncLocalStorage behavior across VM context boundaries.
2025-12-28 18:36:12 +09:00
13 changed files with 85 additions and 62 deletions

View File

@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 863778130931e0081a688f48e8479b8ee61b9507)
set(WEBKIT_VERSION preview-pr-122-786e646a)
endif()
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)

View File

@@ -36,7 +36,7 @@ JSC::Structure* AsyncContextFrame::createStructure(JSC::VM& vm, JSC::JSGlobalObj
JSValue AsyncContextFrame::withAsyncContextIfNeeded(JSGlobalObject* globalObject, JSValue callback)
{
JSValue context = globalObject->m_asyncContextData.get()->getInternalField(0);
JSValue context = globalObject->asyncContext();
// If there is no async context, do not snapshot the callback.
if (context.isUndefined()) {
@@ -96,16 +96,16 @@ extern "C" JSC::EncodedJSValue AsyncContextFrame__withAsyncContextIfNeeded(JSGlo
return jsUndefined(); \
auto& vm = global->vm(); \
JSValue restoreAsyncContext; \
InternalFieldTuple* asyncContextData = nullptr; \
bool hasAsyncContext = false; \
if (auto* wrapper = jsDynamicCast<AsyncContextFrame*>(functionObject)) { \
functionObject = jsCast<JSC::JSObject*>(wrapper->callback.get()); \
asyncContextData = global->m_asyncContextData.get(); \
restoreAsyncContext = asyncContextData->getInternalField(0); \
asyncContextData->putInternalField(vm, 0, wrapper->context.get()); \
restoreAsyncContext = global->asyncContext(); \
global->setAsyncContext(vm, wrapper->context.get()); \
hasAsyncContext = true; \
} \
auto result = JSC::profiledCall(__VA_ARGS__); \
if (asyncContextData) { \
asyncContextData->putInternalField(vm, 0, restoreAsyncContext); \
if (hasAsyncContext) { \
global->setAsyncContext(vm, restoreAsyncContext); \
} \
return result;

View File

@@ -88,7 +88,7 @@ void JSNextTickQueue::drain(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
if (!isEmpty()) {
RETURN_IF_EXCEPTION(throwScope, );
if (mustResetContext) {
globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined());
globalObject->setAsyncContext(vm, jsUndefined());
RETURN_IF_EXCEPTION(throwScope, );
}
auto* drainFn = internalField(2).get().getObject();

View File

@@ -22,13 +22,13 @@ static bool call(JSGlobalObject* globalObject, JSValue timerObject, JSValue call
auto scope = DECLARE_CATCH_SCOPE(vm);
JSValue restoreAsyncContext {};
JSC::InternalFieldTuple* asyncContextData = nullptr;
bool hasAsyncContext = false;
if (auto* wrapper = jsDynamicCast<AsyncContextFrame*>(callbackValue)) {
callbackValue = wrapper->callback.get();
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, wrapper->context.get());
restoreAsyncContext = globalObject->asyncContext();
globalObject->setAsyncContext(vm, wrapper->context.get());
hasAsyncContext = true;
}
if (auto* promise = jsDynamicCast<JSPromise*>(callbackValue)) {
@@ -66,8 +66,8 @@ static bool call(JSGlobalObject* globalObject, JSValue timerObject, JSValue call
hadException = true;
}
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
if (hasAsyncContext) {
globalObject->setAsyncContext(vm, restoreAsyncContext);
}
return hadException;

View File

@@ -789,16 +789,16 @@ void NodeVMGlobalObject::finishCreation(JSC::VM& vm)
vm.ensureTerminationException();
// Share the async context data with the parent Zig::GlobalObject.
// Delegate async context to the parent Zig::GlobalObject.
// This is necessary because AsyncLocalStorage methods (run, getStore, etc.) are defined
// in the parent realm and reference the parent's $asyncContext. However, microtask
// processing (JSMicrotask.cpp) operates on this NodeVMGlobalObject's m_asyncContextData.
// By sharing the same InternalFieldTuple, both the JS code and C++ microtask handling
// in the parent realm and reference the parent's $asyncContext. However, C++ code
// (JSMicrotask.cpp, etc.) uses this NodeVMGlobalObject's asyncContext() accessor.
// By delegating to the parent, both the JS code and C++ async context handling
// will operate on the same async context, ensuring proper AsyncLocalStorage behavior
// across await boundaries in VM contexts.
auto* parentGlobalObject = defaultGlobalObject(this);
if (parentGlobalObject && parentGlobalObject->m_asyncContextData) {
m_asyncContextData.set(vm, this, parentGlobalObject->m_asyncContextData.get());
if (parentGlobalObject) {
setAsyncContextDelegateParent(parentGlobalObject);
}
}

View File

@@ -358,7 +358,7 @@ static void checkIfNextTickWasCalledDuringMicrotask(JSC::VM& vm)
static void cleanupAsyncHooksData(JSC::VM& vm)
{
auto* globalObject = defaultGlobalObject();
globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined());
globalObject->setAsyncContext(vm, jsUndefined());
globalObject->asyncHooksNeedsCleanup = false;
if (!globalObject->m_nextTickQueue) {
vm.setOnEachMicrotaskTick(&checkIfNextTickWasCalledDuringMicrotask);
@@ -1058,7 +1058,7 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
RETURN_IF_EXCEPTION(scope, {});
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSC::JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
JSC::JSValue asyncContext = globalObject->asyncContext();
auto function = globalObject->performMicrotaskFunction();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(function, "Invalid microtask function");
@@ -1340,6 +1340,23 @@ JSC_DEFINE_HOST_FUNCTION(functionCreateUninitializedArrayBuffer,
RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSArrayBuffer::create(globalObject->vm(), globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTF::move(arrayBuffer))));
}
JSC_DECLARE_HOST_FUNCTION(functionGetAsyncContext);
JSC_DEFINE_HOST_FUNCTION(functionGetAsyncContext,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame*))
{
return JSValue::encode(globalObject->asyncContext());
}
JSC_DECLARE_HOST_FUNCTION(functionSetAsyncContext);
JSC_DEFINE_HOST_FUNCTION(functionSetAsyncContext,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();
JSValue value = callFrame->argument(0);
globalObject->setAsyncContext(vm, value);
return JSValue::encode(value);
}
static inline JSC::EncodedJSValue jsFunctionAddEventListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, Zig::GlobalObject* castedThis)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
@@ -1573,12 +1590,12 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotask, (JSGlobalObject * globalObj
WTF::NakedPtr<JSC::Exception> exceptionPtr;
JSValue restoreAsyncContext = {};
InternalFieldTuple* asyncContextData = nullptr;
auto setAsyncContext = callframe->argument(1);
if (!setAsyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, setAsyncContext);
bool hasAsyncContext = false;
auto setAsyncContextArg = callframe->argument(1);
if (!setAsyncContextArg.isUndefined()) {
restoreAsyncContext = globalObject->asyncContext();
globalObject->setAsyncContext(vm, setAsyncContextArg);
hasAsyncContext = true;
}
size_t argCount = callframe->argumentCount();
@@ -1598,8 +1615,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotask, (JSGlobalObject * globalObj
JSC::profiledCall(globalObject, ProfilingReason::API, job, callData, jsUndefined(), arguments, exceptionPtr);
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
if (hasAsyncContext) {
globalObject->setAsyncContext(vm, restoreAsyncContext);
}
if (auto* exception = exceptionPtr.get()) {
@@ -1640,18 +1657,18 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionPerformMicrotaskVariadic, (JSGlobalObject * g
}
JSValue restoreAsyncContext = {};
InternalFieldTuple* asyncContextData = nullptr;
auto setAsyncContext = callframe->argument(2);
if (!setAsyncContext.isUndefined()) {
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, setAsyncContext);
bool hasAsyncContext = false;
auto setAsyncContextArg = callframe->argument(2);
if (!setAsyncContextArg.isUndefined()) {
restoreAsyncContext = globalObject->asyncContext();
globalObject->setAsyncContext(vm, setAsyncContextArg);
hasAsyncContext = true;
}
JSC::profiledCall(globalObject, ProfilingReason::API, job, callData, thisValue, arguments, exceptionPtr);
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
if (hasAsyncContext) {
globalObject->setAsyncContext(vm, restoreAsyncContext);
}
if (auto* exception = exceptionPtr.get()) {
@@ -2723,6 +2740,8 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)
putDirectBuiltinFunction(vm, this, builtinNames.overridableRequirePrivateName(), commonJSOverridableRequireCodeGenerator(vm), 0);
putDirectNativeFunction(vm, this, builtinNames.createUninitializedArrayBufferPrivateName(), 1, functionCreateUninitializedArrayBuffer, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
putDirectNativeFunction(vm, this, builtinNames.getAsyncContextPrivateName(), 0, functionGetAsyncContext, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
putDirectNativeFunction(vm, this, builtinNames.setAsyncContextPrivateName(), 1, functionSetAsyncContext, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
putDirectNativeFunction(vm, this, builtinNames.resolveSyncPrivateName(), 1, functionImportMeta__resolveSyncPrivate, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
putDirectNativeFunction(vm, this, builtinNames.createInternalModuleByIdPrivateName(), 1, InternalModuleRegistry::jsCreateInternalModuleById, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);

View File

@@ -2734,12 +2734,12 @@ extern "C" JSC::EncodedJSValue Bun__JSValue__call(JSC::JSGlobalObject* globalObj
JSC::JSValue jsThisObject = JSValue::decode(thisObject);
JSValue restoreAsyncContext;
InternalFieldTuple* asyncContextData = nullptr;
bool hasAsyncContext = false;
if (auto* wrapper = jsDynamicCast<AsyncContextFrame*>(jsObject)) {
jsObject = jsCast<JSC::JSFunction*>(wrapper->callback.get());
asyncContextData = globalObject->m_asyncContextData.get();
restoreAsyncContext = asyncContextData->getInternalField(0);
asyncContextData->putInternalField(vm, 0, wrapper->context.get());
restoreAsyncContext = globalObject->asyncContext();
globalObject->setAsyncContext(vm, wrapper->context.get());
hasAsyncContext = true;
}
if (!jsThisObject)
@@ -2771,8 +2771,8 @@ extern "C" JSC::EncodedJSValue Bun__JSValue__call(JSC::JSGlobalObject* globalObj
auto result = JSC::profiledCall(globalObject, ProfilingReason::API, jsObject, callData, jsThisObject, argList);
if (asyncContextData) {
asyncContextData->putInternalField(vm, 0, restoreAsyncContext);
if (hasAsyncContext) {
globalObject->setAsyncContext(vm, restoreAsyncContext);
}
RETURN_IF_EXCEPTION(scope, {});
@@ -3524,7 +3524,7 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
auto microtaskFunction = globalObject->performMicrotaskFunction();
auto rejectPromiseFunction = globalObject->rejectPromiseFunction();
auto asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0);
auto asyncContext = globalObject->asyncContext();
#if ASSERT_ENABLED
ASSERT_WITH_MESSAGE(microtaskFunction, "Invalid microtask function");
@@ -3540,7 +3540,7 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
value = jsUndefined();
}
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, globalObject, microtaskFunction, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, globalObject, microtaskFunction, rejectPromiseFunction, asyncContext, promise, value };
globalObject->vm().queueMicrotask(WTF::move(task));
RETURN_IF_EXCEPTION(scope, );
}
@@ -5390,7 +5390,7 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
Zig::GlobalObject* globalObject = static_cast<Zig::GlobalObject*>(arg0);
JSValue microtaskArgs[] = {
JSValue::decode(JSValue1),
globalObject->m_asyncContextData.get()->getInternalField(0),
globalObject->asyncContext(),
JSValue::decode(JSValue3),
JSValue::decode(JSValue4)
};

View File

@@ -439,6 +439,7 @@ declare function $flushAlgorithm(): TODO;
declare function $format(): TODO;
declare function $fulfillModuleSync(key: string): void;
declare function $get(): TODO;
declare function $getAsyncContext(): ReadonlyArray<any> | undefined;
declare function $getInternalWritableStream(writable: WritableStream): TODO;
declare function $handleEvent(): TODO;
declare function $hash(): TODO;
@@ -531,6 +532,7 @@ declare function $search(): TODO;
declare function $searchParams(): TODO;
declare function $self(): TODO;
declare function $sep(): TODO;
declare function $setAsyncContext<T extends ReadonlyArray<any> | undefined>(context: T): T;
declare function $setBody(): TODO;
declare function $setStatus(): TODO;
declare function $setup(): TODO;

View File

@@ -126,6 +126,7 @@ using namespace JSC;
macro(flushAlgorithm) \
macro(format) \
macro(fulfillModuleSync) \
macro(getAsyncContext) \
macro(getInternalWritableStream) \
macro(handleEvent) \
macro(hash) \
@@ -230,6 +231,7 @@ using namespace JSC;
macro(secure) \
macro(self) \
macro(sep) \
macro(setAsyncContext) \
macro(setBody) \
macro(setStatus) \
macro(setup) \

View File

@@ -299,8 +299,8 @@ export function initializeNextTickQueue(
var callback = tock.callback;
var args = tock.args;
var frame = tock.frame;
var restore = $getInternalField($asyncContext, 0);
$putInternalField($asyncContext, 0, frame);
var restore = $getAsyncContext();
$setAsyncContext(frame);
try {
if (args === undefined) {
callback();
@@ -326,7 +326,7 @@ export function initializeNextTickQueue(
} catch (e) {
reportUncaughtException(e);
} finally {
$putInternalField($asyncContext, 0, restore);
$setAsyncContext(restore);
}
}
@@ -353,7 +353,7 @@ export function initializeNextTickQueue(
// We want to avoid materializing the args if there are none because it's
// a waste of memory and Array.prototype.slice shows up in profiling.
args: $argumentCount() > 1 ? args : undefined,
frame: $getInternalField($asyncContext, 0),
frame: $getAsyncContext(),
});
$putInternalField(nextTickQueue, 0, 1);
}

View File

@@ -49,7 +49,7 @@ export function initializeReadableStream(
$putByIdDirectPrivate(this, "readableStreamController", null);
this.$bunNativePtr = $getByIdDirectPrivate(underlyingSource, "bunNativePtr") ?? undefined;
$putByIdDirectPrivate(this, "asyncContext", $getInternalField($asyncContext, 0));
$putByIdDirectPrivate(this, "asyncContext", $getAsyncContext());
const isDirect = underlyingSource.type === "direct";
// direct streams are always lazy

View File

@@ -143,11 +143,11 @@ export function setupReadableStreamDefaultController(
const pullAlgorithm = () => $promiseInvokeOrNoopMethod(underlyingSource, pullMethod, [controller]);
const cancelAlgorithm = asyncContext
? reason => {
var prev = $getInternalField($asyncContext, 0);
$putInternalField($asyncContext, 0, asyncContext);
var prev = $getAsyncContext();
$setAsyncContext(asyncContext);
// this does not throw, but can returns a rejected promise
var result = $promiseInvokeOrNoopMethod(underlyingSource, cancelMethod, [reason]);
$putInternalField($asyncContext, 0, prev);
$setAsyncContext(prev);
return result;
}
: reason => $promiseInvokeOrNoopMethod(underlyingSource, cancelMethod, [reason]);
@@ -1035,8 +1035,8 @@ export function onPullDirectStream(controller: ReadableStreamDirectController) {
var asyncContext = stream.$asyncContext;
if (asyncContext) {
var prev = $getInternalField($asyncContext, 0);
$putInternalField($asyncContext, 0, asyncContext);
var prev = $getAsyncContext();
$setAsyncContext(asyncContext);
}
// Direct streams allow $pull to be called multiple times, unlike the spec.
@@ -1063,7 +1063,7 @@ export function onPullDirectStream(controller: ReadableStreamDirectController) {
controller._deferFlush = controller._deferClose = 0;
if (asyncContext) {
$putInternalField($asyncContext, 0, prev);
$setAsyncContext(prev);
}
}

View File

@@ -63,14 +63,14 @@ function debugFormatContextValue(value: ReadonlyArray<any> | undefined) {
}
function get(): ReadonlyArray<any> | undefined {
$debug("get", debugFormatContextValue($getInternalField($asyncContext, 0)));
return $getInternalField($asyncContext, 0);
$debug("get", debugFormatContextValue($getAsyncContext()));
return $getAsyncContext();
}
function set(contextValue: ReadonlyArray<any> | undefined) {
$assert(assertValidAsyncContextArray(contextValue));
$debug("set", debugFormatContextValue(contextValue));
return $putInternalField($asyncContext, 0, contextValue);
return $setAsyncContext(contextValue);
}
class AsyncLocalStorage {