From bbc254db5db672643aad89a436a4938412a5704e Mon Sep 17 00:00:00 2001 From: Myles Borins Date: Tue, 30 Apr 2019 00:27:20 +0800 Subject: [PATCH] esm: --experimental-wasm-modules integration support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/27659 Reviewed-By: Gus Caplan Reviewed-By: Michaƫl Zasso Reviewed-By: Myles Borins Reviewed-By: Benjamin Gruenbaum --- doc/api/esm.md | 36 ++++++++++++--- lib/internal/modules/cjs/loader.js | 2 +- .../modules/esm/create_dynamic_module.js | 18 +++++--- lib/internal/modules/esm/default_resolve.js | 21 +++------ lib/internal/modules/esm/loader.js | 2 +- lib/internal/modules/esm/translators.js | 41 +++++++++++++++--- src/node_options.cc | 9 ++++ src/node_options.h | 1 + test/es-module/test-esm-wasm.mjs | 15 +++++++ test/fixtures/es-modules/simple.wasm | Bin 0 -> 136 bytes test/fixtures/es-modules/simple.wat | 23 ++++++++++ test/fixtures/es-modules/wasm-dep.mjs | 13 ++++++ 12 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 test/es-module/test-esm-wasm.mjs create mode 100644 test/fixtures/es-modules/simple.wasm create mode 100644 test/fixtures/es-modules/simple.wat create mode 100644 test/fixtures/es-modules/wasm-dep.mjs diff --git a/doc/api/esm.md b/doc/api/esm.md index 2d1d4085e7359e..60f9f6787dbcf4 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -441,6 +441,30 @@ node --experimental-modules index.mjs # fails 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 **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 | | --- | --- | -| `'module'` | Load a standard JavaScript module | -| `'commonjs'` | Load a Node.js CommonJS 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][] | +| `'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 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 _"commonjs"_ format is used to indicate loading through the legacy -CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be -extended in future updates. +CommonJS loader. Additional formats such as _"addon"_ can be extended in future +updates. In the following algorithms, all subroutine errors are propagated as errors 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 [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 +[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration [dynamic instantiate hook]: #esm_dynamic_instantiate_hook [the official standard format]: https://tc39.github.io/ecma262/#sec-modules diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 5d4ae26c02ebbd..2798032fd96239 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -658,7 +658,7 @@ Module.prototype.load = function(filename) { url, new ModuleJob(ESMLoader, url, async () => { return createDynamicModule( - ['default'], url, (reflect) => { + [], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); }) diff --git a/lib/internal/modules/esm/create_dynamic_module.js b/lib/internal/modules/esm/create_dynamic_module.js index eea01bed31ee01..45f964d5ad8020 100644 --- a/lib/internal/modules/esm/create_dynamic_module.js +++ b/lib/internal/modules/esm/create_dynamic_module.js @@ -1,14 +1,18 @@ 'use strict'; -const { ArrayPrototype } = primordials; +const { ArrayPrototype, JSON, Object } = primordials; 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); const names = ArrayPrototype.map(exports, (name) => `${name}`); 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) => `let $${name}; export { $${name} as ${name} }; @@ -22,19 +26,21 @@ import.meta.done(); `; const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const m = new ModuleWrap(source, `${url}`); - m.link(() => 0); - m.instantiate(); const readyfns = new Set(); const reflect = { - namespace: m.namespace(), - exports: {}, + exports: Object.create(null), onReady: (cb) => { readyfns.add(cb); }, }; + if (imports.length) + reflect.imports = Object.create(null); + callbackMap.set(m, { initializeImportMeta: (meta, wrap) => { meta.exports = reflect.exports; + if (reflect.imports) + meta.imports = reflect.imports; meta.done = () => { evaluate(reflect); reflect.onReady = (cb) => cb(reflect); diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index a83cf9c67561b9..67b8db716c5b01 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -10,17 +10,14 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const experimentalJsonModules = getOptionValue('--experimental-json-modules'); const typeFlag = getOptionValue('--input-type'); - +const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); const { resolve: moduleWrapResolve, getPackageType } = internalBinding('module_wrap'); const { pathToFileURL, fileURLToPath } = require('internal/url'); const { ERR_INPUT_TYPE_NOT_ALLOWED, ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; -const { - Object, - SafeMap -} = primordials; +const { SafeMap } = primordials; const realpathCache = new SafeMap(); @@ -44,15 +41,11 @@ const legacyExtensionFormatMap = { '.node': 'commonjs' }; -if (experimentalJsonModules) { - // This is a total hack - Object.assign(extensionFormatMap, { - '.json': 'json' - }); - Object.assign(legacyExtensionFormatMap, { - '.json': 'json' - }); -} +if (experimentalWasmModules) + extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm'; + +if (experimentalJsonModules) + extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; function resolve(specifier, parentURL) { if (NativeModule.canBeRequiredByUsers(specifier)) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index f752550d12c205..0ea1e6f4e56955 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -153,7 +153,7 @@ class Loader { loaderInstance = async (url) => { debug(`Translating dynamic ${url}`); const { exports, execute } = await this._dynamicInstantiate(url); - return createDynamicModule(exports, url, (reflect) => { + return createDynamicModule([], exports, url, (reflect) => { debug(`Loading dynamic ${url}`); execute(reflect.exports); }); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 72350fb2b2c6c6..4ca9be4d622faa 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -1,9 +1,12 @@ 'use strict'; +/* global WebAssembly */ + const { + JSON, + Object, SafeMap, - StringPrototype, - JSON + StringPrototype } = primordials; const { NativeModule } = require('internal/bootstrap/loaders'); @@ -72,11 +75,11 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) { ]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); } - return createDynamicModule(['default'], url, () => { + return createDynamicModule([], ['default'], url, () => { debug(`Loading CJSModule ${url}`); // 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 @@ -97,7 +100,7 @@ translators.set('builtin', async function builtinStrategy(url) { } module.compileForPublicLoader(true); return createDynamicModule( - [...module.exportKeys, 'default'], url, (reflect) => { + [], [...module.exportKeys, 'default'], url, (reflect) => { debug(`Loading BuiltinModule ${url}`); module.reflect = reflect; for (const key of module.exportKeys) @@ -116,7 +119,7 @@ translators.set('json', async function jsonStrategy(url) { let module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { reflect.exports.default.set(exports); }); } @@ -136,8 +139,32 @@ translators.set('json', async function jsonStrategy(url) { throw err; } CJSModule._cache[modulePath] = module; - return createDynamicModule(['default'], url, (reflect) => { + return createDynamicModule([], ['default'], url, (reflect) => { debug(`Parsing JSONModule ${url}`); 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]); + }); +}); diff --git a/src/node_options.cc b/src/node_options.cc index a36666c3e0f452..12e0cb09d085fe 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -122,6 +122,11 @@ void EnvironmentOptions::CheckOptions(std::vector* errors) { "--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 (!experimental_modules) { errors->push_back("--es-module-specifier-resolution requires " @@ -274,6 +279,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental ES Module support and caching modules", &EnvironmentOptions::experimental_modules, kAllowedInEnvironment); + AddOption("--experimental-wasm-modules", + "experimental ES Module support for webassembly modules", + &EnvironmentOptions::experimental_wasm_modules, + kAllowedInEnvironment); AddOption("--experimental-policy", "use the specified file as a " "security policy", diff --git a/src/node_options.h b/src/node_options.h index db564ddb3d3e6d..b0a1844df58d2e 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -94,6 +94,7 @@ class EnvironmentOptions : public Options { bool experimental_json_modules = false; bool experimental_modules = false; std::string es_module_specifier_resolution; + bool experimental_wasm_modules = false; std::string module_type; std::string experimental_policy; bool experimental_repl_await = false; diff --git a/test/es-module/test-esm-wasm.mjs b/test/es-module/test-esm-wasm.mjs new file mode 100644 index 00000000000000..bcfce797a9cc8b --- /dev/null +++ b/test/es-module/test-esm-wasm.mjs @@ -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); diff --git a/test/fixtures/es-modules/simple.wasm b/test/fixtures/es-modules/simple.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9e035904b2e4d0a30ce5a63bcf05cc3a2c8449db GIT binary patch literal 136 zcmZ9COA5k35Jam#kl=s>MAx~130^|TEhaEo*f23T0he=in=IYDDqa=lk_iA^G=gdb zBG>AL9Q@$(Fn;}VPs=uBD{AGr0)Mu(GOe%O7ZMd>X|61DN|4~3^7j7hOM