mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
esm: js-string Wasm builtins in ESM Integration
PR-URL: https://github.com/nodejs/node/pull/59020 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
parent
edd66d0130
commit
35e599b3d0
15 changed files with 241 additions and 1 deletions
|
@ -764,6 +764,74 @@ const dynamicLibrary = await import.source('./library.wasm');
|
|||
const instance = await WebAssembly.instantiate(dynamicLibrary, importObject);
|
||||
```
|
||||
|
||||
### JavaScript String Builtins
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
When importing WebAssembly modules, the
|
||||
[WebAssembly JS String Builtins Proposal][] is automatically enabled through the
|
||||
ESM Integration. This allows WebAssembly modules to directly use efficient
|
||||
compile-time string builtins from the `wasm:js-string` namespace.
|
||||
|
||||
For example, the following Wasm module exports a string `getLength` function using
|
||||
the `wasm:js-string` `length` builtin:
|
||||
|
||||
```text
|
||||
(module
|
||||
;; Compile-time import of the string length builtin.
|
||||
(import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))
|
||||
|
||||
;; Define getLength, taking a JS value parameter assumed to be a string,
|
||||
;; calling string length on it and returning the result.
|
||||
(func $getLength (param $str externref) (result i32)
|
||||
local.get $str
|
||||
call $string_length
|
||||
)
|
||||
|
||||
;; Export the getLength function.
|
||||
(export "getLength" (func $get_length))
|
||||
)
|
||||
```
|
||||
|
||||
```js
|
||||
import { getLength } from './string-len.wasm';
|
||||
getLength('foo'); // Returns 3.
|
||||
```
|
||||
|
||||
Wasm builtins are compile-time imports that are linked during module compilation
|
||||
rather than during instantiation. They do not behave like normal module graph
|
||||
imports and they cannot be inspected via `WebAssembly.Module.imports(mod)`
|
||||
or virtualized unless recompiling the module using the direct
|
||||
`WebAssembly.compile` API with string builtins disabled.
|
||||
|
||||
Importing a module in the source phase before it has been instantiated will also
|
||||
use the compile-time builtins automatically:
|
||||
|
||||
```js
|
||||
import source mod from './string-len.wasm';
|
||||
const { exports: { getLength } } = await WebAssembly.instantiate(mod, {});
|
||||
getLength('foo'); // Also returns 3.
|
||||
```
|
||||
|
||||
### Reserved Wasm Namespaces
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
When importing WebAssembly modules through the ESM Integration, they cannot use
|
||||
import module names or import/export names that start with reserved prefixes:
|
||||
|
||||
* `wasm-js:` - reserved in all module import names, module names and export
|
||||
names.
|
||||
* `wasm:` - reserved in module import names and export names (imported module
|
||||
names are allowed in order to support future builtin polyfills).
|
||||
|
||||
Importing a module using the above reserved names will throw a
|
||||
`WebAssembly.LinkError`.
|
||||
|
||||
<i id="esm_experimental_top_level_await"></i>
|
||||
|
||||
## Top-level `await`
|
||||
|
@ -1206,6 +1274,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
|
|||
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
|
||||
[Terminology]: #terminology
|
||||
[URL]: https://url.spec.whatwg.org/
|
||||
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
|
||||
[`"exports"`]: packages.md#exports
|
||||
[`"type"`]: packages.md#type
|
||||
[`--input-type`]: cli.md#--input-typetype
|
||||
|
|
|
@ -506,7 +506,10 @@ translators.set('wasm', async function(url, source) {
|
|||
// TODO(joyeecheung): implement a translator that just uses
|
||||
// compiled = new WebAssembly.Module(source) to compile it
|
||||
// synchronously.
|
||||
compiled = await WebAssembly.compile(source);
|
||||
compiled = await WebAssembly.compile(source, {
|
||||
// The ESM Integration auto-enables Wasm JS builtins by default when available.
|
||||
builtins: ['js-string'],
|
||||
});
|
||||
} catch (err) {
|
||||
err.message = errPath(url) + ': ' + err.message;
|
||||
throw err;
|
||||
|
@ -518,6 +521,13 @@ translators.set('wasm', async function(url, source) {
|
|||
if (impt.kind === 'global') {
|
||||
ArrayPrototypePush(wasmGlobalImports, impt);
|
||||
}
|
||||
// Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
|
||||
if (impt.module.startsWith('wasm-js:')) {
|
||||
throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
|
||||
}
|
||||
if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) {
|
||||
throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
|
||||
}
|
||||
importsList.add(impt.module);
|
||||
}
|
||||
|
||||
|
@ -527,6 +537,9 @@ translators.set('wasm', async function(url, source) {
|
|||
if (expt.kind === 'global') {
|
||||
wasmGlobalExports.add(expt.name);
|
||||
}
|
||||
if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) {
|
||||
throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
|
||||
}
|
||||
exportsList.add(expt.name);
|
||||
}
|
||||
|
||||
|
|
|
@ -403,4 +403,95 @@ describe('ESM: WASM modules', { concurrency: !process.env.TEST_PARALLEL }, () =>
|
|||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should reject wasm: import names', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name.wasm'))})`,
|
||||
]);
|
||||
|
||||
match(stderr, /Invalid Wasm import name/);
|
||||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should reject wasm-js: import names', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-name-wasm-js.wasm'))})`,
|
||||
]);
|
||||
|
||||
match(stderr, /Invalid Wasm import name/);
|
||||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should reject wasm-js: import module names', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-import-module.wasm'))})`,
|
||||
]);
|
||||
|
||||
match(stderr, /Invalid Wasm import/);
|
||||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should reject wasm: export names', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name.wasm'))})`,
|
||||
]);
|
||||
|
||||
match(stderr, /Invalid Wasm export/);
|
||||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should reject wasm-js: export names', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import(${JSON.stringify(fixtures.fileURL('es-modules/invalid-export-name-wasm-js.wasm'))})`,
|
||||
]);
|
||||
|
||||
match(stderr, /Invalid Wasm export/);
|
||||
strictEqual(stdout, '');
|
||||
notStrictEqual(code, 0);
|
||||
});
|
||||
|
||||
it('should support js-string builtins', async () => {
|
||||
const { code, stderr, stdout } = await spawnPromisified(execPath, [
|
||||
'--no-warnings',
|
||||
'--experimental-wasm-modules',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
[
|
||||
'import { strictEqual } from "node:assert";',
|
||||
`import * as wasmExports from ${JSON.stringify(fixtures.fileURL('es-modules/js-string-builtins.wasm'))};`,
|
||||
'strictEqual(wasmExports.getLength("hello"), 5);',
|
||||
'strictEqual(wasmExports.concatStrings("hello", " world"), "hello world");',
|
||||
'strictEqual(wasmExports.compareStrings("test", "test"), 1);',
|
||||
'strictEqual(wasmExports.compareStrings("test", "different"), 0);',
|
||||
].join('\n'),
|
||||
]);
|
||||
|
||||
strictEqual(stderr, '');
|
||||
strictEqual(stdout, '');
|
||||
strictEqual(code, 0);
|
||||
});
|
||||
});
|
||||
|
|
BIN
test/fixtures/es-modules/invalid-export-name-wasm-js.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/invalid-export-name-wasm-js.wasm
vendored
Normal file
Binary file not shown.
7
test/fixtures/es-modules/invalid-export-name-wasm-js.wat
vendored
Normal file
7
test/fixtures/es-modules/invalid-export-name-wasm-js.wat
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
;; Test WASM module with invalid export name starting with 'wasm-js:'
|
||||
(module
|
||||
(func $test (result i32)
|
||||
i32.const 42
|
||||
)
|
||||
(export "wasm-js:invalid" (func $test))
|
||||
)
|
BIN
test/fixtures/es-modules/invalid-export-name.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/invalid-export-name.wasm
vendored
Normal file
Binary file not shown.
7
test/fixtures/es-modules/invalid-export-name.wat
vendored
Normal file
7
test/fixtures/es-modules/invalid-export-name.wat
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
;; Test WASM module with invalid export name starting with 'wasm:'
|
||||
(module
|
||||
(func $test (result i32)
|
||||
i32.const 42
|
||||
)
|
||||
(export "wasm:invalid" (func $test))
|
||||
)
|
BIN
test/fixtures/es-modules/invalid-import-module.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/invalid-import-module.wasm
vendored
Normal file
Binary file not shown.
8
test/fixtures/es-modules/invalid-import-module.wat
vendored
Normal file
8
test/fixtures/es-modules/invalid-import-module.wat
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
;; Test WASM module with invalid import module name starting with 'wasm-js:'
|
||||
(module
|
||||
(import "wasm-js:invalid" "test" (func $invalidImport (result i32)))
|
||||
(export "test" (func $test))
|
||||
(func $test (result i32)
|
||||
call $invalidImport
|
||||
)
|
||||
)
|
BIN
test/fixtures/es-modules/invalid-import-name-wasm-js.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/invalid-import-name-wasm-js.wasm
vendored
Normal file
Binary file not shown.
8
test/fixtures/es-modules/invalid-import-name-wasm-js.wat
vendored
Normal file
8
test/fixtures/es-modules/invalid-import-name-wasm-js.wat
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
;; Test WASM module with invalid import name starting with 'wasm-js:'
|
||||
(module
|
||||
(import "test" "wasm-js:invalid" (func $invalidImport (result i32)))
|
||||
(export "test" (func $test))
|
||||
(func $test (result i32)
|
||||
call $invalidImport
|
||||
)
|
||||
)
|
BIN
test/fixtures/es-modules/invalid-import-name.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/invalid-import-name.wasm
vendored
Normal file
Binary file not shown.
8
test/fixtures/es-modules/invalid-import-name.wat
vendored
Normal file
8
test/fixtures/es-modules/invalid-import-name.wat
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
;; Test WASM module with invalid import name starting with 'wasm:'
|
||||
(module
|
||||
(import "test" "wasm:invalid" (func $invalidImport (result i32)))
|
||||
(export "test" (func $test))
|
||||
(func $test (result i32)
|
||||
call $invalidImport
|
||||
)
|
||||
)
|
BIN
test/fixtures/es-modules/js-string-builtins.wasm
vendored
Normal file
BIN
test/fixtures/es-modules/js-string-builtins.wasm
vendored
Normal file
Binary file not shown.
29
test/fixtures/es-modules/js-string-builtins.wat
vendored
Normal file
29
test/fixtures/es-modules/js-string-builtins.wat
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
;; Test WASM module using js-string builtins
|
||||
(module
|
||||
;; Import js-string builtins with correct signatures
|
||||
(import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))
|
||||
(import "wasm:js-string" "concat" (func $string_concat (param externref externref) (result (ref extern))))
|
||||
(import "wasm:js-string" "equals" (func $string_equals (param externref externref) (result i32)))
|
||||
|
||||
;; Export functions that use the builtins
|
||||
(export "getLength" (func $get_length))
|
||||
(export "concatStrings" (func $concat_strings))
|
||||
(export "compareStrings" (func $compare_strings))
|
||||
|
||||
(func $get_length (param $str externref) (result i32)
|
||||
local.get $str
|
||||
call $string_length
|
||||
)
|
||||
|
||||
(func $concat_strings (param $str1 externref) (param $str2 externref) (result (ref extern))
|
||||
local.get $str1
|
||||
local.get $str2
|
||||
call $string_concat
|
||||
)
|
||||
|
||||
(func $compare_strings (param $str1 externref) (param $str2 externref) (result i32)
|
||||
local.get $str1
|
||||
local.get $str2
|
||||
call $string_equals
|
||||
)
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue