Skip to content

Commit 71bb6cd

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

15 files changed

+241
-1
lines changed

doc/api/esm.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,74 @@ const dynamicLibrary = await import.source('./library.wasm');
760760
const instance = await WebAssembly.instantiate(dynamicLibrary, importObject);
761761
```
762762
763+
### JavaScript String Builtins
764+
765+
<!-- YAML
766+
added: REPLACEME
767+
-->
768+
769+
When importing WebAssembly modules, the
770+
[WebAssembly JS String Builtins Proposal][] is automatically enabled through the
771+
ESM Integration. This allows WebAssembly modules to directly use efficient
772+
compile-time string builtins from the `wasm:js-string` namespace.
773+
774+
For example, the following Wasm module exports a string `getLength` function using
775+
the `wasm:js-string` `length` builtin:
776+
777+
```text
778+
(module
779+
;; Compile-time import of the string length builtin.
780+
(import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))
781+
782+
;; Define getLength, taking a JS value parameter assumed to be a string,
783+
;; calling string length on it and returning the result.
784+
(func $getLength (param $str externref) (result i32)
785+
local.get $str
786+
call $string_length
787+
)
788+
789+
;; Export the getLength function.
790+
(export "getLength" (func $get_length))
791+
)
792+
```
793+
794+
```js
795+
import { getLength } from './string-len.wasm';
796+
getLength('foo'); // Returns 3.
797+
```
798+
799+
Wasm builtins are compile-time imports that are linked during module compilation
800+
rather than during instantiation. They do not behave like normal module graph
801+
imports and they cannot be inspected via `WebAssembly.Module.imports(mod)`
802+
or virtualized unless recompiling the module using the direct
803+
`WebAssembly.compile` API with string builtins disabled.
804+
805+
Importing a module in the source phase before it has been instantiated will also
806+
use the compile-time builtins automatically:
807+
808+
```js
809+
import source mod from './string-len.wasm';
810+
const { exports: { getLength } } = await WebAssembly.instantiate(mod, {});
811+
getLength('foo'); // Also returns 3.
812+
```
813+
814+
### Reserved Wasm Namespaces
815+
816+
<!-- YAML
817+
added: REPLACEME
818+
-->
819+
820+
When importing WebAssembly modules through the ESM Integration, they cannot use
821+
import module names or import/export names that start with reserved prefixes:
822+
823+
* `wasm-js:` - reserved in all module import names, module names and export
824+
names.
825+
* `wasm:` - reserved in module import names and export names (imported module
826+
names are allowed in order to support future builtin polyfills).
827+
828+
Importing a module using the above reserved names will throw a
829+
`WebAssembly.LinkError`.
830+
763831
<i id="esm_experimental_top_level_await"></i>
764832
765833
## Top-level `await`
@@ -1202,6 +1270,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
12021270
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
12031271
[Terminology]: #terminology
12041272
[URL]: https://url.spec.whatwg.org/
1273+
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
12051274
[`"exports"`]: packages.md#exports
12061275
[`"type"`]: packages.md#type
12071276
[`--input-type`]: cli.md#--input-typetype

lib/internal/modules/esm/translators.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,10 @@ translators.set('wasm', async function(url, source) {
506506
// TODO(joyeecheung): implement a translator that just uses
507507
// compiled = new WebAssembly.Module(source) to compile it
508508
// synchronously.
509-
compiled = await WebAssembly.compile(source);
509+
compiled = await WebAssembly.compile(source, {
510+
// The ESM Integration auto-enables Wasm JS builtins by default when available.
511+
builtins: ['js-string'],
512+
});
510513
} catch (err) {
511514
err.message = errPath(url) + ': ' + err.message;
512515
throw err;
@@ -518,6 +521,13 @@ translators.set('wasm', async function(url, source) {
518521
if (impt.kind === 'global') {
519522
ArrayPrototypePush(wasmGlobalImports, impt);
520523
}
524+
// Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
525+
if (impt.module.startsWith('wasm-js:')) {
526+
throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
527+
}
528+
if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) {
529+
throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
530+
}
521531
importsList.add(impt.module);
522532
}
523533

@@ -527,6 +537,9 @@ translators.set('wasm', async function(url, source) {
527537
if (expt.kind === 'global') {
528538
wasmGlobalExports.add(expt.name);
529539
}
540+
if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) {
541+
throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
542+
}
530543
exportsList.add(expt.name);
531544
}
532545

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

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

0 commit comments

Comments
 (0)