Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/workerd/api/tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
load("@aspect_rules_js//js:defs.bzl", "js_binary")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//:build/wasm_tools_parse.bzl", "wasm_tools_parse")
load("//:build/wd_test.bzl", "wd_test")

wasm_tools_parse(
name = "js-string-builtins.wasm",
src = "js-string-builtins.wat",
)

exports_files(["starttls-server.pem"])

# Compile all ts test sources in one project. This is needed to work around https://github.com/aspect-build/rules_ts/issues/664 –
Expand Down Expand Up @@ -1123,10 +1129,19 @@ wd_test(
data = [
"new-module-registry-test.js",
"test.wasm",
":js-string-builtins.wasm",
],
lint = False,
)

wd_test(
src = "wasm-js-string-builtins-test.wd-test",
data = [
"wasm-js-string-builtins-test.js",
":js-string-builtins.wasm",
],
)

wd_test(
src = "new-module-registry-do-test.wd-test",
args = ["--experimental"],
Expand Down Expand Up @@ -1299,7 +1314,9 @@ wd_test(
args = ["--experimental"],
data = [
"test.wasm",
"worker-loader-wasm-builtins-test.js",
"worker-loader-wasm-test.js",
":js-string-builtins.wasm",
],
# The test uses `import source` syntax, which eslint's parser does not yet support.
lint = False,
Expand Down
12 changes: 12 additions & 0 deletions src/workerd/api/tests/js-string-builtins.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
;; Only links if the runtime compiles Wasm with
;; { builtins: ['js-string'], importedStringConstants: 'wasm:js/string-constants' }.
(module
(import "wasm:js-string" "length"
(func $length (param externref) (result i32)))

(import "wasm:js/string-constants" "hello world"
(global $hello externref))

(func (export "constantLength") (result i32)
(call $length (global.get $hello)))
)
30 changes: 30 additions & 0 deletions src/workerd/api/tests/new-module-registry-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,36 @@ export const wasmDynamicSourcePhaseFailureTest = {
},
};

// js-string-builtins.wasm imports the js-string `length` builtin and the
// "hello world" imported string constant, so it only links if the runtime
// compiles Wasm with js-string builtins and imported string constants enabled.
// Query-distinct specifiers give each import path its own compile.
async function assertJsStringBuiltinsLink(wasm) {
ok(wasm instanceof WebAssembly.Module);
const instance = await WebAssembly.instantiate(wasm, {});
strictEqual(instance.exports.constantLength(), 11);
}

export const wasmJsStringBuiltinsModuleTest = {
async test() {
const { default: wasm } = await import('wasm-js-string');
await assertJsStringBuiltinsLink(wasm);
},
};

import source wasmJsStringSource from 'wasm-js-string?1';
export const wasmJsStringBuiltinsSourcePhaseTest = {
async test() {
await assertJsStringBuiltinsLink(wasmJsStringSource);
},
};

export const wasmJsStringBuiltinsDynamicSourcePhaseTest = {
async test() {
await assertJsStringBuiltinsLink(await import.source('wasm-js-string?2'));
},
};

export const complexModuleTest = {
async test() {
const { abc } = await import('complex');
Expand Down
2 changes: 2 additions & 0 deletions src/workerd/api/tests/new-module-registry-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const unitTests :Workerd.Config = (
(name = "invalid-json", json = "1n"),

(name = "wasm", wasm = embed "test.wasm"),
(name = "wasm-js-string", wasm = embed "js-string-builtins.wasm"),

(name = "complex", esModule = "import * as abc from '././././././complex/complex2'; export { abc };"),
(name = "complex/complex2", esModule = "import * as def from '/bundle/complex/complex3'; export { def }; export const foo = 1;"),
Expand Down Expand Up @@ -134,6 +135,7 @@ const unitTests :Workerd.Config = (
"nodejs_compat_v2",
"new_module_registry",
"experimental",
"wasm_esm_integration_builtins",
],
bindings = [
(name = "unsafe", unsafeEval = void),
Expand Down
17 changes: 17 additions & 0 deletions src/workerd/api/tests/wasm-js-string-builtins-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

import { rejects } from 'node:assert';
import wasm from 'wasm-js-string';

// Without the wasm_esm_integration_builtins compat flag, the `wasm:*` imports are
// ordinary imports that must be supplied by the embedder.
export const wasmJsStringBuiltinsDisabledTest = {
async test() {
await rejects(WebAssembly.instantiate(wasm, {}), {
name: 'TypeError',
message: /wasm:js-string/,
});
},
};
15 changes: 15 additions & 0 deletions src/workerd/api/tests/wasm-js-string-builtins-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Workerd = import "/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
( name = "wasm-js-string-builtins-test",
worker = (
modules = [
(name = "worker", esModule = embed "wasm-js-string-builtins-test.js"),
(name = "wasm-js-string", wasm = embed "js-string-builtins.wasm"),
],
compatibilityFlags = ["nodejs_compat"],
),
),
],
);
49 changes: 49 additions & 0 deletions src/workerd/api/tests/worker-loader-wasm-builtins-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0
import assert from 'node:assert';
import source jsStringWasm from './js-string.wasm';

// This worker compiles with wasm_esm_integration_builtins, so the module passed to the dynamic
// worker was compiled with the builtins. The child does not enable the flag, so it must not
// share the parent's compiled code.
export let wasmModuleSourceBuiltinsDisabledInChild = {
async test(ctrl, env, ctx) {
const instance = await WebAssembly.instantiate(jsStringWasm, {});
assert.strictEqual(instance.exports.constantLength(), 11);

for (let extraFlags of [[], ['new_module_registry']]) {
let worker = env.loader.get(`wasmBuiltinsDisabled-${extraFlags}`, () => {
return {
compatibilityDate: '2025-01-01',
compatibilityFlags: extraFlags,
allowExperimental: extraFlags.length > 0,
mainModule: 'main.js',
modules: {
'main.js': `
import {WorkerEntrypoint} from "cloudflare:workers";
import source wasm from './js-string.wasm';

export default class extends WorkerEntrypoint {
async instantiate() {
try {
await WebAssembly.instantiate(wasm, {});
} catch (e) {
return e.name;
}
return 'ok';
}
}
`,
'js-string.wasm': jsStringWasm,
},
};
});

assert.strictEqual(
await worker.getEntrypoint().instantiate(),
'TypeError'
);
}
},
};
39 changes: 39 additions & 0 deletions src/workerd/api/tests/worker-loader-wasm-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,42 @@ export let wasmModuleSource = {
}
},
};

// This worker compiles without wasm_esm_integration_builtins, so the module passed to the
// dynamic worker was compiled without the builtins. The child enables the flag, so it must
// recompile from the wire bytes rather than share the parent's compiled code.
import source jsStringWasm from './js-string.wasm';
export let wasmModuleSourceBuiltinsMismatch = {
async test(ctrl, env, ctx) {
await assert.rejects(WebAssembly.instantiate(jsStringWasm, {}), {
name: 'TypeError',
});

for (let extraFlags of [[], ['new_module_registry']]) {
Comment thread
ask-bonk[bot] marked this conversation as resolved.
let worker = env.loader.get(`wasmBuiltinsMismatch-${extraFlags}`, () => {
return {
compatibilityDate: '2025-01-01',
compatibilityFlags: ['wasm_esm_integration_builtins', ...extraFlags],
allowExperimental: extraFlags.length > 0,
mainModule: 'main.js',
modules: {
'main.js': `
import {WorkerEntrypoint} from "cloudflare:workers";
import source wasm from './js-string.wasm';

export default class extends WorkerEntrypoint {
async constantLength() {
const instance = await WebAssembly.instantiate(wasm, {});
return instance.exports.constantLength();
}
}
`,
'js-string.wasm': jsStringWasm,
},
};
});

assert.strictEqual(await worker.getEntrypoint().constantLength(), 11);
}
},
};
13 changes: 13 additions & 0 deletions src/workerd/api/tests/worker-loader-wasm-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@ const unitTests :Workerd.Config = (
modules = [
(name = "worker", esModule = embed "worker-loader-wasm-test.js"),
(name = "math.wasm", wasm = embed "test.wasm"),
(name = "js-string.wasm", wasm = embed "js-string-builtins.wasm"),
],
compatibilityFlags = ["nodejs_compat", "experimental"],
bindings = [
(name = "loader", workerLoader = ()),
],
)
),
( name = "worker-loader-wasm-builtins-test",
worker = (
modules = [
(name = "worker", esModule = embed "worker-loader-wasm-builtins-test.js"),
(name = "js-string.wasm", wasm = embed "js-string-builtins.wasm"),
],
compatibilityFlags = ["nodejs_compat", "experimental", "wasm_esm_integration_builtins"],
bindings = [
(name = "loader", workerLoader = ()),
],
)
),
],
);
1 change: 1 addition & 0 deletions src/workerd/api/worker-loader.c++
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ static Worker::Script::ModuleContent extractWasmModuleContent(
return Worker::Script::WasmModule{
.body = kj::arrayPtr(wireBytes.data(), wireBytes.size()),
.compiledModule = kj::mv(compiled),
.compiledWithEsmIntegrationBuiltins = js.isWasmEsmIntegrationBuiltinsEnabled(),
};
}

Expand Down
8 changes: 8 additions & 0 deletions src/workerd/io/compatibility-date.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -1686,4 +1686,12 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
# key encapsulation helpers, getPublicKey(), SubtleCrypto.supports(), and AKP JWK support. It is
# explicitly gated because the draft is still changing and workerd does not implement the full
# proposal. The API may change as the draft evolves.

wasmEsmIntegrationBuiltins @192 :Bool
$compatEnableFlag("wasm_esm_integration_builtins");
# Compiles Wasm modules with the js-string builtins and imported string constants enabled,
# equivalent to `new WebAssembly.Module(src, { builtins: ['js-string'],
# importedStringConstants: 'wasm:js/string-constants' })`. The proposal is still in the
# implementation phase, so the feature is opt-in. Modules that don't import the `wasm:*`
# namespaces are unaffected.
}
18 changes: 16 additions & 2 deletions src/workerd/io/worker-modules.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ jsg::ModuleRegistry::ModuleInfo addCapnpModule(
}
} // namespace modules::capnp

// The compiled code of a Wasm module provided by another isolate, if that isolate compiled it
// with the same compile options this one would use.
inline kj::Maybe<v8::CompiledWasmModule> sharedCompiledWasmModule(
const Worker::Script::WasmModule& content, const CompatibilityFlags::Reader& featureFlags) {
KJ_IF_SOME(compiled, content.compiledModule) {
if (content.compiledWithEsmIntegrationBuiltins ==
featureFlags.getWasmEsmIntegrationBuiltins()) {
return compiled;
}
}
return kj::none;
}

