GeezerVison is showing github.com — esm: js-string Wasm builtins in ESM Integration · nodejs/node@71bb6cd
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +69Lines changed: 69 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -760,6 +760,74 @@ const dynamicLibrary = await import.source('./library.wasm'); | 760 | 760 | const instance = await WebAssembly.instantiate(dynamicLibrary, importObject); | 761 | 761 | ``` | 762 | 762 |
| | 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 | +
| 763 | 831 | <i id="esm_experimental_top_level_await"></i> | 764 | 832 |
| 765 | 833 | ## Top-level `await` | @@ -1202,6 +1270,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][]. | 1202 | 1270 | [Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports | 1203 | 1271 | [Terminology]: #terminology | 1204 | 1272 | [URL]: https://url.spec.whatwg.org/ | | 1273 | +[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins | 1205 | 1274 | [`"exports"`]: packages.md#exports | 1206 | 1275 | [`"type"`]: packages.md#type | 1207 | 1276 | [`--input-type`]: cli.md#--input-typetype | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js +14-1Lines changed: 14 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -506,7 +506,10 @@ translators.set('wasm', async function(url, source) { | 506 | 506 | // TODO(joyeecheung): implement a translator that just uses | 507 | 507 | // compiled = new WebAssembly.Module(source) to compile it | 508 | 508 | // 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 | + }); | 510 | 513 | } catch (err) { | 511 | 514 | err.message = errPath(url) + ': ' + err.message; | 512 | 515 | throw err; | @@ -518,6 +521,13 @@ translators.set('wasm', async function(url, source) { | 518 | 521 | if (impt.kind === 'global') { | 519 | 522 | ArrayPrototypePush(wasmGlobalImports, impt); | 520 | 523 | } | | 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 | + } | 521 | 531 | importsList.add(impt.module); | 522 | 532 | } | 523 | 533 |
| @@ -527,6 +537,9 @@ translators.set('wasm', async function(url, source) { | 527 | 537 | if (expt.kind === 'global') { | 528 | 538 | wasmGlobalExports.add(expt.name); | 529 | 539 | } | | 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 | + } | 530 | 543 | exportsList.add(expt.name); | 531 | 544 | } | 532 | 545 |
| |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-wasm.mjs +91Lines changed: 91 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -403,4 +403,95 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () => | 403 | 403 | strictEqual(stdout, ''); | 404 | 404 | notStrictEqual(code, 0); | 405 | 405 | }); | | 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 | + }); | 406 | 497 | }); |
Collapse file Copy file name to clipboard Collapse file Copy file name to clipboard +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +) |
Collapse file Copy file name to clipboard Collapse file Copy file name to clipboard +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +) |
Collapse file Copy file name to clipboard Collapse file Copy file name to clipboard +8Lines changed: 8 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +) |
Collapse file Copy file name to clipboard
|