Skip to content

Commit

Permalink
esm: --experimental-wasm-modules integration support
Browse files Browse the repository at this point in the history
PR-URL: #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>
  • Loading branch information
MylesBorins authored and guybedford committed May 17, 2019
1 parent 6982dc7 commit bbc254d
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 34 deletions.
36 changes: 31 additions & 5 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
})
Expand Down
18 changes: 12 additions & 6 deletions lib/internal/modules/esm/create_dynamic_module.js
Original file line number Diff line number Diff line change
@@ -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} };
Expand All @@ -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);
Expand Down
21 changes: 7 additions & 14 deletions lib/internal/modules/esm/default_resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
41 changes: 34 additions & 7 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use strict';

/* global WebAssembly */

const {
JSON,
Object,
SafeMap,
StringPrototype,
JSON
StringPrototype
} = primordials;

const { NativeModule } = require('internal/bootstrap/loaders');
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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);
});
}
Expand All @@ -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]);
});
});
9 changes: 9 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* 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 "
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions test/es-module/test-esm-wasm.mjs
Original file line number Diff line number Diff line change
@@ -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);
Binary file added test/fixtures/es-modules/simple.wasm
Binary file not shown.
23 changes: 23 additions & 0 deletions test/fixtures/es-modules/simple.wat
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions test/fixtures/es-modules/wasm-dep.mjs
Original file line number Diff line number Diff line change
@@ -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';
}

0 comments on commit bbc254d

Please sign in to comment.