// Creates an instance of the (new) ModuleRegistry. This method provides the
// initialization logic that is agnostic to the Worker::Api implementation,
// but accepts a callback parameter to handle the Worker::Api-specific details.
Expand Down Expand Up @@ -172,7 +185,8 @@ kj::Arc<jsg::modules::ModuleRegistry> newWorkerModuleRegistry(
// (see the ESM comment above for the ownership details). If the module was
// already compiled in another isolate, the compiled code is passed along to
// avoid recompilation.
bundleBuilder.addWasmModule(def.name, content.body, content.compiledModule);
bundleBuilder.addWasmModule(
def.name, content.body, sharedCompiledWasmModule(content, featureFlags));
break;
}
KJ_CASE_ONEOF(content, Worker::Script::JsonModule) {
Expand Down Expand Up @@ -337,7 +351,7 @@ kj::Maybe<jsg::ModuleRegistry::ModuleInfo> tryCompileLegacyModule(jsg::Lock& js,
}
KJ_CASE_ONEOF(content, Worker::Script::WasmModule) {
v8::Local<v8::WasmModuleObject> wasmModule;
KJ_IF_SOME(compiled, content.compiledModule) {
KJ_IF_SOME(compiled, sharedCompiledWasmModule(content, featureFlags)) {
// The module was already compiled in another isolate; share the compiled code rather
// than recompiling the wire bytes.
auto metrics = observer.onWasmCompilationFromCacheStart(js.v8Isolate);
Expand Down
5 changes: 5 additions & 0 deletions src/workerd/io/worker-source.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ struct WorkerSource {
// isolate to avoid recompiling. In this case `body` points at the module's wire bytes, which
// are owned by the compiled module.
kj::Maybe<v8::CompiledWasmModule> compiledModule;

// Whether `compiledModule` was compiled with the wasm_esm_integration_builtins compile
// options. Compiled code is only shared with a target whose own setting matches, since the
// setting is baked into the compiled module; otherwise the target recompiles from `body`.
bool compiledWithEsmIntegrationBuiltins = false;
};
struct JsonModule {
// JSON-encoded content; will be parsed automatically when imported.
Expand Down
3 changes: 3 additions & 0 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,9 @@ Worker::Isolate::Isolate(kj::Own<Api> apiParam,
if (features.getFastJsgStruct()) {
lock->setUsingFastJsgStruct();
}
if (features.getWasmEsmIntegrationBuiltins()) {
lock->setWasmEsmIntegrationBuiltinsEnabled();
}

if (impl->inspector != kj::none || ::kj::_::Debug::shouldLog(::kj::LogSeverity::INFO)) {
lock->setLoggerCallback([this](jsg::Lock& js, kj::StringPtr message) {
Expand Down
8 changes: 8 additions & 0 deletions src/workerd/jsg/jsg.c++
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ bool Lock::getThrowOnUnrecognizedImportAssertion() const {
return IsolateBase::from(v8Isolate).getThrowOnUnrecognizedImportAssertion();
}

void Lock::setWasmEsmIntegrationBuiltinsEnabled() {
IsolateBase::from(v8Isolate).setWasmEsmIntegrationBuiltinsEnabled();
}

bool Lock::isWasmEsmIntegrationBuiltinsEnabled() const {
return IsolateBase::from(v8Isolate).isWasmEsmIntegrationBuiltinsEnabled();
}

void Lock::disableTopLevelAwait() {
IsolateBase::from(v8Isolate).disableTopLevelAwait();
}
Expand Down
2 changes: 2 additions & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -3108,6 +3108,8 @@ class Lock {
void setRequireReturnsDefaultExportEnabled();
void setThrowOnUnrecognizedImportAssertion();
bool getThrowOnUnrecognizedImportAssertion() const;
void setWasmEsmIntegrationBuiltinsEnabled();
bool isWasmEsmIntegrationBuiltinsEnabled() const;
void setToStringTag();
void setImmutablePrototype();
void setSpecCompliantPropertyAttributes();
Expand Down
7 changes: 6 additions & 1 deletion src/workerd/jsg/modules.c++
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,13 @@ v8::Local<v8::WasmModuleObject> compileWasmModule(
// destroy the observer after compilation finishes to indicate the end of the process.
auto compilationObserver = observer.onWasmCompilationStart(js.v8Isolate, code.size());

v8::WasmModuleObject::CompileOptions options;
if (js.isWasmEsmIntegrationBuiltinsEnabled()) {
options.builtins = v8::WasmModuleObject::CompileOptions::Builtins::kJsString;
options.imported_string_constants_module = "wasm:js/string-constants";
}
return jsg::check(v8::WasmModuleObject::Compile(
Comment thread
ask-bonk[bot] marked this conversation as resolved.
js.v8Isolate, std::span<const uint8_t>(code.begin(), code.size())));
js.v8Isolate, std::span<const uint8_t>(code.begin(), code.size()), options));
}

// ======================================================================================
Expand Down
Loading
Loading