Skip to content

Commit 47f913b

Browse files
MylesBorinstargos
authored andcommitted
esm: --experimental-wasm-modules integration support
PR-URL: #27659 Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Myles Borins <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent fc8ad77 commit 47f913b

File tree

12 files changed

+147
-34
lines changed

12 files changed

+147
-34
lines changed

doc/api/esm.md

+31-5
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,30 @@ node --experimental-modules index.mjs # fails
441441
node --experimental-modules --experimental-json-modules index.mjs # works
442442
```
443443
444+
## Experimental Wasm Modules
445+
446+
Importing Web Assembly modules is supported under the
447+
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
448+
imported as normal modules while also supporting their module imports.
449+
450+
This integration is in line with the
451+
[ES Module Integration Proposal for Web Assembly][].
452+
453+
For example, an `index.mjs` containing:
454+
455+
```js
456+
import * as M from './module.wasm';
457+
console.log(M);
458+
```
459+
460+
executed under:
461+
462+
```bash
463+
node --experimental-modules --experimental-wasm-modules index.mjs
464+
```
465+
466+
would provide the exports interface for the instantiation of `module.wasm`.
467+
444468
## Experimental Loader hooks
445469
446470
**Note: This API is currently being redesigned and will still change.**
@@ -484,11 +508,12 @@ module. This can be one of the following:
484508
485509
| `format` | Description |
486510
| --- | --- |
487-
| `'module'` | Load a standard JavaScript module |
488-
| `'commonjs'` | Load a Node.js CommonJS module |
489511
| `'builtin'` | Load a Node.js builtin module |
490-
| `'json'` | Load a JSON file |
512+
| `'commonjs'` | Load a Node.js CommonJS module |
491513
| `'dynamic'` | Use a [dynamic instantiate hook][] |
514+
| `'json'` | Load a JSON file |
515+
| `'module'` | Load a standard JavaScript module |
516+
| `'wasm'` | Load a WebAssembly module |
492517
493518
For example, a dummy loader to load JavaScript restricted to browser resolution
494519
rules with only JS file extension and Node.js builtin modules support could
@@ -585,8 +610,8 @@ format for that resolved URL given by the **ESM_FORMAT** routine.
585610
586611
The _"module"_ format is returned for an ECMAScript Module, while the
587612
_"commonjs"_ format is used to indicate loading through the legacy
588-
CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be
589-
extended in future updates.
613+
CommonJS loader. Additional formats such as _"addon"_ can be extended in future
614+
updates.
590615
591616
In the following algorithms, all subroutine errors are propagated as errors
592617
of these top-level routines.
@@ -739,5 +764,6 @@ success!
739764
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
740765
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
741766
[WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315
767+
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
742768
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
743769
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules

lib/internal/modules/cjs/loader.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ Module.prototype.load = function(filename) {
658658
url,
659659
new ModuleJob(ESMLoader, url, async () => {
660660
return createDynamicModule(
661-
['default'], url, (reflect) => {
661+
[], ['default'], url, (reflect) => {
662662
reflect.exports.default.set(exports);
663663
});
664664
})

lib/internal/modules/esm/create_dynamic_module.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
'use strict';
22

3-
const { ArrayPrototype } = primordials;
3+
const { ArrayPrototype, JSON, Object } = primordials;
44

55
const debug = require('internal/util/debuglog').debuglog('esm');
66

7-
const createDynamicModule = (exports, url = '', evaluate) => {
7+
const createDynamicModule = (imports, exports, url = '', evaluate) => {
88
debug('creating ESM facade for %s with exports: %j', url, exports);
99
const names = ArrayPrototype.map(exports, (name) => `${name}`);
1010

1111
const source = `
12+
${ArrayPrototype.join(ArrayPrototype.map(imports, (impt, index) =>
13+
`import * as $import_${index} from ${JSON.stringify(impt)};
14+
import.meta.imports[${JSON.stringify(impt)}] = $import_${index};`), '\n')
15+
}
1216
${ArrayPrototype.join(ArrayPrototype.map(names, (name) =>
1317
`let $${name};
1418
export { $${name} as ${name} };
@@ -22,19 +26,21 @@ import.meta.done();
2226
`;
2327
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
2428
const m = new ModuleWrap(source, `${url}`);
25-
m.link(() => 0);
26-
m.instantiate();
2729

2830
const readyfns = new Set();
2931
const reflect = {
30-
namespace: m.namespace(),
31-
exports: {},
32+
exports: Object.create(null),
3233
onReady: (cb) => { readyfns.add(cb); },
3334
};
3435

36+
if (imports.length)
37+
reflect.imports = Object.create(null);
38+
3539
callbackMap.set(m, {
3640
initializeImportMeta: (meta, wrap) => {
3741
meta.exports = reflect.exports;
42+
if (reflect.imports)
43+
meta.imports = reflect.imports;
3844
meta.done = () => {
3945
evaluate(reflect);
4046
reflect.onReady = (cb) => cb(reflect);

lib/internal/modules/esm/default_resolve.js

+7-14
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
1010
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
1111
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
1212
const typeFlag = getOptionValue('--input-type');
13-
13+
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
1414
const { resolve: moduleWrapResolve,
1515
getPackageType } = internalBinding('module_wrap');
1616
const { pathToFileURL, fileURLToPath } = require('internal/url');
1717
const { ERR_INPUT_TYPE_NOT_ALLOWED,
1818
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
1919

20-
const {
21-
Object,
22-
SafeMap
23-
} = primordials;
20+
const { SafeMap } = primordials;
2421

2522
const realpathCache = new SafeMap();
2623

@@ -44,15 +41,11 @@ const legacyExtensionFormatMap = {
4441
'.node': 'commonjs'
4542
};
4643

47-
if (experimentalJsonModules) {
48-
// This is a total hack
49-
Object.assign(extensionFormatMap, {
50-
'.json': 'json'
51-
});
52-
Object.assign(legacyExtensionFormatMap, {
53-
'.json': 'json'
54-
});
55-
}
44+
if (experimentalWasmModules)
45+
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
46+
47+
if (experimentalJsonModules)
48+
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
5649

5750
function resolve(specifier, parentURL) {
5851
if (NativeModule.canBeRequiredByUsers(specifier)) {

lib/internal/modules/esm/loader.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class Loader {
153153
loaderInstance = async (url) => {
154154
debug(`Translating dynamic ${url}`);
155155
const { exports, execute } = await this._dynamicInstantiate(url);
156-
return createDynamicModule(exports, url, (reflect) => {
156+
return createDynamicModule([], exports, url, (reflect) => {
157157
debug(`Loading dynamic ${url}`);
158158
execute(reflect.exports);
159159
});

lib/internal/modules/esm/translators.js

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict';
22

3+
/* global WebAssembly */
4+
35
const {
6+
JSON,
7+
Object,
48
SafeMap,
5-
StringPrototype,
6-
JSON
9+
StringPrototype
710
} = primordials;
811

912
const { NativeModule } = require('internal/bootstrap/loaders');
@@ -72,11 +75,11 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) {
7275
];
7376
if (module && module.loaded) {
7477
const exports = module.exports;
75-
return createDynamicModule(['default'], url, (reflect) => {
78+
return createDynamicModule([], ['default'], url, (reflect) => {
7679
reflect.exports.default.set(exports);
7780
});
7881
}
79-
return createDynamicModule(['default'], url, () => {
82+
return createDynamicModule([], ['default'], url, () => {
8083
debug(`Loading CJSModule ${url}`);
8184
// We don't care about the return val of _load here because Module#load
8285
// will handle it for us by checking the loader registry and filling the
@@ -97,7 +100,7 @@ translators.set('builtin', async function builtinStrategy(url) {
97100
}
98101
module.compileForPublicLoader(true);
99102
return createDynamicModule(
100-
[...module.exportKeys, 'default'], url, (reflect) => {
103+
[], [...module.exportKeys, 'default'], url, (reflect) => {
101104
debug(`Loading BuiltinModule ${url}`);
102105
module.reflect = reflect;
103106
for (const key of module.exportKeys)
@@ -116,7 +119,7 @@ translators.set('json', async function jsonStrategy(url) {
116119
let module = CJSModule._cache[modulePath];
117120
if (module && module.loaded) {
118121
const exports = module.exports;
119-
return createDynamicModule(['default'], url, (reflect) => {
122+
return createDynamicModule([], ['default'], url, (reflect) => {
120123
reflect.exports.default.set(exports);
121124
});
122125
}
@@ -136,8 +139,32 @@ translators.set('json', async function jsonStrategy(url) {
136139
throw err;
137140
}
138141
CJSModule._cache[modulePath] = module;
139-
return createDynamicModule(['default'], url, (reflect) => {
142+
return createDynamicModule([], ['default'], url, (reflect) => {
140143
debug(`Parsing JSONModule ${url}`);
141144
reflect.exports.default.set(module.exports);
142145
});
143146
});
147+
148+
// Strategy for loading a wasm module
149+
translators.set('wasm', async function(url) {
150+
const pathname = fileURLToPath(url);
151+
const buffer = await readFileAsync(pathname);
152+
debug(`Translating WASMModule ${url}`);
153+
let compiled;
154+
try {
155+
compiled = await WebAssembly.compile(buffer);
156+
} catch (err) {
157+
err.message = pathname + ': ' + err.message;
158+
throw err;
159+
}
160+
161+
const imports =
162+
WebAssembly.Module.imports(compiled).map(({ module }) => module);
163+
const exports = WebAssembly.Module.exports(compiled).map(({ name }) => name);
164+
165+
return createDynamicModule(imports, exports, url, (reflect) => {
166+
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
167+
for (const expt of Object.keys(exports))
168+
reflect.exports[expt].set(exports[expt]);
169+
});
170+
});

src/node_options.cc

+9
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
122122
"--experimental-modules be enabled");
123123
}
124124

125+
if (experimental_wasm_modules && !experimental_modules) {
126+
errors->push_back("--experimental-wasm-modules requires "
127+
"--experimental-modules be enabled");
128+
}
129+
125130
if (!es_module_specifier_resolution.empty()) {
126131
if (!experimental_modules) {
127132
errors->push_back("--es-module-specifier-resolution requires "
@@ -274,6 +279,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
274279
"experimental ES Module support and caching modules",
275280
&EnvironmentOptions::experimental_modules,
276281
kAllowedInEnvironment);
282+
AddOption("--experimental-wasm-modules",
283+
"experimental ES Module support for webassembly modules",
284+
&EnvironmentOptions::experimental_wasm_modules,
285+
kAllowedInEnvironment);
277286
AddOption("--experimental-policy",
278287
"use the specified file as a "
279288
"security policy",

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class EnvironmentOptions : public Options {
9494
bool experimental_json_modules = false;
9595
bool experimental_modules = false;
9696
std::string es_module_specifier_resolution;
97+
bool experimental_wasm_modules = false;
9798
std::string module_type;
9899
std::string experimental_policy;
99100
bool experimental_repl_await = false;

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

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Flags: --experimental-modules --experimental-wasm-modules
2+
import '../common/index.mjs';
3+
import { add, addImported } from '../fixtures/es-modules/simple.wasm';
4+
import { state } from '../fixtures/es-modules/wasm-dep.mjs';
5+
import { strictEqual } from 'assert';
6+
7+
strictEqual(state, 'WASM Start Executed');
8+
9+
strictEqual(add(10, 20), 30);
10+
11+
strictEqual(addImported(0), 42);
12+
13+
strictEqual(state, 'WASM JS Function Executed');
14+
15+
strictEqual(addImported(1), 43);

test/fixtures/es-modules/simple.wasm

136 Bytes
Binary file not shown.

test/fixtures/es-modules/simple.wat

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
;; Compiled using the WebAssembly Tootkit (https://github.com/WebAssembly/wabt)
2+
;; $ wat2wasm simple.wat -o simple.wasm
3+
4+
(module
5+
(import "./wasm-dep.mjs" "jsFn" (func $jsFn (result i32)))
6+
(import "./wasm-dep.mjs" "jsInitFn" (func $jsInitFn))
7+
(export "add" (func $add))
8+
(export "addImported" (func $addImported))
9+
(start $startFn)
10+
(func $startFn
11+
call $jsInitFn
12+
)
13+
(func $add (param $a i32) (param $b i32) (result i32)
14+
local.get $a
15+
local.get $b
16+
i32.add
17+
)
18+
(func $addImported (param $a i32) (result i32)
19+
local.get $a
20+
call $jsFn
21+
i32.add
22+
)
23+
)

test/fixtures/es-modules/wasm-dep.mjs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { strictEqual } from 'assert';
2+
3+
export function jsFn () {
4+
state = 'WASM JS Function Executed';
5+
return 42;
6+
}
7+
8+
export let state = 'JS Function Executed';
9+
10+
export function jsInitFn () {
11+
strictEqual(state, 'JS Function Executed');
12+
state = 'WASM Start Executed';
13+
}

0 commit comments

Comments
 (0)