esm: --experimental-wasm-modules integration support

PR-URL: https://github.com/nodejs/node/pull/27659
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
Myles Borins 2019-04-30 00:27:20 +08:00 committed by Guy Bedford
parent 6982dc7198
commit bbc254db5d
12 changed files with 147 additions and 34 deletions

View File

@ -441,6 +441,30 @@ node --experimental-modules index.mjs # fails
node --experimental-modules --experimental-json-modules index.mjs # works node --experimental-modules --experimental-json-modules index.mjs # works
``` ```
## Experimental Wasm Modules
Importing Web Assembly modules is supported under the
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
imported as normal modules while also supporting their module imports.
This integration is in line with the
[ES Module Integration Proposal for Web Assembly][].
For example, an `index.mjs` containing:
```js
import * as M from './module.wasm';
console.log(M);
```
executed under:
```bash
node --experimental-modules --experimental-wasm-modules index.mjs
```
would provide the exports interface for the instantiation of `module.wasm`.
## Experimental Loader hooks ## Experimental Loader hooks
**Note: This API is currently being redesigned and will still change.** **Note: This API is currently being redesigned and will still change.**
@ -484,11 +508,12 @@ module. This can be one of the following:
| `format` | Description | | `format` | Description |
| --- | --- | | --- | --- |
| `'module'` | Load a standard JavaScript module |
| `'commonjs'` | Load a Node.js CommonJS module |
| `'builtin'` | Load a Node.js builtin module | | `'builtin'` | Load a Node.js builtin module |
| `'json'` | Load a JSON file | | `'commonjs'` | Load a Node.js CommonJS module |
| `'dynamic'` | Use a [dynamic instantiate hook][] | | `'dynamic'` | Use a [dynamic instantiate hook][] |
| `'json'` | Load a JSON file |
| `'module'` | Load a standard JavaScript module |
| `'wasm'` | Load a WebAssembly module |
For example, a dummy loader to load JavaScript restricted to browser resolution For example, a dummy loader to load JavaScript restricted to browser resolution
rules with only JS file extension and Node.js builtin modules support could 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.
The _"module"_ format is returned for an ECMAScript Module, while the The _"module"_ format is returned for an ECMAScript Module, while the
_"commonjs"_ format is used to indicate loading through the legacy _"commonjs"_ format is used to indicate loading through the legacy
CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be CommonJS loader. Additional formats such as _"addon"_ can be extended in future
extended in future updates. updates.
In the following algorithms, all subroutine errors are propagated as errors In the following algorithms, all subroutine errors are propagated as errors
of these top-level routines. of these top-level routines.
@ -739,5 +764,6 @@ success!
[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md [ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
[WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315 [WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook [dynamic instantiate hook]: #esm_dynamic_instantiate_hook
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules [the official standard format]: https://tc39.github.io/ecma262/#sec-modules

View File

@ -658,7 +658,7 @@ Module.prototype.load = function(filename) {
url, url,
new ModuleJob(ESMLoader, url, async () => { new ModuleJob(ESMLoader, url, async () => {
return createDynamicModule( return createDynamicModule(
['default'], url, (reflect) => { [], ['default'], url, (reflect) => {
reflect.exports.default.set(exports); reflect.exports.default.set(exports);
}); });
}) })

View File

@ -1,14 +1,18 @@
'use strict'; 'use strict';
const { ArrayPrototype } = primordials; const { ArrayPrototype, JSON, Object } = primordials;
const debug = require('internal/util/debuglog').debuglog('esm'); const debug = require('internal/util/debuglog').debuglog('esm');
const createDynamicModule = (exports, url = '', evaluate) => { const createDynamicModule = (imports, exports, url = '', evaluate) => {
debug('creating ESM facade for %s with exports: %j', url, exports); debug('creating ESM facade for %s with exports: %j', url, exports);
const names = ArrayPrototype.map(exports, (name) => `${name}`); const names = ArrayPrototype.map(exports, (name) => `${name}`);
const source = ` const source = `
${ArrayPrototype.join(ArrayPrototype.map(imports, (impt, index) =>
`import * as $import_${index} from ${JSON.stringify(impt)};
import.meta.imports[${JSON.stringify(impt)}] = $import_${index};`), '\n')
}
${ArrayPrototype.join(ArrayPrototype.map(names, (name) => ${ArrayPrototype.join(ArrayPrototype.map(names, (name) =>
`let $${name}; `let $${name};
export { $${name} as ${name} }; export { $${name} as ${name} };
@ -22,19 +26,21 @@ import.meta.done();
`; `;
const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const m = new ModuleWrap(source, `${url}`); const m = new ModuleWrap(source, `${url}`);
m.link(() => 0);
m.instantiate();
const readyfns = new Set(); const readyfns = new Set();
const reflect = { const reflect = {
namespace: m.namespace(), exports: Object.create(null),
exports: {},
onReady: (cb) => { readyfns.add(cb); }, onReady: (cb) => { readyfns.add(cb); },
}; };
if (imports.length)
reflect.imports = Object.create(null);
callbackMap.set(m, { callbackMap.set(m, {
initializeImportMeta: (meta, wrap) => { initializeImportMeta: (meta, wrap) => {
meta.exports = reflect.exports; meta.exports = reflect.exports;
if (reflect.imports)
meta.imports = reflect.imports;
meta.done = () => { meta.done = () => {
evaluate(reflect); evaluate(reflect);
reflect.onReady = (cb) => cb(reflect); reflect.onReady = (cb) => cb(reflect);

View File

@ -10,17 +10,14 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const experimentalJsonModules = getOptionValue('--experimental-json-modules'); const experimentalJsonModules = getOptionValue('--experimental-json-modules');
const typeFlag = getOptionValue('--input-type'); const typeFlag = getOptionValue('--input-type');
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
const { resolve: moduleWrapResolve, const { resolve: moduleWrapResolve,
getPackageType } = internalBinding('module_wrap'); getPackageType } = internalBinding('module_wrap');
const { pathToFileURL, fileURLToPath } = require('internal/url'); const { pathToFileURL, fileURLToPath } = require('internal/url');
const { ERR_INPUT_TYPE_NOT_ALLOWED, const { ERR_INPUT_TYPE_NOT_ALLOWED,
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
const { const { SafeMap } = primordials;
Object,
SafeMap
} = primordials;
const realpathCache = new SafeMap(); const realpathCache = new SafeMap();
@ -44,15 +41,11 @@ const legacyExtensionFormatMap = {
'.node': 'commonjs' '.node': 'commonjs'
}; };
if (experimentalJsonModules) { if (experimentalWasmModules)
// This is a total hack extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
Object.assign(extensionFormatMap, {
'.json': 'json' if (experimentalJsonModules)
}); extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
Object.assign(legacyExtensionFormatMap, {
'.json': 'json'
});
}
function resolve(specifier, parentURL) { function resolve(specifier, parentURL) {
if (NativeModule.canBeRequiredByUsers(specifier)) { if (NativeModule.canBeRequiredByUsers(specifier)) {

View File

@ -153,7 +153,7 @@ class Loader {
loaderInstance = async (url) => { loaderInstance = async (url) => {
debug(`Translating dynamic ${url}`); debug(`Translating dynamic ${url}`);
const { exports, execute } = await this._dynamicInstantiate(url); const { exports, execute } = await this._dynamicInstantiate(url);
return createDynamicModule(exports, url, (reflect) => { return createDynamicModule([], exports, url, (reflect) => {
debug(`Loading dynamic ${url}`); debug(`Loading dynamic ${url}`);
execute(reflect.exports); execute(reflect.exports);
}); });

View File

@ -1,9 +1,12 @@
'use strict'; 'use strict';
/* global WebAssembly */
const { const {
JSON,
Object,
SafeMap, SafeMap,
StringPrototype, StringPrototype
JSON
} = primordials; } = primordials;
const { NativeModule } = require('internal/bootstrap/loaders'); const { NativeModule } = require('internal/bootstrap/loaders');
@ -72,11 +75,11 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) {
]; ];
if (module && module.loaded) { if (module && module.loaded) {
const exports = module.exports; const exports = module.exports;
return createDynamicModule(['default'], url, (reflect) => { return createDynamicModule([], ['default'], url, (reflect) => {
reflect.exports.default.set(exports); reflect.exports.default.set(exports);
}); });
} }
return createDynamicModule(['default'], url, () => { return createDynamicModule([], ['default'], url, () => {
debug(`Loading CJSModule ${url}`); debug(`Loading CJSModule ${url}`);
// We don't care about the return val of _load here because Module#load // We don't care about the return val of _load here because Module#load
// will handle it for us by checking the loader registry and filling the // will handle it for us by checking the loader registry and filling the
@ -97,7 +100,7 @@ translators.set('builtin', async function builtinStrategy(url) {
} }
module.compileForPublicLoader(true); module.compileForPublicLoader(true);
return createDynamicModule( return createDynamicModule(
[...module.exportKeys, 'default'], url, (reflect) => { [], [...module.exportKeys, 'default'], url, (reflect) => {
debug(`Loading BuiltinModule ${url}`); debug(`Loading BuiltinModule ${url}`);
module.reflect = reflect; module.reflect = reflect;
for (const key of module.exportKeys) for (const key of module.exportKeys)
@ -116,7 +119,7 @@ translators.set('json', async function jsonStrategy(url) {
let module = CJSModule._cache[modulePath]; let module = CJSModule._cache[modulePath];
if (module && module.loaded) { if (module && module.loaded) {
const exports = module.exports; const exports = module.exports;
return createDynamicModule(['default'], url, (reflect) => { return createDynamicModule([], ['default'], url, (reflect) => {
reflect.exports.default.set(exports); reflect.exports.default.set(exports);
}); });
} }
@ -136,8 +139,32 @@ translators.set('json', async function jsonStrategy(url) {
throw err; throw err;
} }
CJSModule._cache[modulePath] = module; CJSModule._cache[modulePath] = module;
return createDynamicModule(['default'], url, (reflect) => { return createDynamicModule([], ['default'], url, (reflect) => {
debug(`Parsing JSONModule ${url}`); debug(`Parsing JSONModule ${url}`);
reflect.exports.default.set(module.exports); reflect.exports.default.set(module.exports);
}); });
}); });
// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
const pathname = fileURLToPath(url);
const buffer = await readFileAsync(pathname);
debug(`Translating WASMModule ${url}`);
let compiled;
try {
compiled = await WebAssembly.compile(buffer);
} catch (err) {
err.message = pathname + ': ' + err.message;
throw err;
}
const imports =
WebAssembly.Module.imports(compiled).map(({ module }) => module);
const exports = WebAssembly.Module.exports(compiled).map(({ name }) => name);
return createDynamicModule(imports, exports, url, (reflect) => {
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
for (const expt of Object.keys(exports))
reflect.exports[expt].set(exports[expt]);
});
});

View File

@ -122,6 +122,11 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
"--experimental-modules be enabled"); "--experimental-modules be enabled");
} }
if (experimental_wasm_modules && !experimental_modules) {
errors->push_back("--experimental-wasm-modules requires "
"--experimental-modules be enabled");
}
if (!es_module_specifier_resolution.empty()) { if (!es_module_specifier_resolution.empty()) {
if (!experimental_modules) { if (!experimental_modules) {
errors->push_back("--es-module-specifier-resolution requires " errors->push_back("--es-module-specifier-resolution requires "
@ -274,6 +279,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental ES Module support and caching modules", "experimental ES Module support and caching modules",
&EnvironmentOptions::experimental_modules, &EnvironmentOptions::experimental_modules,
kAllowedInEnvironment); kAllowedInEnvironment);
AddOption("--experimental-wasm-modules",
"experimental ES Module support for webassembly modules",
&EnvironmentOptions::experimental_wasm_modules,
kAllowedInEnvironment);
AddOption("--experimental-policy", AddOption("--experimental-policy",
"use the specified file as a " "use the specified file as a "
"security policy", "security policy",

View File

@ -94,6 +94,7 @@ class EnvironmentOptions : public Options {
bool experimental_json_modules = false; bool experimental_json_modules = false;
bool experimental_modules = false; bool experimental_modules = false;
std::string es_module_specifier_resolution; std::string es_module_specifier_resolution;
bool experimental_wasm_modules = false;
std::string module_type; std::string module_type;
std::string experimental_policy; std::string experimental_policy;
bool experimental_repl_await = false; bool experimental_repl_await = false;

View File

@ -0,0 +1,15 @@
// Flags: --experimental-modules --experimental-wasm-modules
import '../common/index.mjs';
import { add, addImported } from '../fixtures/es-modules/simple.wasm';
import { state } from '../fixtures/es-modules/wasm-dep.mjs';
import { strictEqual } from 'assert';
strictEqual(state, 'WASM Start Executed');
strictEqual(add(10, 20), 30);
strictEqual(addImported(0), 42);
strictEqual(state, 'WASM JS Function Executed');
strictEqual(addImported(1), 43);

BIN
test/fixtures/es-modules/simple.wasm vendored Normal file

Binary file not shown.

23
test/fixtures/es-modules/simple.wat vendored Normal file
View File

@ -0,0 +1,23 @@
;; Compiled using the WebAssembly Tootkit (https://github.com/WebAssembly/wabt)
;; $ wat2wasm simple.wat -o simple.wasm
(module
(import "./wasm-dep.mjs" "jsFn" (func $jsFn (result i32)))
(import "./wasm-dep.mjs" "jsInitFn" (func $jsInitFn))
(export "add" (func $add))
(export "addImported" (func $addImported))
(start $startFn)
(func $startFn
call $jsInitFn
)
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(func $addImported (param $a i32) (result i32)
local.get $a
call $jsFn
i32.add
)
)

13
test/fixtures/es-modules/wasm-dep.mjs vendored Normal file
View File

@ -0,0 +1,13 @@
import { strictEqual } from 'assert';
export function jsFn () {
state = 'WASM JS Function Executed';
return 42;
}
export let state = 'JS Function Executed';
export function jsInitFn () {
strictEqual(state, 'JS Function Executed');
state = 'WASM Start Executed';
}