Skip to content

Commit 177ed3b

Browse files
guybedfordaduh95
authored andcommitted
esm: js-string Wasm builtins in ESM Integration
PR-URL: #59020 Backport-PR-URL: #59179 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent f64f5df commit 177ed3b

16 files changed

+246
-1
lines changed

doc/api/esm.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,74 @@ node --experimental-wasm-modules index.mjs
696696
697697
would provide the exports interface for the instantiation of `module.wasm`.
698698
699+
### JavaScript String Builtins
700+
701+
<!-- YAML
702+
added: REPLACEME
703+
-->
704+
705+
When importing WebAssembly modules, the
706+
[WebAssembly JS String Builtins Proposal][] is automatically enabled through the
707+
ESM Integration. This allows WebAssembly modules to directly use efficient
708+
compile-time string builtins from the `wasm:js-string` namespace.
709+
710+
For example, the following Wasm module exports a string `getLength` function using
711+
the `wasm:js-string` `length` builtin:
712+
713+
```text
714+
(module
715+
;; Compile-time import of the string length builtin.
716+
(import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))
717+
718+
;; Define getLength, taking a JS value parameter assumed to be a string,
719+
;; calling string length on it and returning the result.
720+
(func $getLength (param $str externref) (result i32)
721+
local.get $str
722+
call $string_length
723+
)
724+
725+
;; Export the getLength function.
726+
(export "getLength" (func $get_length))
727+
)
728+
```
729+
730+
```js
731+
import { getLength } from './string-len.wasm';
732+
getLength('foo'); // Returns 3.
733+
```
734+
735+
Wasm builtins are compile-time imports that are linked during module compilation
736+
rather than during instantiation. They do not behave like normal module graph
737+
imports and they cannot be inspected via `WebAssembly.Module.imports(mod)`
738+
or virtualized unless recompiling the module using the direct
739+
`WebAssembly.compile` API with string builtins disabled.
740+
741+
Importing a module in the source phase before it has been instantiated will also
742+
use the compile-time builtins automatically:
743+
744+
```js
745+
import source mod from './string-len.wasm';
746+
const { exports: { getLength } } = await WebAssembly.instantiate(mod, {});
747+
getLength('foo'); // Also returns 3.
748+
```
749+
750+
### Reserved Wasm Namespaces
751+
752+
<!-- YAML
753+
added: REPLACEME
754+
-->
755+
756+
When importing WebAssembly modules through the ESM Integration, they cannot use
757+
import module names or import/export names that start with reserved prefixes:
758+
759+
* `wasm-js:` - reserved in all module import names, module names and export
760+
names.
761+
* `wasm:` - reserved in module import names and export names (imported module
762+
names are allowed in order to support future builtin polyfills).
763+
764+
Importing a module using the above reserved names will throw a
765+
`WebAssembly.LinkError`.
766+
699767
<i id="esm_experimental_top_level_await"></i>
700768
701769
## Top-level `await`
@@ -1134,6 +1202,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
11341202
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
11351203
[Terminology]: #terminology
11361204
[URL]: https://url.spec.whatwg.org/
1205+
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
11371206
[`"exports"`]: packages.md#exports
11381207
[`"type"`]: packages.md#type
11391208
[`--experimental-default-type`]: cli.md#--experimental-default-typetype

lib/internal/modules/esm/translators.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@ translators.set('wasm', async function(url, source) {
444444
// TODO(joyeecheung): implement a translator that just uses
445445
// compiled = new WebAssembly.Module(source) to compile it
446446
// synchronously.
447-
compiled = await WebAssembly.compile(source);
447+
compiled = await WebAssembly.compile(source, {
448+
builtins: ['js-string'],
449+
});
448450
} catch (err) {
449451
err.message = errPath(url) + ': ' + err.message;
450452
throw err;
@@ -456,6 +458,13 @@ translators.set('wasm', async function(url, source) {
456458
if (impt.kind === 'global') {
457459
ArrayPrototypePush(wasmGlobalImports, impt);
458460
}
461+
// Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
462+
if (impt.module.startsWith('wasm-js:')) {
463+
throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
464+
}
465+
if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) {
466+
throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
467+
}
459468
importsList.add(impt.module);
460469
}
461470

@@ -465,6 +474,9 @@ translators.set('wasm', async function(url, source) {
465474
if (expt.kind === 'global') {
466475
wasmGlobalExports.add(expt.name);
467476
}
477+
if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) {
478+
throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
479+
}
468480
exportsList.add(expt.name);
469481
}
470482

@@ -487,6 +499,7 @@ translators.set('wasm', async function(url, source) {
487499
reflect.imports[impt] = wrappedModule;
488500
}
489501
}
502+
490503
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
491504
// instantiation, since all bindings will be in the Temporal Deadzone (TDZ).
492505
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);

src/node.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,11 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
826826
env_opts->abort_on_uncaught_exception = true;
827827
}
828828

