From bc0f19c44a11f0121f6a734e7ab6a80376719352 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:00:24 +0000 Subject: [PATCH 1/2] [JSC] A JSSourceCode traces the value its synthetic module exports A SyntheticSourceProvider generator is a closure. A generator that exports a JS value captured that value, and a closure cannot be traced, so the embedder had to root the value and decide when to release the root. The loader runs a generator every time it makes a module from the source: never, once, or more than once. No release point is right for all three. JSSourceCode::createWithPayload() makes the source of a synthetic module together with the value it exports. The cell traces the value, and JSModuleLoader::makeModule() passes it to the generator, which is a plain function (PayloadSyntheticSourceGenerator) and captures nothing. The three generator forms share one slot in SyntheticSourceProvider, and the payload takes the bytes that rounding up to the cell size left unused in JSSourceCode, so neither object grows. --- Source/JavaScriptCore/parser/SourceProvider.h | 43 ++++++++++++++----- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 2 +- .../JavaScriptCore/runtime/JSSourceCode.cpp | 21 +++++++++ Source/JavaScriptCore/runtime/JSSourceCode.h | 20 +++++++++ .../runtime/SyntheticModuleRecord.cpp | 5 ++- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/Source/JavaScriptCore/parser/SourceProvider.h b/Source/JavaScriptCore/parser/SourceProvider.h index 423b7514e2f09..259cd1f519952 100644 --- a/Source/JavaScriptCore/parser/SourceProvider.h +++ b/Source/JavaScriptCore/parser/SourceProvider.h @@ -43,6 +43,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN #include #include #include +#include #include #include #include @@ -348,15 +349,20 @@ class BuiltinsSourceProvider final : public StringSourceProvider { // binds to them; see SyntheticModuleRecord::tryCreateWithExportNamesAndValues(..., JSObject* lazyExportsSource). // The generator returns nullptr when it provided every value. using LazySyntheticSourceGenerator = WTF::Function& exportNames, MarkedArgumentBuffer& exportValues)>; + // A generator that captures nothing. The value it exports is the payload of the JSSourceCode that holds the + // provider: JSSourceCode::createWithPayload() makes the two together, and nothing else makes either. That cell + // traces the payload, so the generator can run every time a module is made from the source, which the loader + // does never, once, or more than once. + using PayloadSyntheticSourceGenerator = void (*)(JSGlobalObject*, Identifier, JSValue payload, Vector& exportNames, MarkedArgumentBuffer& exportValues); static Ref create(SyntheticSourceGenerator&& generator, const SourceOrigin& sourceOrigin, String sourceURL) { - return adoptRef(*new SyntheticSourceProvider(WTF::move(generator), nullptr, sourceOrigin, WTF::move(sourceURL))); + return adoptRef(*new SyntheticSourceProvider(Generator(WTF::InPlaceType, WTF::move(generator)), sourceOrigin, WTF::move(sourceURL))); } static Ref createWithLazyExports(LazySyntheticSourceGenerator&& generator, const SourceOrigin& sourceOrigin, String sourceURL) { - return adoptRef(*new SyntheticSourceProvider(nullptr, WTF::move(generator), sourceOrigin, WTF::move(sourceURL))); + return adoptRef(*new SyntheticSourceProvider(Generator(WTF::InPlaceType, WTF::move(generator)), sourceOrigin, WTF::move(sourceURL))); } // For a generator that evaluates a module written by the user to find out what it exports (a CommonJS module). @@ -371,6 +377,7 @@ class BuiltinsSourceProvider final : public StringSourceProvider { } bool isDeferred() const { return m_isDeferred; } + bool takesPayload() const { return std::holds_alternative(m_generator); } unsigned hash() const final { @@ -383,27 +390,41 @@ class BuiltinsSourceProvider final : public StringSourceProvider { } // Returns the object that exports declared without a value are read from, or nullptr if there are none. - JSObject* generate(JSGlobalObject* globalObject, Identifier moduleKey, Vector& exportNames, MarkedArgumentBuffer& exportValues) + // `payload` is JSSourceCode::payload() of the cell that holds this provider. Only a + // PayloadSyntheticSourceGenerator reads it. + JSObject* generate(JSGlobalObject* globalObject, Identifier moduleKey, Vector& exportNames, MarkedArgumentBuffer& exportValues, JSValue payload) { - if (m_lazyGenerator) - return m_lazyGenerator(globalObject, moduleKey, exportNames, exportValues); - m_generator(globalObject, moduleKey, exportNames, exportValues); + if (auto* generator = std::get_if(&m_generator)) { + ASSERT(payload); + (*generator)(globalObject, moduleKey, payload, exportNames, exportValues); + return nullptr; + } + if (auto* generator = std::get_if(&m_generator)) + return (*generator)(globalObject, moduleKey, exportNames, exportValues); + std::get(m_generator)(globalObject, moduleKey, exportNames, exportValues); return nullptr; } - private: - JS_EXPORT_PRIVATE SyntheticSourceProvider(SyntheticSourceGenerator&& generator, LazySyntheticSourceGenerator&& lazyGenerator, const SourceOrigin& sourceOrigin, String&& sourceURL, String&& preRedirectURL = String()) + friend class JSSourceCode; + + // A provider has one of the three generator forms, so they share one slot. + using Generator = Variant; + + static Ref createWithPayloadGenerator(PayloadSyntheticSourceGenerator generator, const SourceOrigin& sourceOrigin, String&& sourceURL) + { + return adoptRef(*new SyntheticSourceProvider(Generator(WTF::InPlaceType, generator), sourceOrigin, WTF::move(sourceURL))); + } + + JS_EXPORT_PRIVATE SyntheticSourceProvider(Generator&& generator, const SourceOrigin& sourceOrigin, String&& sourceURL, String&& preRedirectURL = String()) : SourceProvider(sourceOrigin, WTF::move(sourceURL), WTF::move(preRedirectURL), SourceTaintedOrigin::Untainted, TextPosition(), SourceProviderSourceType::Synthetic) , m_source("[native code]"_s) , m_generator(WTF::move(generator)) - , m_lazyGenerator(WTF::move(lazyGenerator)) { } String m_source; - SyntheticSourceGenerator m_generator; - LazySyntheticSourceGenerator m_lazyGenerator; + Generator m_generator; bool m_isDeferred { false }; }; diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index ef4f008c26d6a..5572a30611015 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -1507,7 +1507,7 @@ JSPromise* JSModuleLoader::makeModule(JSGlobalObject* globalObject, const Identi MarkedArgumentBuffer args; Vector exportNames; - JSObject* lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); + JSObject* lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args, jsSourceCode->payload()); RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); auto* moduleRecord = SyntheticModuleRecord::tryCreateWithExportNamesAndValues(globalObject, this, moduleKey, exportNames, args, lazyExportsSource); diff --git a/Source/JavaScriptCore/runtime/JSSourceCode.cpp b/Source/JavaScriptCore/runtime/JSSourceCode.cpp index 4e200e56dcfa4..f92e66ab9a266 100644 --- a/Source/JavaScriptCore/runtime/JSSourceCode.cpp +++ b/Source/JavaScriptCore/runtime/JSSourceCode.cpp @@ -37,4 +37,25 @@ void JSSourceCode::destroy(JSCell* cell) static_cast(cell)->~JSSourceCode(); } +#if USE(BUN_JSC_ADDITIONS) +JSSourceCode* JSSourceCode::createWithPayload(VM& vm, SyntheticSourceProvider::PayloadSyntheticSourceGenerator generator, JSValue payload, const SourceOrigin& sourceOrigin, String&& sourceURL) +{ + ASSERT(payload); + auto* result = create(vm, SourceCode(SyntheticSourceProvider::createWithPayloadGenerator(generator, sourceOrigin, WTF::move(sourceURL)))); + result->m_payload.set(vm, result, payload); + return result; +} + +template +void JSSourceCode::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_payload); +} + +DEFINE_VISIT_CHILDREN(JSSourceCode); +#endif + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/JSSourceCode.h b/Source/JavaScriptCore/runtime/JSSourceCode.h index 18f61d5eb82c2..c97ff1885a447 100644 --- a/Source/JavaScriptCore/runtime/JSSourceCode.h +++ b/Source/JavaScriptCore/runtime/JSSourceCode.h @@ -39,6 +39,9 @@ class JSSourceCode final : public JSCell { static constexpr DestructionMode needsDestruction = NeedsDestruction; DECLARE_EXPORT_INFO; +#if USE(BUN_JSC_ADDITIONS) + DECLARE_VISIT_CHILDREN; +#endif template static GCClient::IsoSubspace* subspaceFor(VM& vm) @@ -60,6 +63,14 @@ class JSSourceCode final : public JSCell { return create(vm, vm.sourceCodeStructure.get(), WTF::move(sourceCode)); } +#if USE(BUN_JSC_ADDITIONS) + // The source of a synthetic module that exports `payload`. JSModuleLoader::makeModule() passes `payload` to + // `generator` each time it makes a module from this source, and this cell keeps `payload` alive until then. + JS_EXPORT_PRIVATE static JSSourceCode* createWithPayload(VM&, SyntheticSourceProvider::PayloadSyntheticSourceGenerator, JSValue payload, const SourceOrigin&, String&& sourceURL); + + JSValue payload() const { return m_payload.get(); } +#endif + const SourceCode& sourceCode() const { return m_sourceCode; @@ -75,6 +86,15 @@ class JSSourceCode final : public JSCell { } SourceCode m_sourceCode; +#if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_payload; +#endif }; +#if USE(BUN_JSC_ADDITIONS) && CPU(ADDRESS64) +// The payload takes the bytes that rounding up to the cell size left unused: a JSSourceCode is a 32-byte cell with it +// and without it. +static_assert(sizeof(JSSourceCode) <= 32); +#endif + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp index 1205bf2ff6e82..5ba6294cb5656 100644 --- a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp @@ -116,9 +116,12 @@ void SyntheticModuleRecord::runDeferredGenerator(JSGlobalObject* globalObject) }; Ref generator = *m_deferredGenerator; + // A deferred generator runs after makeModule() returned, without the JSSourceCode, so it is never one that takes + // that cell's payload. createDeferred() cannot make one. + ASSERT(!generator->takesPayload()); MarkedArgumentBuffer exportValues; Vector exportNames; - JSObject* lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues); + JSObject* lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues, { }); if (Exception* exception = scope.exception()) [[unlikely]] { failed(exception); return; From 47ef27fde5c8c93cda26d0ccbbc8ba6f2d03a490 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:43:09 +0000 Subject: [PATCH 2/2] SyntheticSourceProvider: say what createWithPayload() is the only way to make --- Source/JavaScriptCore/parser/SourceProvider.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/JavaScriptCore/parser/SourceProvider.h b/Source/JavaScriptCore/parser/SourceProvider.h index 259cd1f519952..d862c17251519 100644 --- a/Source/JavaScriptCore/parser/SourceProvider.h +++ b/Source/JavaScriptCore/parser/SourceProvider.h @@ -350,9 +350,9 @@ class BuiltinsSourceProvider final : public StringSourceProvider { // The generator returns nullptr when it provided every value. using LazySyntheticSourceGenerator = WTF::Function& exportNames, MarkedArgumentBuffer& exportValues)>; // A generator that captures nothing. The value it exports is the payload of the JSSourceCode that holds the - // provider: JSSourceCode::createWithPayload() makes the two together, and nothing else makes either. That cell - // traces the payload, so the generator can run every time a module is made from the source, which the loader - // does never, once, or more than once. + // provider: JSSourceCode::createWithPayload() makes the two together, and is the only way to make a provider + // with this form. That cell traces the payload, so the generator can run every time a module is made from the + // source, which the loader does never, once, or more than once. using PayloadSyntheticSourceGenerator = void (*)(JSGlobalObject*, Identifier, JSValue payload, Vector& exportNames, MarkedArgumentBuffer& exportValues); static Ref create(SyntheticSourceGenerator&& generator, const SourceOrigin& sourceOrigin, String sourceURL)