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

Lines changed: 241 additions & 1 deletion

‎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
constinstance=awaitWebAssembly.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+
importsourcemodfrom'./string-len.wasm';
810+
const { exports: { getLength } } =awaitWebAssembly.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=awaitWebAssembly.compile(source);
509+
compiled=awaitWebAssembly.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
throwerr;
@@ -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+
thrownewWebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
527+
}
528+
if(impt.name.startsWith('wasm:')||impt.name.startsWith('wasm-js:')){
529+
thrownewWebAssembly.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+
thrownewWebAssembly.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 }=awaitspawnPromisified(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,/InvalidWasmimportname/);
417+
strictEqual(stdout,'');
418+
notStrictEqual(code,0);
419+
});
420+
421+
it('should reject wasm-js: import names',async()=>{
422+
const{ code, stderr, stdout }=awaitspawnPromisified(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,/InvalidWasmimportname/);
431+
strictEqual(stdout,'');
432+
notStrictEqual(code,0);
433+
});
434+
435+
it('should reject wasm-js: import module names',async()=>{
436+
const{ code, stderr, stdout }=awaitspawnPromisified(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,/InvalidWasmimport/);
445+
strictEqual(stdout,'');
446+
notStrictEqual(code,0);
447+
});
448+
449+
it('should reject wasm: export names',async()=>{
450+
const{ code, stderr, stdout }=awaitspawnPromisified(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,/InvalidWasmexport/);
459+
strictEqual(stdout,'');
460+
notStrictEqual(code,0);
461+
});
462+
463+
it('should reject wasm-js: export names',async()=>{
464+
const{ code, stderr, stdout }=awaitspawnPromisified(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,/InvalidWasmexport/);
473+
strictEqual(stdout,'');
474+
notStrictEqual(code,0);
475+
});
476+
477+
it('should support js-string builtins',async()=>{
478+
const{ code, stderr, stdout }=awaitspawnPromisified(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
});
64 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-js:'
2+
(module
3+
(func$test (resulti32)
4+
i32.const42
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 (resulti32)
4+
i32.const42
5+
)
6+
(export"wasm:invalid" (func$test))
7+
)
94 Bytes
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 (resulti32)))
4+
(export"test" (func$test))
5+
(func$test (resulti32)
6+
call$invalidImport
7+
)
8+
)
94 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)