From 90562ae35685c7c0eafb743bfa14fd8ec9305d1b Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 4 Oct 2019 13:37:27 -0400 Subject: [PATCH] module: use v8 synthetic modules PR-URL: https://github.com/nodejs/node/pull/29846 Reviewed-By: Gus Caplan Reviewed-By: Minwoo Jung --- lib/internal/bootstrap/loaders.js | 30 ++--- lib/internal/modules/cjs/loader.js | 28 ++--- lib/internal/modules/esm/loader.js | 9 +- lib/internal/modules/esm/module_job.js | 6 +- lib/internal/modules/esm/translators.js | 42 ++++--- src/module_wrap.cc | 147 +++++++++++++++++++----- src/module_wrap.h | 7 ++ 7 files changed, 173 insertions(+), 96 deletions(-) diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index dbc21a9697e800..0cad5209c4ff4e 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -150,8 +150,7 @@ function NativeModule(id) { this.filename = `${id}.js`; this.id = id; this.exports = {}; - this.reflect = undefined; - this.esmFacade = undefined; + this.module = undefined; this.exportKeys = undefined; this.loaded = false; this.loading = false; @@ -240,16 +239,18 @@ NativeModule.prototype.getURL = function() { }; NativeModule.prototype.getESMFacade = function() { - if (this.esmFacade) return this.esmFacade; - const createDynamicModule = nativeModuleRequire( - 'internal/modules/esm/create_dynamic_module'); + if (this.module) return this.module; + const { ModuleWrap } = internalBinding('module_wrap'); const url = this.getURL(); - return this.esmFacade = createDynamicModule( - [], [...this.exportKeys, 'default'], url, (reflect) => { - this.reflect = reflect; - this.syncExports(); - reflect.exports.default.set(this.exports); - }); + const nativeModule = this; + this.module = new ModuleWrap(function() { + nativeModule.syncExports(); + this.setExport('default', nativeModule.exports); + }, [...this.exportKeys, 'default'], url); + // Ensure immediate sync execution to capture exports now + this.module.instantiate(); + this.module.evaluate(-1, false); + return this.module; }; // Provide named exports for all builtin libraries so that the libraries @@ -258,13 +259,12 @@ NativeModule.prototype.getESMFacade = function() { // called so that APMs and other behavior are supported. NativeModule.prototype.syncExports = function() { const names = this.exportKeys; - if (this.reflect) { + if (this.module) { for (let i = 0; i < names.length; i++) { const exportName = names[i]; if (exportName === 'default') continue; - this.reflect.exports[exportName].set( - getOwn(this.exports, exportName, this.exports) - ); + this.module.setExport(exportName, + getOwn(this.exports, exportName, this.exports)); } } }; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 77f5eeb319c4b9..2d058b7e0dace3 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -75,9 +75,7 @@ const experimentalExports = getOptionValue('--experimental-exports'); module.exports = Module; -let asyncESM; -let ModuleJob; -let createDynamicModule; +let asyncESM, ModuleJob, ModuleWrap, kInstantiated; const { CHAR_FORWARD_SLASH, @@ -820,21 +818,18 @@ Module.prototype.load = function(filename) { const module = ESMLoader.moduleMap.get(url); // Create module entry at load time to snapshot exports correctly const exports = this.exports; - if (module !== undefined) { // Called from cjs translator - if (module.reflect) { - module.reflect.onReady((reflect) => { - reflect.exports.default.set(exports); - }); - } + // Called from cjs translator + if (module !== undefined && module.module !== undefined) { + if (module.module.getStatus() >= kInstantiated) + module.module.setExport('default', exports); } else { // preemptively cache ESMLoader.moduleMap.set( url, - new ModuleJob(ESMLoader, url, async () => { - return createDynamicModule( - [], ['default'], url, (reflect) => { - reflect.exports.default.set(exports); - }); - }) + new ModuleJob(ESMLoader, url, () => + new ModuleWrap(function() { + this.setExport('default', exports); + }, ['default'], url) + ) ); } } @@ -1145,6 +1140,5 @@ Module.Module = Module; if (experimentalModules) { asyncESM = require('internal/process/esm_loader'); ModuleJob = require('internal/modules/esm/module_job'); - createDynamicModule = require( - 'internal/modules/esm/create_dynamic_module'); + ({ ModuleWrap, kInstantiated } = internalBinding('module_wrap')); } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 9800e8a550c2e0..138cf8b5ecc3ed 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -117,12 +117,7 @@ class Loader { source, url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href ) { - const evalInstance = async (url) => { - return { - module: new ModuleWrap(source, url), - reflect: undefined - }; - }; + const evalInstance = (url) => new ModuleWrap(source, url); const job = new ModuleJob(this, url, evalInstance, false); this.moduleMap.set(url, job); const { module, result } = await job.run(); @@ -165,7 +160,7 @@ class Loader { return createDynamicModule([], exports, url, (reflect) => { debug(`Loading dynamic ${url}`); execute(reflect.exports); - }); + }).module; }; } else { if (!translators.has(format)) diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 6f265ed4608743..ef11e2ec833b89 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -30,19 +30,17 @@ class ModuleJob { // onto `this` by `link()` below once it has been resolved. this.modulePromise = moduleProvider.call(loader, url, isMain); this.module = undefined; - this.reflect = undefined; // Wait for the ModuleWrap instance being linked with all dependencies. const link = async () => { - ({ module: this.module, - reflect: this.reflect } = await this.modulePromise); + this.module = await this.modulePromise; assert(this.module instanceof ModuleWrap); const dependencyJobs = []; const promises = this.module.link(async (specifier) => { const jobPromise = this.loader.getModuleJob(specifier, url); dependencyJobs.push(jobPromise); - return (await (await jobPromise).modulePromise).module; + return (await jobPromise).modulePromise; }); if (promises !== undefined) diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 7e6c017213b0c4..6c710202b85b77 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -33,6 +33,8 @@ const { const readFileAsync = promisify(fs.readFile); const JsonParse = JSON.parse; const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); +const moduleWrap = internalBinding('module_wrap'); +const { ModuleWrap } = moduleWrap; const debug = debuglog('esm'); @@ -78,22 +80,18 @@ translators.set('module', async function moduleStrategy(url) { const source = `${await getSource(url)}`; maybeCacheSourceMap(url, source); debug(`Translating StandardModule ${url}`); - const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const module = new ModuleWrap(stripShebang(source), url); - callbackMap.set(module, { + moduleWrap.callbackMap.set(module, { initializeImportMeta, importModuleDynamically, }); - return { - module, - reflect: undefined, - }; + return module; }); // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; -translators.set('commonjs', async function commonjsStrategy(url, isMain) { +translators.set('commonjs', function commonjsStrategy(url, isMain) { debug(`Translating CJSModule ${url}`); const pathname = internalURLModule.fileURLToPath(new URL(url)); const cached = this.cjsCache.get(url); @@ -106,17 +104,17 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) { ]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule([], ['default'], url, (reflect) => { - reflect.exports.default.set(exports); - }); + return new ModuleWrap(function() { + this.setExport('default', exports); + }, ['default'], url); } - return createDynamicModule([], ['default'], url, () => { + return new ModuleWrap(function() { 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 // exports like above CJSModule._load(pathname, undefined, isMain); - }); + }, ['default'], url); }); // Strategy for loading a node builtin CommonJS module that isn't @@ -146,9 +144,9 @@ translators.set('json', async function jsonStrategy(url) { module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule([], ['default'], url, (reflect) => { - reflect.exports.default.set(exports); - }); + return new ModuleWrap(function() { + this.setExport('default', exports); + }, ['default'], url); } } const content = `${await getSource(url)}`; @@ -159,9 +157,9 @@ translators.set('json', async function jsonStrategy(url) { module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports; - return createDynamicModule(['default'], url, (reflect) => { - reflect.exports.default.set(exports); - }); + return new ModuleWrap(function() { + this.setExport('default', exports); + }, ['default'], url); } } try { @@ -181,10 +179,10 @@ translators.set('json', async function jsonStrategy(url) { if (pathname) { CJSModule._cache[modulePath] = module; } - return createDynamicModule([], ['default'], url, (reflect) => { + return new ModuleWrap(function() { debug(`Parsing JSONModule ${url}`); - reflect.exports.default.set(module.exports); - }); + this.setExport('default', module.exports); + }, ['default'], url); }); // Strategy for loading a wasm module @@ -207,5 +205,5 @@ translators.set('wasm', async function(url) { const { exports } = new WebAssembly.Instance(compiled, reflect.imports); for (const expt of Object.keys(exports)) reflect.exports[expt].set(exports[expt]); - }); + }).module; }); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 2d0829860cefbf..67e0cbc6185f34 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -98,6 +98,9 @@ ModuleWrap* ModuleWrap::GetFromID(Environment* env, uint32_t id) { return module_wrap_it->second; } +// new ModuleWrap(source, url) +// new ModuleWrap(source, url, context?, lineOffset, columnOffset) +// new ModuleWrap(syntheticExecutionFunction, export_names, url) void ModuleWrap::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -108,12 +111,6 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { const int argc = args.Length(); CHECK_GE(argc, 2); - CHECK(args[0]->IsString()); - Local source_text = args[0].As(); - - CHECK(args[1]->IsString()); - Local url = args[1].As(); - Local context; Local line_offset; Local column_offset; @@ -143,8 +140,7 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { column_offset = Integer::New(isolate, 0); } - ShouldNotAbortOnUncaughtScope no_abort_scope(env); - TryCatchScope try_catch(env); + Local url; Local module; Local host_defined_options = @@ -152,29 +148,60 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { host_defined_options->Set(isolate, HostDefinedOptions::kType, Number::New(isolate, ScriptType::kModule)); - // compile - { - ScriptOrigin origin(url, - line_offset, // line offset - column_offset, // column offset - True(isolate), // is cross origin - Local(), // script id - Local(), // source map URL - False(isolate), // is opaque (?) - False(isolate), // is WASM - True(isolate), // is ES Module - host_defined_options); - Context::Scope context_scope(context); - ScriptCompiler::Source source(source_text, origin); - if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { - if (try_catch.HasCaught() && !try_catch.HasTerminated()) { - CHECK(!try_catch.Message().IsEmpty()); - CHECK(!try_catch.Exception().IsEmpty()); - AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(), - ErrorHandlingMode::MODULE_ERROR); - try_catch.ReThrow(); + // new ModuleWrap(syntheticExecutionFunction, export_names, url) + bool synthetic = args[0]->IsFunction(); + if (synthetic) { + CHECK(args[1]->IsArray()); + Local export_names_arr = args[1].As(); + + uint32_t len = export_names_arr->Length(); + std::vector> export_names(len); + for (uint32_t i = 0; i < len; i++) { + Local export_name_val = + export_names_arr->Get(context, i).ToLocalChecked(); + CHECK(export_name_val->IsString()); + export_names[i] = export_name_val.As(); + } + + CHECK(args[2]->IsString()); + url = args[2].As(); + + module = Module::CreateSyntheticModule(isolate, url, export_names, + SyntheticModuleEvaluationStepsCallback); + // Compile + } else { + CHECK(args[0]->IsString()); + Local source_text = args[0].As(); + + CHECK(args[1]->IsString()); + url = args[1].As(); + + ShouldNotAbortOnUncaughtScope no_abort_scope(env); + TryCatchScope try_catch(env); + + { + ScriptOrigin origin(url, + line_offset, // line offset + column_offset, // column offset + True(isolate), // is cross origin + Local(), // script id + Local(), // source map URL + False(isolate), // is opaque (?) + False(isolate), // is WASM + True(isolate), // is ES Module + host_defined_options); + Context::Scope context_scope(context); + ScriptCompiler::Source source(source_text, origin); + if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + CHECK(!try_catch.Message().IsEmpty()); + CHECK(!try_catch.Exception().IsEmpty()); + AppendExceptionLine(env, try_catch.Exception(), try_catch.Message(), + ErrorHandlingMode::MODULE_ERROR); + try_catch.ReThrow(); + } + return; } - return; } } @@ -183,6 +210,13 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { } ModuleWrap* obj = new ModuleWrap(env, that, module, url); + + if (synthetic) { + obj->synthetic_ = true; + obj->synthetic_evaluation_steps_.Reset( + env->isolate(), args[0].As()); + } + obj->context_.Reset(isolate, context); env->hash_to_module_map.emplace(module->GetIdentityHash(), obj); @@ -307,6 +341,10 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { result = module->Evaluate(context); } + if (result.IsEmpty()) { + CHECK(try_catch.HasCaught()); + } + // Convert the termination exception into a regular exception. if (timed_out || received_signal) { if (!env->is_main_thread() && env->is_stopping()) @@ -1295,7 +1333,7 @@ static MaybeLocal ImportModuleDynamically( Local result; if (import_callback->Call( context, - v8::Undefined(iso), + Undefined(iso), arraysize(import_args), import_args).ToLocal(&result)) { CHECK(result->IsPromise()); @@ -1355,6 +1393,52 @@ void ModuleWrap::SetInitializeImportMetaObjectCallback( HostInitializeImportMetaObjectCallback); } +MaybeLocal ModuleWrap::SyntheticModuleEvaluationStepsCallback( + Local context, Local module) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + + ModuleWrap* obj = GetFromModule(env, module); + + TryCatchScope try_catch(env); + Local synthetic_evaluation_steps = + obj->synthetic_evaluation_steps_.Get(isolate); + MaybeLocal ret = synthetic_evaluation_steps->Call(context, + obj->object(), 0, nullptr); + if (ret.IsEmpty()) { + CHECK(try_catch.HasCaught()); + } + obj->synthetic_evaluation_steps_.Reset(); + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + CHECK(!try_catch.Message().IsEmpty()); + CHECK(!try_catch.Exception().IsEmpty()); + try_catch.ReThrow(); + return MaybeLocal(); + } + return Undefined(isolate); +} + +void ModuleWrap::SetSyntheticExport( + const v8::FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local that = args.This(); + + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, that); + + CHECK(obj->synthetic_); + + CHECK_EQ(args.Length(), 2); + + CHECK(args[0]->IsString()); + Local export_name = args[0].As(); + + Local export_value = args[1]; + + Local module = obj->module_.Get(isolate); + module->SetSyntheticModuleExport(export_name, export_value); +} + void ModuleWrap::Initialize(Local target, Local unused, Local context, @@ -1369,6 +1453,7 @@ void ModuleWrap::Initialize(Local target, env->SetProtoMethod(tpl, "link", Link); env->SetProtoMethod(tpl, "instantiate", Instantiate); env->SetProtoMethod(tpl, "evaluate", Evaluate); + env->SetProtoMethod(tpl, "setExport", SetSyntheticExport); env->SetProtoMethodNoSideEffect(tpl, "getNamespace", GetNamespace); env->SetProtoMethodNoSideEffect(tpl, "getStatus", GetStatus); env->SetProtoMethodNoSideEffect(tpl, "getError", GetError); diff --git a/src/module_wrap.h b/src/module_wrap.h index 9e2aa3b9489dd3..ef20d255e916da 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -69,12 +69,19 @@ class ModuleWrap : public BaseObject { const v8::FunctionCallbackInfo& args); static void SetInitializeImportMetaObjectCallback( const v8::FunctionCallbackInfo& args); + static v8::MaybeLocal SyntheticModuleEvaluationStepsCallback( + v8::Local context, v8::Local module); + static void SetSyntheticExport( + const v8::FunctionCallbackInfo& args); + static v8::MaybeLocal ResolveCallback( v8::Local context, v8::Local specifier, v8::Local referrer); static ModuleWrap* GetFromModule(node::Environment*, v8::Local); + v8::Global synthetic_evaluation_steps_; + bool synthetic_ = false; v8::Global module_; v8::Global url_; bool linked_ = false;