Commit cf127e81 authored by adamk's avatar adamk Committed by Commit bot

[modules] Expand API to allow linking and use it in d8

This patch gives the ability for the embedder to ask for the
module requests of a module, and to pass a ResolveCallback
into Module::Instantiate().

In d8, I've implemented a simple module_map that's used
along with this API to allow loading, compiling, instantiating,
and evaluating a whole tree of modules.

No path resolution is yet implemented, meaning that all
import paths are relative to whatever directory d8 runs
in. And no imports are linked to the exports of the
requested module.

BUG=v8:1569

Review-Url: https://codereview.chromium.org/2351113004
Cr-Commit-Position: refs/heads/master@{#39569}
parent a037f350
......@@ -1078,12 +1078,30 @@ class V8_EXPORT UnboundScript {
*/
class V8_EXPORT Module {
public:
/**
* Returns the number of modules requested by this module.
*/
int GetModuleRequestsLength() const;
/**
* Returns the ith module specifier in this module.
* i must be < GetModuleRequestsLength() and >= 0.
*/
Local<String> GetModuleRequest(int i) const;
typedef MaybeLocal<Module> (*ResolveCallback)(Local<Context> context,
Local<String> specifier,
Local<Module> referrer,
Local<Value> data);
/**
* ModuleDeclarationInstantiation
*
* Returns false if an exception occurred during instantiation.
*/
V8_WARN_UNUSED_RESULT bool Instantiate(Local<Context> context);
V8_WARN_UNUSED_RESULT bool Instantiate(
Local<Context> context, ResolveCallback callback,
Local<Value> callback_data = Local<Value>());
/**
* ModuleEvaluation
......
......@@ -1884,7 +1884,40 @@ Local<UnboundScript> Script::GetUnboundScript() {
i::Handle<i::SharedFunctionInfo>(i::JSFunction::cast(*obj)->shared()));
}
bool Module::Instantiate(Local<Context> v8_context) {
int Module::GetModuleRequestsLength() const {
i::Handle<i::Module> self = Utils::OpenHandle(this);
i::Isolate* isolate = self->GetIsolate();
i::Handle<i::SharedFunctionInfo> shared;
if (self->code()->IsSharedFunctionInfo()) {
shared = i::handle(i::SharedFunctionInfo::cast(self->code()), isolate);
} else {
shared = i::handle(i::JSFunction::cast(self->code())->shared(), isolate);
}
return shared->scope_info()
->ModuleDescriptorInfo()
->module_requests()
->length();
}
Local<String> Module::GetModuleRequest(int i) const {
CHECK_GE(i, 0);
i::Handle<i::Module> self = Utils::OpenHandle(this);
i::Isolate* isolate = self->GetIsolate();
i::Handle<i::SharedFunctionInfo> shared;
if (self->code()->IsSharedFunctionInfo()) {
shared = i::handle(i::SharedFunctionInfo::cast(self->code()), isolate);
} else {
shared = i::handle(i::JSFunction::cast(self->code())->shared(), isolate);
}
i::Handle<i::FixedArray> module_requests(
shared->scope_info()->ModuleDescriptorInfo()->module_requests(), isolate);
CHECK_LT(i, module_requests->length());
return ToApiHandle<String>(i::handle(module_requests->get(i), isolate));
}
bool Module::Instantiate(Local<Context> v8_context,
Module::ResolveCallback callback,
Local<Value> callback_data) {
i::Handle<i::Module> self = Utils::OpenHandle(this);
i::Isolate* isolate = self->GetIsolate();
......@@ -1899,8 +1932,38 @@ bool Module::Instantiate(Local<Context> v8_context) {
shared, handle(context->native_context(), isolate));
self->set_code(*function);
// TODO(adamk): This could fail in the future when Instantiate
// does linking.
for (int i = 0, length = GetModuleRequestsLength(); i < length; ++i) {
Local<Module> import;
// TODO(adamk): Revisit these failure cases once d8 knows how to
// persist a module_map across multiple top-level module loads, as
// the current module is left in a "half-instantiated" state.
if (!callback(v8_context, GetModuleRequest(i), Utils::ToLocal(self),
callback_data)
.ToLocal(&import)) {
// TODO(adamk): Throw an exception.
return false;
}
if (!import->Instantiate(v8_context, callback, callback_data)) {
return false;
}
self->requested_modules()->set(i, *Utils::OpenHandle(*import));
}
// TODO(neis): This will create multiple cells for the same local variable if
// exported under multiple names, which is wrong but cannot be observed at the
// moment. This will be fixed by doing the full-fledged linking here once we
// get there.
i::Handle<i::FixedArray> regular_exports = i::handle(
shared->scope_info()->ModuleDescriptorInfo()->regular_exports(), isolate);
for (int i = 0, length = regular_exports->length(); i < length; ++i) {
i::Handle<i::ModuleInfoEntry> entry =
i::handle(i::ModuleInfoEntry::cast(regular_exports->get(i)), isolate);
DCHECK(entry->import_name()->IsUndefined(isolate));
i::Handle<i::String> export_name =
handle(i::String::cast(entry->export_name()), isolate);
i::Module::CreateExport(self, export_name);
}
return true;
}
......@@ -1916,6 +1979,19 @@ MaybeLocal<Value> Module::Evaluate(Local<Context> context) {
// It's an API error to call Evaluate before Instantiate.
CHECK(self->code()->IsJSFunction());
// Each module can only be evaluated once.
if (self->evaluated()) return Undefined(reinterpret_cast<Isolate*>(isolate));
self->set_evaluated(true);
i::Handle<i::FixedArray> requested_modules(self->requested_modules(),
isolate);
for (int i = 0, length = requested_modules->length(); i < length; ++i) {
i::Handle<i::Module> import(i::Module::cast(requested_modules->get(i)),
isolate);
MaybeLocal<Value> maybe_result = Utils::ToLocal(import)->Evaluate(context);
if (maybe_result.IsEmpty()) return maybe_result;
}
i::Handle<i::JSFunction> function(i::JSFunction::cast(self->code()), isolate);
DCHECK_EQ(i::MODULE_SCOPE, function->shared()->scope_info()->scope_type());
i::Handle<i::Object> receiver = isolate->factory()->undefined_value();
......@@ -2049,23 +2125,6 @@ MaybeLocal<Module> ScriptCompiler::CompileModule(Isolate* isolate,
i::Handle<i::SharedFunctionInfo> shared = Utils::OpenHandle(*unbound);
i::Handle<i::Module> module = i_isolate->factory()->NewModule(shared);
// TODO(neis): This will create multiple cells for the same local variable if
// exported under multiple names, which is wrong but cannot be observed at the
// moment. This will be fixed by doing the full-fledged linking here once we
// get there.
i::Handle<i::FixedArray> regular_exports =
i::handle(shared->scope_info()->ModuleDescriptorInfo()->regular_exports(),
i_isolate);
for (int i = 0, length = regular_exports->length(); i < length; ++i) {
i::Handle<i::ModuleInfoEntry> entry =
i::handle(i::ModuleInfoEntry::cast(regular_exports->get(i)), i_isolate);
DCHECK(entry->import_name()->IsUndefined(i_isolate));
i::Handle<i::String> export_name =
handle(i::String::cast(entry->export_name()), i_isolate);
i::Module::CreateExport(module, export_name);
}
return ToApiHandle<Module>(module);
}
......
......@@ -124,6 +124,8 @@ class Utils {
v8::internal::Handle<v8::internal::Context> obj);
static inline Local<Value> ToLocal(
v8::internal::Handle<v8::internal::Object> obj);
static inline Local<Module> ToLocal(
v8::internal::Handle<v8::internal::Module> obj);
static inline Local<Name> ToLocal(
v8::internal::Handle<v8::internal::Name> obj);
static inline Local<String> ToLocal(
......@@ -284,6 +286,7 @@ inline bool ToLocal(v8::internal::MaybeHandle<v8::internal::Object> maybe,
MAKE_TO_LOCAL(ToLocal, Context, Context)
MAKE_TO_LOCAL(ToLocal, Object, Value)
MAKE_TO_LOCAL(ToLocal, Module, Module)
MAKE_TO_LOCAL(ToLocal, Name, Name)
MAKE_TO_LOCAL(ToLocal, String, String)
MAKE_TO_LOCAL(ToLocal, Symbol, Symbol)
......
......@@ -9,6 +9,8 @@
#include <algorithm>
#include <fstream>
#include <map>
#include <utility>
#include <vector>
#ifdef ENABLE_VTUNE_JIT_INTERFACE
......@@ -461,7 +463,7 @@ MaybeLocal<Script> Shell::CompileString(
// Executes a string within the current v8 context.
bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
Local<Value> name, bool print_result,
bool report_exceptions, SourceType source_type) {
bool report_exceptions) {
HandleScope handle_scope(isolate);
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);
......@@ -472,31 +474,14 @@ bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
Local<Context> realm =
Local<Context>::New(isolate, data->realms_[data->realm_current_]);
Context::Scope context_scope(realm);
if (source_type == SCRIPT) {
Local<Script> script;
if (!Shell::CompileString(isolate, source, name, options.compile_options)
.ToLocal(&script)) {
// Print errors that happened during compilation.
if (report_exceptions) ReportException(isolate, &try_catch);
return false;
}
maybe_result = script->Run(realm);
} else {
DCHECK_EQ(MODULE, source_type);
Local<Module> module;
ScriptOrigin origin(name);
ScriptCompiler::Source script_source(source, origin);
// TODO(adamk): Make use of compile options for Modules.
if (!ScriptCompiler::CompileModule(isolate, &script_source)
.ToLocal(&module)) {
// Print errors that happened during compilation.
if (report_exceptions) ReportException(isolate, &try_catch);
return false;
}
// This can't fail until we support linking.
CHECK(module->Instantiate(realm));
maybe_result = module->Evaluate(realm);
Local<Script> script;
if (!Shell::CompileString(isolate, source, name, options.compile_options)
.ToLocal(&script)) {
// Print errors that happened during compilation.
if (report_exceptions) ReportException(isolate, &try_catch);
return false;
}
maybe_result = script->Run(realm);
EmptyMessageQueues(isolate);
data->realm_current_ = data->realm_switch_;
}
......@@ -526,6 +511,95 @@ bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
return true;
}
MaybeLocal<Module> Shell::FetchModuleTree(
Isolate* isolate, const std::string& file_name,
std::map<std::string, Global<Module>>* module_map) {
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);
Local<String> source_text = ReadFile(isolate, file_name.c_str());
if (source_text.IsEmpty()) {
printf("Error reading '%s'\n", file_name.c_str());
Shell::Exit(1);
}
ScriptOrigin origin(
String::NewFromUtf8(isolate, file_name.c_str(), NewStringType::kNormal)
.ToLocalChecked());
ScriptCompiler::Source source(source_text, origin);
Local<Module> module;
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
ReportException(isolate, &try_catch);
return MaybeLocal<Module>();
}
module_map->insert(
std::make_pair(file_name, Global<Module>(isolate, module)));
for (int i = 0, length = module->GetModuleRequestsLength(); i < length; ++i) {
Local<String> name = module->GetModuleRequest(i);
// TODO(adamk): Resolve the imported module to a full path.
std::string str = *String::Utf8Value(name);
if (!module_map->count(str)) {
if (FetchModuleTree(isolate, str, module_map).IsEmpty()) {
return MaybeLocal<Module>();
}
}
}
return module;
}
namespace {
MaybeLocal<Module> ResolveModuleCallback(Local<Context> context,
Local<String> specifier,
Local<Module> referrer,
Local<Value> data) {
Isolate* isolate = context->GetIsolate();
auto module_map = static_cast<std::map<std::string, Global<Module>>*>(
External::Cast(*data)->Value());
std::string str_specifier = *String::Utf8Value(specifier);
// TODO(adamk): Resolve the specifier using the referrer
auto it = module_map->find(str_specifier);
if (it != module_map->end()) {
return it->second.Get(isolate);
}
return MaybeLocal<Module>();
}
} // anonymous namespace
bool Shell::ExecuteModule(Isolate* isolate, const char* file_name) {
HandleScope handle_scope(isolate);
Local<Module> root_module;
std::map<std::string, Global<Module>> module_map;
if (!FetchModuleTree(isolate, file_name, &module_map).ToLocal(&root_module)) {
return false;
}
TryCatch try_catch(isolate);
try_catch.SetVerbose(true);
MaybeLocal<Value> maybe_result;
{
PerIsolateData* data = PerIsolateData::Get(isolate);
Local<Context> realm = data->realms_[data->realm_current_].Get(isolate);
Context::Scope context_scope(realm);
// This can't fail until we support linking.
CHECK(root_module->Instantiate(realm, ResolveModuleCallback,
External::New(isolate, &module_map)));
maybe_result = root_module->Evaluate(realm);
EmptyMessageQueues(isolate);
}
Local<Value> result;
if (!maybe_result.ToLocal(&result)) {
DCHECK(try_catch.HasCaught());
// Print errors that happened during execution.
ReportException(isolate, &try_catch);
return false;
}
DCHECK(!try_catch.HasCaught());
return true;
}
PerIsolateData::RealmScope::RealmScope(PerIsolateData* data) : data_(data) {
data_->realm_count_ = 1;
......@@ -1580,7 +1654,6 @@ void SourceGroup::Execute(Isolate* isolate) {
bool exception_was_thrown = false;
for (int i = begin_offset_; i < end_offset_; ++i) {
const char* arg = argv_[i];
Shell::SourceType source_type = Shell::SCRIPT;
if (strcmp(arg, "-e") == 0 && i + 1 < end_offset_) {
// Execute argument given to -e option directly.
HandleScope handle_scope(isolate);
......@@ -1599,8 +1672,13 @@ void SourceGroup::Execute(Isolate* isolate) {
continue;
} else if (strcmp(arg, "--module") == 0 && i + 1 < end_offset_) {
// Treat the next file as a module.
source_type = Shell::MODULE;
arg = argv_[++i];
Shell::options.script_executed = true;
if (!Shell::ExecuteModule(isolate, arg)) {
exception_was_thrown = true;
break;
}
continue;
} else if (arg[0] == '-') {
// Ignore other options. They have been parsed already.
continue;
......@@ -1617,8 +1695,7 @@ void SourceGroup::Execute(Isolate* isolate) {
Shell::Exit(1);
}
Shell::options.script_executed = true;
if (!Shell::ExecuteString(isolate, source, file_name, false, true,
source_type)) {
if (!Shell::ExecuteString(isolate, source, file_name, false, true)) {
exception_was_thrown = true;
break;
}
......
......@@ -5,6 +5,9 @@
#ifndef V8_D8_H_
#define V8_D8_H_
#include <map>
#include <string>
#include "src/allocation.h"
#include "src/base/hashmap.h"
#include "src/base/platform/time.h"
......@@ -313,15 +316,13 @@ class ShellOptions {
class Shell : public i::AllStatic {
public:
enum SourceType { SCRIPT, MODULE };
static MaybeLocal<Script> CompileString(
Isolate* isolate, Local<String> source, Local<Value> name,
v8::ScriptCompiler::CompileOptions compile_options);
static bool ExecuteString(Isolate* isolate, Local<String> source,
Local<Value> name, bool print_result,
bool report_exceptions,
SourceType source_type = SCRIPT);
bool report_exceptions);
static bool ExecuteModule(Isolate* isolate, const char* file_name);
static const char* ToCString(const v8::String::Utf8Value& value);
static void ReportException(Isolate* isolate, TryCatch* try_catch);
static Local<String> ReadFile(Isolate* isolate, const char* name);
......@@ -454,6 +455,9 @@ class Shell : public i::AllStatic {
static Local<ObjectTemplate> CreateGlobalTemplate(Isolate* isolate);
static MaybeLocal<Context> CreateRealm(
const v8::FunctionCallbackInfo<v8::Value>& args);
static MaybeLocal<Module> FetchModuleTree(
Isolate* isolate, const std::string& file_name,
std::map<std::string, Global<Module>>* module_map);
};
......
......@@ -1726,6 +1726,7 @@ Handle<Module> Factory::NewModule(Handle<SharedFunctionInfo> code) {
module->set_code(*code);
module->set_exports(*exports);
module->set_requested_modules(*requested_modules);
module->set_flags(0);
return module;
}
......
......@@ -906,6 +906,7 @@ void Module::ModuleVerify() {
code()->ObjectVerify();
exports()->ObjectVerify();
requested_modules()->ObjectVerify();
VerifySmiField(kFlagsOffset);
// TODO(neis): Check more.
}
......
......@@ -5704,6 +5704,8 @@ ACCESSORS(ContextExtension, extension, Object, kExtensionOffset)
ACCESSORS(Module, code, Object, kCodeOffset)
ACCESSORS(Module, exports, ObjectHashTable, kExportsOffset)
ACCESSORS(Module, requested_modules, FixedArray, kRequestedModulesOffset)
SMI_ACCESSORS(Module, flags, kFlagsOffset)
BOOL_ACCESSORS(Module, flags, evaluated, kEvaluatedBit)
ACCESSORS(AccessorPair, getter, Object, kGetterOffset)
ACCESSORS(AccessorPair, setter, Object, kSetterOffset)
......
......@@ -1160,6 +1160,7 @@ void Module::ModulePrint(std::ostream& os) { // NOLINT
os << "\n - code: " << Brief(code());
os << "\n - exports: " << Brief(exports());
os << "\n - requested_modules: " << Brief(requested_modules());
os << "\n - evaluated: " << evaluated();
os << "\n";
}
......
......@@ -7898,6 +7898,13 @@ class Module : public Struct {
// ModuleInfo::module_requests.
DECL_ACCESSORS(requested_modules, FixedArray)
// [[Evaluated]]: Whether this module has been evaluated. Modules
// are only evaluated a single time.
DECL_BOOLEAN_ACCESSORS(evaluated)
// Storage for [[Evaluated]]
DECL_INT_ACCESSORS(flags)
static void CreateExport(Handle<Module> module, Handle<String> name);
static void StoreExport(Handle<Module> module, Handle<String> name,
Handle<Object> value);
......@@ -7906,9 +7913,12 @@ class Module : public Struct {
static const int kCodeOffset = HeapObject::kHeaderSize;
static const int kExportsOffset = kCodeOffset + kPointerSize;
static const int kRequestedModulesOffset = kExportsOffset + kPointerSize;
static const int kSize = kRequestedModulesOffset + kPointerSize;
static const int kFlagsOffset = kRequestedModulesOffset + kPointerSize;
static const int kSize = kFlagsOffset + kPointerSize;
private:
enum { kEvaluatedBit };
DISALLOW_IMPLICIT_CONSTRUCTORS(Module);
};
......
......@@ -147,6 +147,7 @@ v8_executable("cctest") {
"test-lockers.cc",
"test-log.cc",
"test-mementos.cc",
"test-modules.cc",
"test-object.cc",
"test-parsing.cc",
"test-platform.cc",
......
......@@ -169,6 +169,7 @@
'test-lockers.cc',
'test-log.cc',
'test-mementos.cc',
'test-modules.cc',
'test-object.cc',
'test-parsing.cc',
'test-platform.cc',
......
......@@ -439,4 +439,10 @@
'*': [SKIP],
}], # variant == asm_wasm
['variant != ignition and variant != ignition_staging', {
# Ongoing implementation of modules.
# https://bugs.chromium.org/p/v8/issues/detail?id=1569
'test-modules/*': [SKIP],
}], # variants != ignition and variant != ignition_staging
]
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/flags.h"
#include "test/cctest/cctest.h"
using v8::Context;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::Module;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::Value;
static MaybeLocal<Module> AlwaysEmptyResolveCallback(Local<Context> context,
Local<String> specifier,
Local<Module> referrer,
Local<Value> data) {
return MaybeLocal<Module>();
}
static int g_count = 0;
static MaybeLocal<Module> FailOnSecondCallResolveCallback(
Local<Context> context, Local<String> specifier, Local<Module> referrer,
Local<Value> data) {
if (g_count++ > 0) return MaybeLocal<Module>();
Local<String> source_text = v8_str("");
ScriptOrigin origin(v8_str("module.js"));
ScriptCompiler::Source source(source_text, origin);
return ScriptCompiler::CompileModule(CcTest::isolate(), &source)
.ToLocalChecked();
}
TEST(ModuleInstantiationFailures) {
Isolate* isolate = CcTest::isolate();
HandleScope scope(isolate);
LocalContext env;
Local<String> source_text = v8_str(
"import './foo.js';"
"export {} from './bar.js';");
ScriptOrigin origin(v8_str("file.js"));
ScriptCompiler::Source source(source_text, origin);
Local<Module> module =
ScriptCompiler::CompileModule(isolate, &source).ToLocalChecked();
CHECK_EQ(2, module->GetModuleRequestsLength());
CHECK(v8_str("./foo.js")->StrictEquals(module->GetModuleRequest(0)));
CHECK(v8_str("./bar.js")->StrictEquals(module->GetModuleRequest(1)));
// Instantiation should fail.
CHECK(!module->Instantiate(env.local(), AlwaysEmptyResolveCallback));
// Start over again...
module = ScriptCompiler::CompileModule(isolate, &source).ToLocalChecked();
// Instantiation should fail if a sub-module fails to resolve.
g_count = 0;
CHECK(!module->Instantiate(env.local(), FailOnSecondCallResolveCallback));
}
static MaybeLocal<Module> CompileSpecifierAsModuleResolveCallback(
Local<Context> context, Local<String> specifier, Local<Module> referrer,
Local<Value> data) {
ScriptOrigin origin(v8_str("module.js"));
ScriptCompiler::Source source(specifier, origin);
return ScriptCompiler::CompileModule(CcTest::isolate(), &source)
.ToLocalChecked();
}
TEST(ModuleEvaluation) {
Isolate* isolate = CcTest::isolate();
HandleScope scope(isolate);
LocalContext env;
Local<String> source_text = v8_str(
"import 'Object.expando = 5';"
"import 'Object.expando *= 2';");
ScriptOrigin origin(v8_str("file.js"));
ScriptCompiler::Source source(source_text, origin);
Local<Module> module =
ScriptCompiler::CompileModule(isolate, &source).ToLocalChecked();
CHECK(module->Instantiate(env.local(),
CompileSpecifierAsModuleResolveCallback));
CHECK(!module->Evaluate(env.local()).IsEmpty());
ExpectInt32("Object.expando", 10);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment