diff --git a/docs/jsg.md b/docs/jsg.md index 194e010e9ff..2188fd8db51 100644 --- a/docs/jsg.md +++ b/docs/jsg.md @@ -1841,6 +1841,16 @@ v8::Local newExternalOneByteString(Lock& js, kj::ArrayPtr newExternalTwoByteString(Lock& js, kj::ArrayPtr buf); ``` +For dynamically allocated buffers, use the ownership-carrying overloads: + +```cpp +using OwnedAscii = kj::Array; +using OwnedUtf16 = kj::Array; + +v8::Local newExternalOneByteString(Lock& js, kj::Arc buf); +v8::Local newExternalTwoByteString(Lock& js, kj::Arc buf); +``` + **Important:** The `OneByteString` variant interprets the buffer as Latin-1, not UTF-8. --- @@ -2484,7 +2494,7 @@ bundles, import.meta support, import attributes. ```cpp auto esmModule = Module::newEsm("file:///bundle/worker.js"_url, - Module::Type::BUNDLE, kj::mv(code), + Module::Type::BUNDLE, kj::arc(kj::mv(code)), Module::Flags::MAIN | Module::Flags::ESM); auto syntheticModule = Module::newSynthetic("workerd:my-module"_url, @@ -2514,14 +2524,16 @@ auto wasmHandler = Module::newWasmModuleHandler(wasmBytes); Url bundleBase = "file:///bundle"_url; ModuleBundle::BundleBuilder bundleBuilder(bundleBase); bundleBuilder - .addEsmModule("worker.js", workerSource, Module::Flags::MAIN | Module::Flags::ESM) - .addEsmModule("utils.js", utilsSource) + .addEsmModule("worker.js", kj::arc(kj::mv(workerSource)), + Module::Flags::MAIN | Module::Flags::ESM) + .addEsmModule("utils.js", kj::arc(kj::mv(utilsSource))) .alias("./lib", "./utils.js"); auto workerBundle = bundleBuilder.finish(); // Builtin bundle ModuleBundle::BuiltinBuilder builtinBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN); builtinBuilder + // Builtin ESM source must have static process lifetime. .addEsm("node:buffer"_url, bufferSource) .addSynthetic("cloudflare:sockets"_url, socketsHandler) .addObject("workerd:my-api"_url); @@ -2543,7 +2555,7 @@ auto fallbackBundle = ModuleBundle::newFallbackBundle( -> kj::Maybe>> { KJ_IF_SOME(code, fetchModule(context.normalizedSpecifier)) { return Module::newEsm(context.normalizedSpecifier.clone(), - Module::Type::FALLBACK, kj::mv(code)); + Module::Type::FALLBACK, kj::arc(kj::mv(code))); } if (shouldRedirect(context.normalizedSpecifier)) { return kj::str("node:buffer"); // Redirect diff --git a/docs/reference/detail/new-module-registry.md b/docs/reference/detail/new-module-registry.md index 7ce15a2804d..bb2b84d6d31 100644 --- a/docs/reference/detail/new-module-registry.md +++ b/docs/reference/detail/new-module-registry.md @@ -160,9 +160,9 @@ module construction time. ``` EsModule extends Module { - source: kj::ArrayPtr // Raw UTF-8 source text - encodedSource: kj::Lazy // V8-compatible encoding (shared) - cachedData: MutexGuarded>> // Cross-isolate compile cache + source: kj::OneOf, kj::Arc> // UTF-8 + encodedSource: kj::Lazy // V8-compatible encoding (shared) + cachedData: MutexGuarded>> // Cross-isolate compile cache } ``` @@ -181,9 +181,9 @@ isolate that compiles the module: | Source content | Representation | | ---------------------------------- | ------------------------------------------------ | -| Pure ASCII (the common case) | Zero-copy one-byte external over the raw buffer | -| Non-ASCII, all code points ≤ U+00FF | One-time UTF-8→Latin-1 transcode (one-byte) | -| Anything else (CJK, emoji, ...) | One-time UTF-8→UTF-16 transcode (two-byte) | +| Pure ASCII (the common case) | Zero-copy one-byte external over static or Arc-owned storage | +| Non-ASCII, all code points ≤ U+00FF | One-time UTF-8→Latin-1 transcode into Arc-owned storage | +| Anything else (CJK, emoji, ...) | One-time UTF-8→UTF-16 transcode into Arc-owned storage | Invalid UTF-8 sequences are replaced with U+FFFD (via `kj::encodeUtf16`), matching `v8::String::NewFromUtf8`'s tolerance and the legacy registry. The @@ -274,12 +274,17 @@ to the `bundleBase` URL (typically `file:///bundle/`): ```cpp BundleBuilder builder(bundleBase); -builder.addEsmModule("index.js", source, Flags::ESM | Flags::MAIN); +builder.addEsmModule( + "index.js", kj::arc(kj::mv(source)), Flags::ESM | Flags::MAIN); builder.addSyntheticModule("data.json", Module::newJsonModuleHandler(jsonData)); builder.addWasmModule("module.wasm", wasmBytes); auto bundle = builder.finish(); ``` +The `kj::Arc` overload must be used for ordinary worker source. The +`ArrayPtr` overload is reserved for compiled-in strings with static process +lifetime because V8 can retain an external source string after compilation. + Name normalization (`normalizeModuleName`): 1. Parse name as URL relative to `bundleBase`. diff --git a/src/workerd/io/worker-modules.h b/src/workerd/io/worker-modules.h index 8edd9c36bf1..5c0807d3e20 100644 --- a/src/workerd/io/worker-modules.h +++ b/src/workerd/io/worker-modules.h @@ -141,20 +141,12 @@ static kj::Arc newWorkerModuleRegistry( if (def.name == source.mainModule) { flags = flags | jsg::modules::Module::Flags::MAIN; } - if (content.ownBody != kj::none) { - // When the source is owned (e.g. transpiled TypeScript), we must - // copy it into the module registry since the owning rust::String - // may not outlive the registry. - bundleBuilder.addEsmModule(def.name, kj::heapArray(content.body), flags); - } else { - // The content.body points into memory that outlives the module - // registry. In workerd this is a process-lifetime capnp message - // buffer; in edgeworker, it is the disowned script-fetcher response - // owned by the VirtualFileSystem (which is a sibling of the registry - // in Worker::Script::Impl). In edgeworker, the copy is ensured by - // shouldCopyScriptFetcherResponse() including the NMR flag. - bundleBuilder.addEsmModule(def.name, content.body, flags); - } + // Worker bundle storage is not necessarily static: transpiled source + // can be backed by a temporary rust::String, and edgeworker source is + // backed by a script-fetcher response. Copy it once into shared storage + // so V8 external strings can safely outlive the registry. + bundleBuilder.addEsmModule( + def.name, kj::arc(kj::heapArray(content.body)), flags); break; } KJ_CASE_ONEOF(content, Worker::Script::TextModule) { diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index e286e8cfed1..006ce44e5fd 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -3151,7 +3151,9 @@ class Lock { JsString str(kj::ArrayPtr) KJ_WARN_UNUSED_RESULT; JsString strIntern(kj::StringPtr) KJ_WARN_UNUSED_RESULT; JsString strExtern(kj::ArrayPtr) KJ_WARN_UNUSED_RESULT; + JsString strExtern(kj::Arc) KJ_WARN_UNUSED_RESULT; JsString strExtern(kj::ArrayPtr) KJ_WARN_UNUSED_RESULT; + JsString strExtern(kj::Arc) KJ_WARN_UNUSED_RESULT; JsSymbol symbol(kj::StringPtr) KJ_WARN_UNUSED_RESULT; JsSymbol symbolShared(kj::StringPtr) KJ_WARN_UNUSED_RESULT; JsSymbol symbolInternal(kj::StringPtr) KJ_WARN_UNUSED_RESULT; diff --git a/src/workerd/jsg/jsvalue.h b/src/workerd/jsg/jsvalue.h index 001ff18a062..1604161595b 100644 --- a/src/workerd/jsg/jsvalue.h +++ b/src/workerd/jsg/jsvalue.h @@ -1370,10 +1370,18 @@ inline JsString Lock::strExtern(kj::ArrayPtr str) { return JsString(newExternalOneByteString(*this, str)); } +inline JsString Lock::strExtern(kj::Arc str) { + return JsString(newExternalOneByteString(*this, kj::mv(str))); +} + inline JsString Lock::strExtern(kj::ArrayPtr str) { return JsString(newExternalTwoByteString(*this, str)); } +inline JsString Lock::strExtern(kj::Arc str) { + return JsString(newExternalTwoByteString(*this, kj::mv(str))); +} + inline JsObject Lock::obj() { return JsObject(v8::Object::New(v8Isolate)); } diff --git a/src/workerd/jsg/modules-new-test.c++ b/src/workerd/jsg/modules-new-test.c++ index a6e413b9e4a..1c6e585a5d2 100644 --- a/src/workerd/jsg/modules-new-test.c++ +++ b/src/workerd/jsg/modules-new-test.c++ @@ -1608,21 +1608,22 @@ KJ_TEST("Module source is decoded as UTF-8 across all encoding tiers") { // ====================================================================================== KJ_TEST("Owned ESM source outlives its release points across encoding tiers") { - // The kj::Array-taking addEsmModule overload hands ownership of the UTF-8 - // source buffer to the module. Non-ASCII sources are transcoded to an owned + // The Arc addEsmModule overload shares ownership of the UTF-8 source + // buffer with the module. Non-ASCII sources are transcoded to an owned // V8-compatible representation on first compile, after which the UTF-8 - // original is released; pure-ASCII owned sources must be retained because - // the raw buffer directly backs the external one-byte string. This test - // asserts correctness across repeated resolution; a buffer released too - // early (or read after release) is observed by ASAN builds. + // original is released; pure-ASCII owned sources become the shared encoded + // representation. V8 external strings retain shared ownership independently + // of the module. This test asserts correctness across repeated resolution; a + // buffer released too early (or read after release) is observed by ASAN builds. PREAMBLE([&](Lock& js) { CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); - bundleBuilder.addEsmModule( - "latin1-owned", kj::heapArray("export default 'caf\xc3\xa9';"_kj.asArray())); - bundleBuilder.addEsmModule( - "ascii-owned", kj::heapArray("export default 'plain';"_kj.asArray())); + bundleBuilder.addEsmModule("latin1-owned", + kj::arc( + kj::heapArray("export default 'caf\xc3\xa9';"_kj.asArray()))); + bundleBuilder.addEsmModule("ascii-owned", + kj::arc(kj::heapArray("export default 'plain';"_kj.asArray()))); auto registry = ModuleRegistry::Builder(BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); @@ -1646,6 +1647,51 @@ KJ_TEST("Owned ESM source outlives its release points across encoding tiers") { // ====================================================================================== +KJ_TEST("Compiled ESM functions outlive owned source across encoding tiers") { + PREAMBLE([&](Lock& js) { + CompilationObserver compilationObserver; + kj::Vector> functions; + + JSG_TRY(js) { + { + ModuleBundle::BundleBuilder bundleBuilder(BASE); + bundleBuilder.addEsmModule("ascii-lifetime", + kj::arc(kj::heapArray( + "export default function deferred() { return 'plain'; }"_kj.asArray()))); + bundleBuilder.addEsmModule("latin1-lifetime", + kj::arc(kj::heapArray( + "export default function deferred() { return 'caf\xc3\xa9'; }"_kj.asArray()))); + bundleBuilder.addEsmModule("utf16-lifetime", + kj::arc(kj::heapArray( + "export default function deferred() { return '\xe9\x83\xa8\xe5\x93\x81 \xf0\x9f\x8e\x89'; }"_kj + .asArray()))); + + auto registry = ModuleRegistry::Builder(BASE).add(bundleBuilder.finish()).finish(); + auto attached = registry->attachToIsolate(js, compilationObserver); + + for (auto specifier: {"file:///ascii-lifetime"_kj, "file:///latin1-lifetime"_kj, + "file:///utf16-lifetime"_kj}) { + auto value = ModuleRegistry::resolve(js, specifier); + auto function = KJ_ASSERT_NONNULL(value.tryCast()); + functions.add(JsRef(js, function)); + } + } + + // The exported functions remain live in V8 after the registry, bundles, + // and their owned source buffers have been destroyed. + KJ_ASSERT(kj::str(functions[0].getHandle(js).call(js, js.null())) == "plain"); + KJ_ASSERT(kj::str(functions[1].getHandle(js).call(js, js.null())) == "caf\xc3\xa9"); + KJ_ASSERT(kj::str(functions[2].getHandle(js).call(js, js.null())) == + "\xe9\x83\xa8\xe5\x93\x81 \xf0\x9f\x8e\x89"); + } + JSG_CATCH(exception) { + js.throwException(kj::mv(exception)); + } + }); +} + +// ====================================================================================== + KJ_TEST("Dynamic import from within a CJS-style eval module works") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; @@ -2161,8 +2207,9 @@ KJ_TEST("UNWRAP_DEFAULT honors module.exports, marker order, and builtin fallbac [](const ResolveContext& context) -> kj::Maybe>> { auto source = kj::heapArray( "export default 'fb-default'; export const named = 'fb';"_kj.asArray()); - return kj::Maybe>>(Module::newEsm( - context.normalizedSpecifier.clone(), Module::Type::FALLBACK, kj::mv(source))); + return kj::Maybe>>( + Module::newEsm(context.normalizedSpecifier.clone(), Module::Type::FALLBACK, + kj::arc(kj::mv(source)))); }); auto registry = ModuleRegistry::Builder(BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK) diff --git a/src/workerd/jsg/modules-new.c++ b/src/workerd/jsg/modules-new.c++ index 192712abcd6..cc48c7e2c5d 100644 --- a/src/workerd/jsg/modules-new.c++ +++ b/src/workerd/jsg/modules-new.c++ @@ -106,10 +106,9 @@ kj::Array normalizeNamedExports(kj::Array namedExports) // The source text of an ES module in the representation handed to V8 for // compilation. V8 has no internal UTF-8 string representation — strings are -// either one-byte (Latin-1) or two-byte (UTF-16), and external source strings -// must be one of those two encodings. Worker bundle sources arrive as UTF-8 -// bytes, so each module's source is encoded once, lazily, on first compile, -// and the result is shared by every isolate that compiles the module: +// either one-byte (Latin-1) or two-byte (UTF-16). Worker bundle sources arrive +// as UTF-8 bytes, so each module's source is encoded once, lazily, on first +// compile, and the result is shared by every isolate that compiles the module: // // * Pure-ASCII source (the overwhelmingly common case — bundlers typically // escape non-ASCII): the original buffer directly backs a one-byte external @@ -125,19 +124,16 @@ kj::Array normalizeNamedExports(kj::Array namedExports) // every isolate replica sharing the registry agrees on it, keeping the shared // compile cache consistent. struct EncodedSource { - kj::OneOf, // pure-ASCII: borrows the original buffer - kj::Array, // owned Latin-1 transcode - kj::Array> // owned UTF-16 transcode + kj::OneOf, // borrowed process-lifetime ASCII + kj::Arc, // owned ASCII or Latin-1 + kj::Arc> // owned UTF-16 repr; }; -EncodedSource encodeSource(kj::ArrayPtr source) { - if (simdutf::validate_ascii(source.begin(), source.size())) { - // ASCII is a subset of Latin-1, so the raw bytes can back a one-byte - // external string directly. - return {.repr = source}; - } - +// Transcodes non-ASCII UTF-8 source into the one-byte or two-byte representation +// V8 requires. The returned EncodedSource owns its backing allocation, so it does +// not retain or borrow `source`. Invalid UTF-8 is decoded leniently below. +EncodedSource transcodeSource(kj::ArrayPtr source) { if (simdutf::validate_utf8(source.begin(), source.size())) { // Valid UTF-8. Prefer the half-size Latin-1 representation when every code // point permits it. The buffer is sized exactly, so with already-validated @@ -145,17 +141,17 @@ EncodedSource encodeSource(kj::ArrayPtr source) { auto latin1 = kj::heapArray(simdutf::latin1_length_from_utf8(source.begin(), source.size())); if (simdutf::convert_utf8_to_latin1(source.begin(), source.size(), latin1.begin()) != 0) { - return {.repr = kj::Array(kj::mv(latin1))}; + return {.repr = kj::arc(kj::mv(latin1))}; } auto utf16 = kj::heapArray(simdutf::utf16_length_from_utf8(source.begin(), source.size())); // simdutf writes char16_t; uint16_t is layout-identical and is the element - // type the external two-byte string API accepts. + // type the two-byte string API accepts. size_t written = simdutf::convert_utf8_to_utf16le( source.begin(), source.size(), reinterpret_cast(utf16.begin())); KJ_ASSERT(written == utf16.size()); - return {.repr = kj::Array(kj::mv(utf16))}; + return {.repr = kj::arc(kj::mv(utf16))}; } // Invalid UTF-8: take the (rare) lenient path, which substitutes U+FFFD for @@ -164,14 +160,32 @@ EncodedSource encodeSource(kj::ArrayPtr source) { auto utf16 = kj::encodeUtf16(source); auto owned = kj::heapArray(utf16.size()); memcpy(owned.begin(), utf16.begin(), utf16.size() * sizeof(uint16_t)); - return {.repr = kj::Array(kj::mv(owned))}; + return {.repr = kj::arc(kj::mv(owned))}; +} + +EncodedSource encodeSource(kj::ArrayPtr&& source) { + if (simdutf::validate_ascii(source.begin(), source.size())) { + // Borrowed input is known to have process lifetime. + return {.repr = kj::mv(source)}; + } + return transcodeSource(source); } +EncodedSource encodeSource(kj::Arc&& source) { + auto sourcePtr = source->asPtr(); + if (simdutf::validate_ascii(sourcePtr.begin(), sourcePtr.size())) { + return {.repr = kj::mv(source)}; + } + return transcodeSource(sourcePtr); +} + +using UnencodedSource = kj::OneOf, kj::Arc>; + // The implementation of Module for ESM. class EsModule final: public Module { public: - // Source borrowed from memory that outlives this module (e.g. the worker's - // capnp config buffer or compiled-in builtin source). + // Source borrowed from static process-lifetime storage, such as a + // compiled-in builtin source. explicit EsModule(Url id, Type type, Flags flags, kj::ArrayPtr source) : Module(kj::mv(id), type, flags | Flags::ESM | Flags::EVAL), source(source), @@ -180,14 +194,10 @@ class EsModule final: public Module { } // Source owned by this module (e.g. transpiled TypeScript or fallback-service // responses, where the original buffer is transient). - explicit EsModule(Url id, Type type, Flags flags, kj::Array code) + explicit EsModule(Url id, Type type, Flags flags, kj::Arc code) : Module(kj::mv(id), type, flags | Flags::ESM | Flags::EVAL), - ownedSource(kj::mv(code)), + source(kj::mv(code)), cachedData(kj::none) { - // The view is taken from the owning member (after member initialization) - // rather than from the constructor parameter, so it cannot be mistaken for - // a borrow of the parameter's stack storage. - source = KJ_ASSERT_NONNULL(ownedSource).asPtr(); KJ_DASSERT(isEsm()); } KJ_DISALLOW_COPY_AND_MOVE(EsModule); @@ -245,32 +255,26 @@ class EsModule final: public Module { // once, shared across all isolates compiling this module. See // EncodedSource for the tiering. kj::Lazy handles cross-thread once-init. const auto& encoded = encodedSource.get([this](kj::SpaceFor& space) { - auto result = space.construct(encodeSource(this->source)); - if (!result->repr.is>()) { - // The encoded representation is an owned transcode that does not - // borrow from the UTF-8 original, which now has no remaining readers: - // V8 re-reads source text (lazy compilation, toString) from the - // external string backed by the transcoded buffer, compile-cache - // generation reads the compiled script, and the /bundle virtual file - // system keeps its own copy of module bodies. If this module owns its - // source, release it. Mutating these members is safe here because - // this initializer runs exactly once, under kj::Lazy's internal lock, - // before the encoded result is published to any reader. - source = nullptr; - ownedSource = kj::none; + KJ_SWITCH_ONEOF(source) { + KJ_CASE_ONEOF(borrowed, kj::ArrayPtr) { + return space.construct(encodeSource(kj::mv(borrowed))); + } + KJ_CASE_ONEOF(owned, kj::Arc) { + return space.construct(encodeSource(kj::mv(owned))); + } } - return result; + KJ_UNREACHABLE; }); v8::Local contentStr; KJ_SWITCH_ONEOF(encoded.repr) { KJ_CASE_ONEOF(ascii, kj::ArrayPtr) { contentStr = js.strExtern(ascii); } - KJ_CASE_ONEOF(latin1, kj::Array) { - contentStr = js.strExtern(latin1); + KJ_CASE_ONEOF(oneByte, kj::Arc) { + contentStr = js.strExtern(oneByte.addRef()); } - KJ_CASE_ONEOF(utf16, kj::Array) { - contentStr = js.strExtern(utf16); + KJ_CASE_ONEOF(utf16, kj::Arc) { + contentStr = js.strExtern(utf16.addRef()); } } @@ -369,16 +373,14 @@ class EsModule final: public Module { return actuallyEvaluate(js, module, observer); } - // The UTF-8 source text, and — when this module owns its source — the owning - // buffer. Both are mutable so the encoding initializer can release the UTF-8 - // original once an owned transcode replaces it (see getDescriptor()); after - // that point `source` is null and must not be read, which holds because its - // only reader is the encoding initializer itself. - mutable kj::ArrayPtr source; - mutable kj::Maybe> ownedSource; + // The UTF-8 source text, either borrowed from process-lifetime storage or + // held through shared ownership. The encoding initializer moves this into + // EncodedSource; its only reader is the initializer itself. + mutable UnencodedSource source; // The source encoded into a V8-compatible external-string representation - // (see EncodedSource). Computed on first compile, shared across isolates. + // (see EncodedSource). Computed on first compile and shared across isolates; + // each V8 string retains an Arc to owned backing storage. kj::Lazy encodedSource; // The cachedData holds the cached compilation data for this module, if any. It is @@ -2038,7 +2040,7 @@ ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addEsmModule( } ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addEsmModule( - kj::StringPtr name, kj::Array source, Module::Flags flags) { + kj::StringPtr name, kj::Arc source, Module::Flags flags) { const auto url = processModuleName(name, bundleBase); add(url, [url = url.clone(), source = kj::mv(source), flags, type = type()]( @@ -2413,7 +2415,7 @@ kj::Own Module::newSynthetic(Url id, kj::mv(id), type, kj::mv(callback), kj::mv(namedExports), flags, contentType); } -kj::Own Module::newEsm(Url id, Type type, kj::Array code, Flags flags) { +kj::Own Module::newEsm(Url id, Type type, kj::Arc code, Flags flags) { // The module owns the source buffer (rather than having it attached to the // kj::Own) so that it can release the UTF-8 original once an owned transcoded // representation replaces it on first compile. diff --git a/src/workerd/jsg/modules-new.h b/src/workerd/jsg/modules-new.h index 9d5099445dd..16c1186faaa 100644 --- a/src/workerd/jsg/modules-new.h +++ b/src/workerd/jsg/modules-new.h @@ -404,14 +404,14 @@ class Module { Flags flags = Flags::NONE, ContentType contentType = ContentType::NONE); - // Creates a new ESM module that takes ownership of the given code array. + // Creates a new ESM module that shares ownership of the given code. // This is generally used to construct ESM modules from a worker bundle. static kj::Own newEsm( - Url id, Type type, kj::Array code, Flags flags = Flags::NONE); + Url id, Type type, kj::Arc code, Flags flags = Flags::NONE); - // Creates a new ESM module that does not take ownership of the given code - // array. This is used to construct ESM modules from compiled-in built-in - // modules. + // Creates a new ESM module that does not take ownership of the given code. + // The backing data must have static process lifetime. This is used to + // construct ESM modules from compiled-in built-in modules. // This variation of newEsm does not take Flags as none of the existing // Flags are relevant other than the ESM flag which will be set automatically. static kj::Own newEsm(Url id, Type type, kj::ArrayPtr code); @@ -567,15 +567,16 @@ class ModuleBundle { kj::Array namedExports = nullptr, Module::ContentType contentType = Module::ContentType::NONE) KJ_LIFETIMEBOUND; + // Adds source backed by static process-lifetime storage, such as a + // compiled-in string literal. Use the Arc overload for all + // other source buffers. BundleBuilder& addEsmModule(kj::StringPtr name, kj::ArrayPtr code, Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND; - // Overload that takes ownership of the source data. Use this when the - // source buffer may not outlive the module registry (e.g. transpiled - // TypeScript where the backing rust::String has shorter lifetime). + // Adds source with shared ownership of its backing storage. BundleBuilder& addEsmModule(kj::StringPtr name, - kj::Array code, + kj::Arc code, Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND; BundleBuilder& addWasmModule(kj::StringPtr name, @@ -601,6 +602,7 @@ class ModuleBundle { BuiltinBuilder& addSynthetic( const Url& id, BundleBuilder::EvaluateCallback callback) KJ_LIFETIMEBOUND; + // The source must be backed by static process-lifetime storage. BuiltinBuilder& addEsm(const Url& id, kj::ArrayPtr source) KJ_LIFETIMEBOUND; // Adds a module that is implemented in C++ as a jsg::Object diff --git a/src/workerd/jsg/util.c++ b/src/workerd/jsg/util.c++ index 62d9528022d..a22b50a097e 100644 --- a/src/workerd/jsg/util.c++ +++ b/src/workerd/jsg/util.c++ @@ -851,12 +851,14 @@ class ExternString: public Type { // IN THE SOFTWARE. public: + using Backing = kj::OneOf, kj::Arc>>; + inline const Data* data() const override { - return buf.begin(); + return getBuffer().begin(); } inline size_t length() const override { - return buf.size(); + return getBuffer().size(); } inline uint64_t byteLength() const { @@ -871,9 +873,8 @@ class ExternString: public Type { allocator.deallocate(this); } - static v8::MaybeLocal createExtern( - v8::Isolate* isolate, kj::ArrayPtr& buf) { - if (buf.size() == 0) { + static v8::MaybeLocal createExtern(v8::Isolate* isolate, Backing backing) { + if (getBuffer(backing).size() == 0) { return v8::String::Empty(isolate); } @@ -890,7 +891,7 @@ class ExternString: public Type { return v8::MaybeLocal(); } - auto resource = new (mem) ExternString(isolate, buf); + auto resource = new (mem) ExternString(isolate, kj::mv(backing)); v8::MaybeLocal str; if constexpr (kj::isSameType()) { @@ -913,11 +914,22 @@ class ExternString: public Type { private: v8::Isolate* isolate; - kj::ArrayPtr buf; + Backing backing; + + static kj::ArrayPtr getBuffer(const Backing& backing) { + if (backing.template is>()) { + return backing.template get>(); + } + return backing.template get>>()->asPtr(); + } + + kj::ArrayPtr getBuffer() const { + return getBuffer(backing); + } - inline ExternString(v8::Isolate* isolate, kj::ArrayPtr& buf) + inline ExternString(v8::Isolate* isolate, Backing backing) : isolate(isolate), - buf(buf) {} + backing(kj::mv(backing)) {} }; using ExternOneByteString = ExternString; @@ -927,10 +939,18 @@ v8::Local newExternalOneByteString(Lock& js, kj::ArrayPtr newExternalOneByteString(Lock& js, kj::Arc buf) { + return check(ExternOneByteString::createExtern(js.v8Isolate, kj::mv(buf))); +} + v8::Local newExternalTwoByteString(Lock& js, kj::ArrayPtr buf) { return check(ExternTwoByteString::createExtern(js.v8Isolate, buf)); } +v8::Local newExternalTwoByteString(Lock& js, kj::Arc buf) { + return check(ExternTwoByteString::createExtern(js.v8Isolate, kj::mv(buf))); +} + // ====================================================================================== // Module utilities diff --git a/src/workerd/jsg/util.h b/src/workerd/jsg/util.h index 3df7510597c..13777b0c425 100644 --- a/src/workerd/jsg/util.h +++ b/src/workerd/jsg/util.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -544,6 +545,9 @@ class ExternalStringAllocator { // Returns a singleton DefaultExternalStringAllocator. kj::Own defaultExternalStringAllocator(); +using OwnedAscii = kj::Array; +using OwnedUtf16 = kj::Array; + // Creates v8 Strings from buffers not on the v8 heap. These do not copy and do not // take ownership of the buf. The buf *must* point to a static constant with infinite // lifetime. @@ -558,6 +562,10 @@ kj::Own defaultExternalStringAllocator(); // that are not owned by the v8 heap. v8::Local newExternalOneByteString(Lock& js, kj::ArrayPtr buf); +// Creates a V8 external string whose resource shares ownership of `buf`. The backing +// allocation remains alive until both the caller and all V8 strings release their Arcs. +v8::Local newExternalOneByteString(Lock& js, kj::Arc buf); + // Creates v8 Strings from buffers not on the v8 heap. These do not copy and do not // take ownership of the buf. The buf *must* point to a static constant with infinite // lifetime. @@ -572,6 +580,9 @@ v8::Local newExternalOneByteString(Lock& js, kj::ArrayPtr newExternalTwoByteString(Lock& js, kj::ArrayPtr buf); +// Two-byte counterpart to the owning one-byte overload above. +v8::Local newExternalTwoByteString(Lock& js, kj::Arc buf); + // Use this type to mark APIs that are not implemented. Attempts to use the API will throw an // exception. // - Use Unimplemented as a method parameter type or struct field type to mark that diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index ed1f6d246d1..8e83b8bccdd 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -1067,7 +1067,7 @@ kj::Arc WorkerdApi::newWorkerdModuleRegistry( return kj::Maybe>>( jsg::modules::Module::newEsm(kj::mv(id), jsg::modules::Module::Type::FALLBACK, - kj::heapArray(content.body))); + kj::arc(kj::heapArray(content.body)))); } KJ_CASE_ONEOF(content, Worker::Script::TextModule) { auto ownedData = kj::str(content.body);