829+
// Support stable Phase 5 WebAssembly proposals
830+
v8_args.emplace_back("--experimental-wasm-imported-strings");
831+
v8_args.emplace_back("--experimental-wasm-memory64");
832+
v8_args.emplace_back("--experimental-wasm-exnref");
833+
829834
#ifdef __POSIX__
830835
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the
831836
// performance penalty of frequent EINTR wakeups when the profiler is running.

test/es-module/test-esm-wasm.mjs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,4 +410,95 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
410410
strictEqual(stdout, '');
411411
notStrictEqual(code, 0);
412412
});
413+
414+
it('should reject wasm: import names', async () => {
415+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
416+
'--no-warnings',
417+
'--experimental-wasm-modules',
418+
'--input-type=module',
419+
'--eval',
420+
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name.wasm'))})`,
421+
]);
422+
423+
match(stderr, /Invalid Wasm import name/);
424+
strictEqual(stdout, '');
425+
notStrictEqual(code, 0);
426+
});
427+
428+
it('should reject wasm-js: import names', async () => {
429+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
430+
'--no-warnings',
431+
'--experimental-wasm-modules',
432+
'--input-type=module',
433+
'--eval',
434+
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name-wasm-js.wasm'))})`,
435+
]);
436+
437+
match(stderr, /Invalid Wasm import name/);
438+
strictEqual(stdout, '');
439+
notStrictEqual(code, 0);
440+
});
441+
442+
it('should reject wasm-js: import module names', async () => {
443+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
444+
'--no-warnings',
445+
'--experimental-wasm-modules',
446+
'--input-type=module',
447+
'--eval',
448+
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-module.wasm'))})`,
449+
]);
450+
451+
match(stderr, /Invalid Wasm import/);
452+
strictEqual(stdout, '');
453+
notStrictEqual(code, 0);
454+
});
455+
456+
it('should reject wasm: export names', async () => {
457+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
458+
'--no-warnings',
459+
'--experimental-wasm-modules',
460+
'--input-type=module',
461+
'--eval',
462+
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name.wasm'))})`,
463+
]);
464+
465+
match(stderr, /Invalid Wasm export/);
466+
strictEqual(stdout, '');
467+
notStrictEqual(code, 0);
468+
});
469+
470+
it('should reject wasm-js: export names', async () => {
471+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
472+
'--no-warnings',
473+
'--experimental-wasm-modules',
474+
'--input-type=module',
475+
'--eval',
476+
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name-wasm-js.wasm'))})`,
477+
]);
478+
479+
match(stderr, /Invalid Wasm export/);
480+
strictEqual(stdout, '');
481+
notStrictEqual(code, 0);
482+
});
483+
484+
it('should support js-string builtins', async () => {
485+
const { code, stderr, stdout } = await spawnPromisified(execPath, [
486+
'--no-warnings',
487+
'--experimental-wasm-modules',
488+
'--input-type=module',
489+
'--eval',
490+
[
491+
'import { strictEqual } from "node:assert";',
492+
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/js-string-builtins.wasm'))};`,
493+
'strictEqual(wasmExports.getLength("hello"), 5);',
494+
'strictEqual(wasmExports.concatStrings("hello", " world"), "hello world");',
495+
'strictEqual(wasmExports.compareStrings("test", "test"), 1);',
496+
'strictEqual(wasmExports.compareStrings("test", "different"), 0);',
497+
].join('\n'),
498+
]);
499+
500+
strictEqual(stderr, '');
501+
strictEqual(stdout, '');
502+
strictEqual(code, 0);
503+
});
413504
});
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
;; Test WASM module with invalid export name starting with 'wasm-js:'
2+
(module
3+
(func $test (result i32)
4+
i32.const 42
5+
)
6+
(export "wasm-js:invalid" (func $test))
7+
)
61 Bytes
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
;; Test WASM module with invalid export name starting with 'wasm:'
2+
(module
3+
(func $test (result i32)
4+
i32.const 42
5+
)
6+
(export "wasm:invalid" (func $test))
7+
)
Binary file not shown.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
;; Test WASM module with invalid import module name starting with 'wasm-js:'
2+
(module
3+
(import "wasm-js:invalid" "test" (func $invalidImport (result i32)))
4+
(export "test" (func $test))
5+
(func $test (result i32)
6+
call $invalidImport
7+
)
8+
)

0 commit comments

Comments
 (0)