From 00597ffde1ebb05a6c60ea7e09e6578c11f92820 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Wed, 30 Jan 2019 17:21:31 -0500 Subject: [PATCH] Refactor libdeno ES module interface. (#1624) Allows for future asynchronous module loading. Add support for import.meta.url Fixes #1496 --- libdeno/BUILD.gn | 2 + libdeno/api.cc | 20 -- libdeno/binding.cc | 288 ++++++----------------- libdeno/deno.h | 49 ++-- libdeno/internal.h | 46 ++-- libdeno/libdeno_test.cc | 143 ++--------- libdeno/modules.cc | 206 ++++++++++++++++ libdeno/modules_test.cc | 268 +++++++++++++++++++++ libdeno/snapshot_creator.cc | 2 +- src/deno_dir.rs | 5 +- src/errors.rs | 18 ++ src/isolate.rs | 235 ++++++++++++++---- src/libdeno.rs | 45 +++- src/main.rs | 6 +- tests/circular1.js | 2 + tests/circular1.js.out | 2 + tests/circular1.test | 2 + tests/circular2.js | 2 + tests/error_009_missing_js_module.js.out | 2 +- tests/imports_meta.js | 3 + tests/imports_meta.js.out | 2 + tests/imports_meta.test | 2 + tests/imports_meta2.js | 1 + 23 files changed, 884 insertions(+), 467 deletions(-) create mode 100644 libdeno/modules.cc create mode 100644 libdeno/modules_test.cc create mode 100644 tests/circular1.js create mode 100644 tests/circular1.js.out create mode 100644 tests/circular1.test create mode 100644 tests/circular2.js create mode 100644 tests/imports_meta.js create mode 100644 tests/imports_meta.js.out create mode 100644 tests/imports_meta.test create mode 100644 tests/imports_meta2.js diff --git a/libdeno/BUILD.gn b/libdeno/BUILD.gn index 4620dba387..6cea30b2aa 100644 --- a/libdeno/BUILD.gn +++ b/libdeno/BUILD.gn @@ -52,6 +52,7 @@ v8_source_set("libdeno") { "file_util.cc", "file_util.h", "internal.h", + "modules.cc", ] deps = [ ":v8", @@ -84,6 +85,7 @@ v8_executable("test_cc") { sources = [ "file_util_test.cc", "libdeno_test.cc", + "modules_test.cc", "test.cc", ] deps = [ diff --git a/libdeno/api.cc b/libdeno/api.cc index 1c62cdd5ac..af3c4b2156 100644 --- a/libdeno/api.cc +++ b/libdeno/api.cc @@ -80,7 +80,6 @@ deno::DenoIsolate* unwrap(Deno* d_) { deno_buf deno_get_snapshot(Deno* d_) { auto* d = unwrap(d_); CHECK_NE(d->snapshot_creator_, nullptr); - CHECK(d->resolve_module_.IsEmpty()); d->ClearModules(); d->context_.Reset(); @@ -126,20 +125,6 @@ int deno_execute(Deno* d_, void* user_data, const char* js_filename, return deno::Execute(context, js_filename, js_source) ? 1 : 0; } -int deno_execute_mod(Deno* d_, void* user_data, const char* js_filename, - const char* js_source, int resolve_only) { - auto* d = unwrap(d_); - deno::UserDataScope user_data_scope(d, user_data); - auto* isolate = d->isolate_; - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handle_scope(isolate); - auto context = d->context_.Get(d->isolate_); - CHECK(!context.IsEmpty()); - return deno::ExecuteMod(context, js_filename, js_source, resolve_only) ? 1 - : 0; -} - int deno_respond(Deno* d_, void* user_data, int32_t req_id, deno_buf buf) { auto* d = unwrap(d_); if (d->current_args_ != nullptr) { @@ -210,9 +195,4 @@ void deno_terminate_execution(Deno* d_) { deno::DenoIsolate* d = reinterpret_cast(d_); d->isolate_->TerminateExecution(); } - -void deno_resolve_ok(Deno* d_, const char* filename, const char* source) { - deno::DenoIsolate* d = reinterpret_cast(d_); - d->ResolveOk(filename, source); -} } diff --git a/libdeno/binding.cc b/libdeno/binding.cc index 39007405fa..6c636774a9 100644 --- a/libdeno/binding.cc +++ b/libdeno/binding.cc @@ -246,6 +246,57 @@ v8::Local DenoIsolate::GetBuiltinModules() { return handle_scope.Escape(builtin_modules_.Get(isolate_)); } +v8::ScriptOrigin ModuleOrigin(v8::Isolate* isolate, + v8::Local resource_name) { + return v8::ScriptOrigin(resource_name, v8::Local(), + v8::Local(), v8::Local(), + v8::Local(), v8::Local(), + v8::Local(), v8::Local(), + v8::True(isolate)); +} + +deno_mod DenoIsolate::RegisterModule(const char* name, const char* source) { + v8::Isolate::Scope isolate_scope(isolate_); + v8::Locker locker(isolate_); + v8::HandleScope handle_scope(isolate_); + auto context = context_.Get(isolate_); + v8::Context::Scope context_scope(context); + + v8::Local name_str = v8_str(name, true); + v8::Local source_str = v8_str(source, true); + + auto origin = ModuleOrigin(isolate_, name_str); + v8::ScriptCompiler::Source source_(source_str, origin); + + v8::TryCatch try_catch(isolate_); + + auto maybe_module = v8::ScriptCompiler::CompileModule(isolate_, &source_); + + if (try_catch.HasCaught()) { + CHECK(maybe_module.IsEmpty()); + HandleException(context, try_catch.Exception()); + return 0; + } + + auto module = maybe_module.ToLocalChecked(); + + int id = module->GetIdentityHash(); + + std::vector import_specifiers; + + for (int i = 0; i < module->GetModuleRequestsLength(); ++i) { + v8::Local specifier = module->GetModuleRequest(i); + v8::String::Utf8Value specifier_utf8(isolate_, specifier); + import_specifiers.push_back(*specifier_utf8); + } + + mods_.emplace(std::piecewise_construct, std::make_tuple(id), + std::make_tuple(isolate_, module, name, import_specifiers)); + mods_by_name_[name] = id; + + return id; +} + void BuiltinModules(v8::Local property, const v8::PropertyCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); @@ -275,214 +326,12 @@ void Shared(v8::Local property, info.GetReturnValue().Set(ab); } -v8::ScriptOrigin ModuleOrigin(v8::Local resource_name, - v8::Isolate* isolate) { - return v8::ScriptOrigin(resource_name, v8::Local(), - v8::Local(), v8::Local(), - v8::Local(), v8::Local(), - v8::Local(), v8::Local(), - v8::True(isolate)); -} - void DenoIsolate::ClearModules() { - for (auto it = module_map_.begin(); it != module_map_.end(); it++) { - it->second.Reset(); + for (auto it = mods_.begin(); it != mods_.end(); it++) { + it->second.handle.Reset(); } - module_map_.clear(); - for (auto it = module_info_map_.begin(); it != module_info_map_.end(); it++) { - it->second.second.Reset(); - } - module_info_map_.clear(); -} - -void DenoIsolate::RegisterModule(const char* filename, - v8::Local module) { - int id = module->GetIdentityHash(); - - module_map_.emplace(std::piecewise_construct, std::make_tuple(filename), - std::make_tuple(isolate_, module)); - - // Identity hash is not necessarily unique - // Therefore, we store a persistent handle along with filenames - // such that we can compare the identites and select the correct module - module_info_map_.emplace( - std::piecewise_construct, std::make_tuple(id), - std::make_tuple(std::piecewise_construct, std::make_tuple(filename), - std::make_tuple(isolate_, module))); -} - -v8::MaybeLocal CompileModule(v8::Local context, - const char* js_filename, - v8::Local source_text) { - auto* isolate = context->GetIsolate(); - - v8::Isolate::Scope isolate_scope(isolate); - v8::EscapableHandleScope handle_scope(isolate); - v8::Context::Scope context_scope(context); - - auto origin = ModuleOrigin(v8_str(js_filename, true), isolate); - v8::ScriptCompiler::Source source(source_text, origin); - - auto maybe_module = v8::ScriptCompiler::CompileModule(isolate, &source); - - if (!maybe_module.IsEmpty()) { - auto module = maybe_module.ToLocalChecked(); - CHECK_EQ(v8::Module::kUninstantiated, module->GetStatus()); - DenoIsolate* d = DenoIsolate::FromIsolate(isolate); - d->RegisterModule(js_filename, module); - } - - return handle_scope.EscapeMaybe(maybe_module); -} - -v8::MaybeLocal ResolveCallback(v8::Local context, - v8::Local specifier, - v8::Local referrer) { - auto* isolate = context->GetIsolate(); - DenoIsolate* d = DenoIsolate::FromIsolate(isolate); - - v8::Isolate::Scope isolate_scope(isolate); - v8::EscapableHandleScope handle_scope(isolate); - v8::Context::Scope context_scope(context); - - v8::String::Utf8Value specifier_utf8val(isolate, specifier); - const char* specifier_cstr = ToCString(specifier_utf8val); - - auto builtin_modules = d->GetBuiltinModules(); - bool has_builtin = builtin_modules->Has(context, specifier).ToChecked(); - if (has_builtin) { - auto val = builtin_modules->Get(context, specifier).ToLocalChecked(); - CHECK(val->IsObject()); - auto obj = val->ToObject(isolate); - - // In order to export obj as a module, we must iterate over its properties - // and export them each individually. - std::string src = "let globalEval = eval\nlet g = globalEval('this');\n"; - auto names = obj->GetOwnPropertyNames(context).ToLocalChecked(); - for (uint32_t i = 0; i < names->Length(); i++) { - auto name = names->Get(context, i).ToLocalChecked(); - v8::String::Utf8Value name_utf8val(isolate, name); - const char* name_cstr = ToCString(name_utf8val); - src.append("export const "); - src.append(name_cstr); - src.append(" = g.libdeno.builtinModules."); - src.append(specifier_cstr); - src.append("."); - src.append(name_cstr); - src.append(";\n"); - } - auto export_str = v8_str(src.c_str(), true); - - auto module = - CompileModule(context, specifier_cstr, export_str).ToLocalChecked(); - auto maybe_ok = module->InstantiateModule(context, ResolveCallback); - CHECK(!maybe_ok.IsNothing()); - - return handle_scope.Escape(module); - } - - int ref_id = referrer->GetIdentityHash(); - auto range = d->module_info_map_.equal_range(ref_id); - std::string referrer_filename; - for (auto it = range.first; it != range.second; ++it) { - // it->second: > - // operator== compares value identities stored in the handles - // https://denolib.github.io/v8-docs/include_2v8_8h_source.html#l00487 - // Due to possibilities of identity hash collision, this is necessary - if (it->second.second == referrer) { - referrer_filename = it->second.first; - break; - } - } - CHECK_NE(referrer_filename.size(), 0); - - v8::String::Utf8Value specifier_(isolate, specifier); - const char* specifier_c = ToCString(specifier_); - - CHECK_NE(d->resolve_cb_, nullptr); - d->resolve_cb_(d->user_data_, specifier_c, referrer_filename.c_str()); - - if (d->resolve_module_.IsEmpty()) { - // Resolution Error. - std::stringstream err_ss; - err_ss << "NotFound: Cannot resolve module \"" << specifier_c - << "\" from \"" << referrer_filename << "\""; - auto resolve_error = v8_str(err_ss.str().c_str()); - isolate->ThrowException(resolve_error); - return v8::MaybeLocal(); - } else { - auto module = d->resolve_module_.Get(isolate); - d->resolve_module_.Reset(); - return handle_scope.Escape(module); - } -} - -void DenoIsolate::ResolveOk(const char* filename, const char* source) { - CHECK(resolve_module_.IsEmpty()); - auto count = module_map_.count(filename); - if (count == 1) { - auto module = module_map_[filename].Get(isolate_); - resolve_module_.Reset(isolate_, module); - } else { - CHECK_EQ(count, 0); - v8::HandleScope handle_scope(isolate_); - auto context = context_.Get(isolate_); - v8::TryCatch try_catch(isolate_); - auto maybe_module = CompileModule(context, filename, v8_str(source, true)); - if (maybe_module.IsEmpty()) { - DCHECK(try_catch.HasCaught()); - HandleException(context, try_catch.Exception()); - } else { - auto module = maybe_module.ToLocalChecked(); - resolve_module_.Reset(isolate_, module); - } - } -} - -bool ExecuteMod(v8::Local context, const char* js_filename, - const char* js_source, bool resolve_only) { - auto* isolate = context->GetIsolate(); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handle_scope(isolate); - v8::Context::Scope context_scope(context); - - auto source = v8_str(js_source, true); - - v8::TryCatch try_catch(isolate); - - auto maybe_module = CompileModule(context, js_filename, source); - - if (maybe_module.IsEmpty()) { - DCHECK(try_catch.HasCaught()); - HandleException(context, try_catch.Exception()); - return false; - } - DCHECK(!try_catch.HasCaught()); - - auto module = maybe_module.ToLocalChecked(); - auto maybe_ok = module->InstantiateModule(context, ResolveCallback); - if (maybe_ok.IsNothing()) { - DCHECK(try_catch.HasCaught()); - HandleException(context, try_catch.Exception()); - return false; - } - - CHECK_EQ(v8::Module::kInstantiated, module->GetStatus()); - - if (resolve_only) { - return true; - } - - auto result = module->Evaluate(context); - - if (result.IsEmpty()) { - DCHECK(try_catch.HasCaught()); - CHECK_EQ(v8::Module::kErrored, module->GetStatus()); - HandleException(context, module->GetException()); - return false; - } - - return true; + mods_.clear(); + mods_by_name_.clear(); } bool Execute(v8::Local context, const char* js_filename, @@ -558,18 +407,35 @@ void MessageCallback(v8::Local message, HandleExceptionMessage(context, message); } +void HostInitializeImportMetaObjectCallback(v8::Local context, + v8::Local module, + v8::Local meta) { + auto* isolate = context->GetIsolate(); + DenoIsolate* d = DenoIsolate::FromIsolate(isolate); + v8::Isolate::Scope isolate_scope(isolate); + + CHECK(!module.IsEmpty()); + + deno_mod id = module->GetIdentityHash(); + CHECK_NE(id, 0); + + auto* info = d->GetModuleInfo(id); + + const char* url = info->name.c_str(); + + meta->CreateDataProperty(context, v8_str("url"), v8_str(url, true)) + .ToChecked(); +} + void DenoIsolate::AddIsolate(v8::Isolate* isolate) { isolate_ = isolate; - // Leaving this code here because it will probably be useful later on, but - // disabling it now as I haven't got tests for the desired behavior. - // d->isolate->SetAbortOnUncaughtExceptionCallback(AbortOnUncaughtExceptionCallback); - // d->isolate->AddMessageListener(MessageCallback2); - // d->isolate->SetFatalErrorHandler(FatalErrorCallback2); isolate_->SetCaptureStackTraceForUncaughtExceptions( true, 10, v8::StackTrace::kDetailed); isolate_->SetPromiseRejectCallback(deno::PromiseRejectCallback); isolate_->SetData(0, this); isolate_->AddMessageListener(MessageCallback); + isolate->SetHostInitializeImportMetaObjectCallback( + HostInitializeImportMetaObjectCallback); } } // namespace deno diff --git a/libdeno/deno.h b/libdeno/deno.h index 7568002ae5..42df808258 100644 --- a/libdeno/deno.h +++ b/libdeno/deno.h @@ -25,26 +25,15 @@ typedef struct deno_s Deno; typedef void (*deno_recv_cb)(void* user_data, int32_t req_id, deno_buf control_buf, deno_buf data_buf); -// A callback to implement ES Module imports. User must call deno_resolve_ok() -// at most once during deno_resolve_cb. If deno_resolve_ok() is not called, the -// specifier is considered invalid and will issue an error in JS. The reason -// deno_resolve_cb does not return deno_module is to avoid unnecessary heap -// allocations. -typedef void (*deno_resolve_cb)(void* user_data, const char* specifier, - const char* referrer); - -void deno_resolve_ok(Deno* d, const char* filename, const char* source); - void deno_init(); const char* deno_v8_version(); void deno_set_v8_flags(int* argc, char** argv); typedef struct { - int will_snapshot; // Default 0. If calling deno_get_snapshot 1. - deno_buf load_snapshot; // Optionally: A deno_buf from deno_get_snapshot. - deno_buf shared; // Shared buffer to be mapped to libdeno.shared - deno_recv_cb recv_cb; // Maps to libdeno.send() calls. - deno_resolve_cb resolve_cb; // Each import calls this. + int will_snapshot; // Default 0. If calling deno_get_snapshot 1. + deno_buf load_snapshot; // Optionally: A deno_buf from deno_get_snapshot. + deno_buf shared; // Shared buffer to be mapped to libdeno.shared + deno_recv_cb recv_cb; // Maps to libdeno.send() calls. } deno_config; // Create a new deno isolate. @@ -65,16 +54,6 @@ void deno_delete(Deno* d); int deno_execute(Deno* d, void* user_data, const char* js_filename, const char* js_source); -// Compile and execute an ES module. Caller must have provided a deno_resolve_cb -// when instantiating the Deno object. -// Return value: 0 = fail, 1 = success -// Get error text with deno_last_exception(). -// If resolve_only is 0, compile and evaluate the module. -// If resolve_only is 1, compile and collect dependencies of the module -// without running the code. -int deno_execute_mod(Deno* d, void* user_data, const char* js_filename, - const char* js_source, int resolve_only); - // deno_respond sends up to one message back for every deno_recv_cb made. // // If this is called during deno_recv_cb, the issuing libdeno.send() in @@ -101,6 +80,26 @@ const char* deno_last_exception(Deno* d); void deno_terminate_execution(Deno* d); +// Module API + +typedef int deno_mod; + +// Returns zero on error - check deno_last_exception(). +deno_mod deno_mod_new(Deno* d, const char* name, const char* source); + +size_t deno_mod_imports_len(Deno* d, deno_mod id); + +// Returned pointer is valid for the lifetime of the Deno isolate "d". +const char* deno_mod_imports_get(Deno* d, deno_mod id, size_t index); + +typedef deno_mod (*deno_resolve_cb)(void* user_data, const char* specifier, + deno_mod referrer); + +void deno_mod_instantiate(Deno* d, void* user_data, deno_mod id, + deno_resolve_cb cb); + +void deno_mod_evaluate(Deno* d, void* user_data, deno_mod id); + #ifdef __cplusplus } // extern "C" #endif diff --git a/libdeno/internal.h b/libdeno/internal.h index 58dae013c9..022fd8f1e9 100644 --- a/libdeno/internal.h +++ b/libdeno/internal.h @@ -5,12 +5,25 @@ #include #include #include +#include #include "deno.h" #include "third_party/v8/include/v8.h" #include "third_party/v8/src/base/logging.h" namespace deno { +struct ModuleInfo { + std::string name; + v8::Persistent handle; + std::vector import_specifiers; + + ModuleInfo(v8::Isolate* isolate, v8::Local module, + const char* name_, std::vector import_specifiers_) + : name(name_), import_specifiers(import_specifiers_) { + handle.Reset(isolate, module); + } +}; + // deno_s = Wrapped Isolate. class DenoIsolate { public: @@ -21,9 +34,9 @@ class DenoIsolate { snapshot_creator_(nullptr), global_import_buf_ptr_(nullptr), recv_cb_(config.recv_cb), - resolve_cb_(config.resolve_cb), next_req_id_(0), - user_data_(nullptr) { + user_data_(nullptr), + resolve_cb_(nullptr) { array_buffer_allocator_ = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); if (config.load_snapshot.data_ptr) { snapshot_.data = @@ -46,11 +59,22 @@ class DenoIsolate { } void AddIsolate(v8::Isolate* isolate); - void RegisterModule(const char* filename, v8::Local module); - void ResolveOk(const char* filename, const char* source); + + deno_mod RegisterModule(const char* name, const char* source); + v8::Local GetBuiltinModules(); void ClearModules(); - v8::Local GetBuiltinModules(); + ModuleInfo* GetModuleInfo(deno_mod id) { + if (id == 0) { + return nullptr; + } + auto it = mods_.find(id); + if (it != mods_.end()) { + return &it->second; + } else { + return nullptr; + } + } v8::Isolate* isolate_; v8::ArrayBuffer::Allocator* array_buffer_allocator_; @@ -59,19 +83,13 @@ class DenoIsolate { v8::SnapshotCreator* snapshot_creator_; void* global_import_buf_ptr_; deno_recv_cb recv_cb_; - deno_resolve_cb resolve_cb_; int32_t next_req_id_; void* user_data_; - // identity hash -> filename, module (avoid hash collision) - std::multimap>> - module_info_map_; - // filename -> Module - std::map> module_map_; - // Set by deno_resolve_ok - v8::Persistent resolve_module_; - v8::Persistent builtin_modules_; + std::map mods_; + std::map mods_by_name_; + deno_resolve_cb resolve_cb_; v8::Persistent context_; std::map> async_data_map_; diff --git a/libdeno/libdeno_test.cc b/libdeno/libdeno_test.cc index 6036746638..5628be00e3 100644 --- a/libdeno/libdeno_test.cc +++ b/libdeno/libdeno_test.cc @@ -3,18 +3,18 @@ TEST(LibDenoTest, InitializesCorrectly) { EXPECT_NE(snapshot.data_ptr, nullptr); - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "1 + 2")); deno_delete(d); } TEST(LibDenoTest, Snapshotter) { - Deno* d1 = deno_new(deno_config{1, empty, empty, nullptr, nullptr}); + Deno* d1 = deno_new(deno_config{1, empty, empty, nullptr}); EXPECT_TRUE(deno_execute(d1, nullptr, "a.js", "a = 1 + 2")); deno_buf test_snapshot = deno_get_snapshot(d1); deno_delete(d1); - Deno* d2 = deno_new(deno_config{0, test_snapshot, empty, nullptr, nullptr}); + Deno* d2 = deno_new(deno_config{0, test_snapshot, empty, nullptr}); EXPECT_TRUE( deno_execute(d2, nullptr, "b.js", "if (a != 3) throw Error('x');")); deno_delete(d2); @@ -23,14 +23,14 @@ TEST(LibDenoTest, Snapshotter) { } TEST(LibDenoTest, CanCallFunction) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "if (CanCallFunction() != 'foo') throw Error();")); deno_delete(d); } TEST(LibDenoTest, ErrorsCorrectly) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "throw Error()")); deno_delete(d); } @@ -75,7 +75,7 @@ TEST(LibDenoTest, RecvReturnEmpty) { EXPECT_EQ(buf.data_ptr[1], 'b'); EXPECT_EQ(buf.data_ptr[2], 'c'); }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "RecvReturnEmpty()")); EXPECT_EQ(count, 2); deno_delete(d); @@ -93,14 +93,14 @@ TEST(LibDenoTest, RecvReturnBar) { EXPECT_EQ(buf.data_ptr[2], 'c'); deno_respond(d, user_data, req_id, strbuf("bar")); }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_TRUE(deno_execute(d, d, "a.js", "RecvReturnBar()")); EXPECT_EQ(count, 1); deno_delete(d); } TEST(LibDenoTest, DoubleRecvFails) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "DoubleRecvFails()")); deno_delete(d); } @@ -134,7 +134,7 @@ TEST(LibDenoTest, SendRecvSlice) { // Send back. deno_respond(d, user_data, req_id, buf2); }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_TRUE(deno_execute(d, d, "a.js", "SendRecvSlice()")); EXPECT_EQ(count, 5); deno_delete(d); @@ -151,26 +151,26 @@ TEST(LibDenoTest, JSSendArrayBufferViewTypes) { EXPECT_EQ(buf.alloc_len, 4321u); EXPECT_EQ(buf.data_ptr[0], count); }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "JSSendArrayBufferViewTypes()")); EXPECT_EQ(count, 3); deno_delete(d); } TEST(LibDenoTest, TypedArraySnapshots) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "TypedArraySnapshots()")); deno_delete(d); } TEST(LibDenoTest, SnapshotBug) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "SnapshotBug()")); deno_delete(d); } TEST(LibDenoTest, GlobalErrorHandling) { - Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, nullptr}); EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "GlobalErrorHandling()")); std::string expected = "{\"message\":\"Uncaught ReferenceError: notdefined is not defined\"," @@ -199,7 +199,7 @@ TEST(LibDenoTest, DataBuf) { EXPECT_EQ(buf.data_ptr[0], 1); EXPECT_EQ(buf.data_ptr[1], 2); }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "DataBuf()")); EXPECT_EQ(count, 1); // data_buf was subsequently changed in JS, let's check that our copy reflects @@ -212,7 +212,7 @@ TEST(LibDenoTest, DataBuf) { TEST(LibDenoTest, CheckPromiseErrors) { static int count = 0; auto recv_cb = [](auto _, int req_id, auto buf, auto data_buf) { count++; }; - Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, empty, recv_cb}); EXPECT_EQ(deno_last_exception(d), nullptr); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "CheckPromiseErrors()")); EXPECT_EQ(deno_last_exception(d), nullptr); @@ -225,7 +225,7 @@ TEST(LibDenoTest, CheckPromiseErrors) { } TEST(LibDenoTest, LastException) { - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, empty, empty, nullptr}); EXPECT_EQ(deno_last_exception(d), nullptr); EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "\n\nthrow Error('boo');\n\n")); EXPECT_STREQ(deno_last_exception(d), @@ -240,7 +240,7 @@ TEST(LibDenoTest, LastException) { } TEST(LibDenoTest, EncodeErrorBug) { - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, empty, empty, nullptr}); EXPECT_EQ(deno_last_exception(d), nullptr); EXPECT_FALSE(deno_execute(d, nullptr, "a.js", "eval('a')")); EXPECT_STREQ( @@ -259,117 +259,10 @@ TEST(LibDenoTest, EncodeErrorBug) { TEST(LibDenoTest, Shared) { uint8_t s[] = {0, 1, 2}; deno_buf shared = {nullptr, 0, s, 3}; - Deno* d = deno_new(deno_config{0, snapshot, shared, nullptr, nullptr}); + Deno* d = deno_new(deno_config{0, snapshot, shared, nullptr}); EXPECT_TRUE(deno_execute(d, nullptr, "a.js", "Shared()")); EXPECT_EQ(s[0], 42); EXPECT_EQ(s[1], 43); EXPECT_EQ(s[2], 44); deno_delete(d); } - -static const char* mod_a = - "import { retb } from 'b.js'\n" - "if (retb() != 'b') throw Error();"; - -static const char* mod_b = "export function retb() { return 'b' }"; - -TEST(LibDenoTest, ModuleResolution) { - static int count = 0; - auto resolve_cb = [](void* user_data, const char* specifier, - const char* referrer) { - EXPECT_STREQ(specifier, "b.js"); - EXPECT_STREQ(referrer, "a.js"); - count++; - auto d = reinterpret_cast(user_data); - deno_resolve_ok(d, "b.js", mod_b); - }; - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, resolve_cb}); - EXPECT_TRUE(deno_execute_mod(d, d, "a.js", mod_a, false)); - EXPECT_EQ(count, 1); - deno_delete(d); -} - -TEST(LibDenoTest, ModuleResolutionFail) { - static int count = 0; - auto resolve_cb = [](void* user_data, const char* specifier, - const char* referrer) { - EXPECT_STREQ(specifier, "b.js"); - EXPECT_STREQ(referrer, "a.js"); - count++; - // Do not call deno_resolve_ok(); - }; - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, resolve_cb}); - EXPECT_FALSE(deno_execute_mod(d, d, "a.js", mod_a, false)); - EXPECT_EQ(count, 1); - deno_delete(d); -} - -TEST(LibDenoTest, ModuleSnapshot) { - Deno* d1 = deno_new(deno_config{1, empty, empty, nullptr, nullptr}); - EXPECT_TRUE(deno_execute_mod(d1, nullptr, "x.js", - "const globalEval = eval\n" - "const global = globalEval('this')\n" - "global.a = 1 + 2", - 0)); - deno_buf test_snapshot = deno_get_snapshot(d1); - deno_delete(d1); - - const char* y_src = "if (a != 3) throw Error('x');"; - - deno_config config{0, test_snapshot, empty, nullptr, nullptr}; - Deno* d2 = deno_new(config); - EXPECT_TRUE(deno_execute(d2, nullptr, "y.js", y_src)); - deno_delete(d2); - - Deno* d3 = deno_new(config); - EXPECT_TRUE(deno_execute_mod(d3, nullptr, "y.js", y_src, false)); - deno_delete(d3); - - delete[] test_snapshot.data_ptr; -} - -TEST(LibDenoTest, ModuleResolveOnly) { - static int count = 0; - auto resolve_cb = [](void* user_data, const char* specifier, - const char* referrer) { - EXPECT_STREQ(specifier, "b.js"); - EXPECT_STREQ(referrer, "a.js"); - count++; - auto d = reinterpret_cast(user_data); - deno_resolve_ok(d, "b.js", mod_b); - }; - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, resolve_cb}); - // Code should not execute. If executed, the error would be thrown - EXPECT_TRUE(deno_execute_mod(d, d, "a.js", - "import { retb } from 'b.js'\n" - "throw Error('unreachable');", - true)); - EXPECT_EQ(count, 1); - deno_delete(d); -} - -TEST(LibDenoTest, BuiltinModules) { - static int count = 0; - auto resolve_cb = [](void* user_data, const char* specifier, - const char* referrer) { - EXPECT_STREQ(specifier, "b.js"); - EXPECT_STREQ(referrer, "c.js"); - count++; - auto d = reinterpret_cast(user_data); - deno_resolve_ok(d, "b.js", mod_b); - }; - Deno* d = deno_new(deno_config{0, empty, empty, nullptr, resolve_cb}); - EXPECT_TRUE(deno_execute( - d, d, "setup.js", "libdeno.builtinModules['deno'] = { foo: 'bar' }; \n")); - EXPECT_EQ(count, 0); - EXPECT_TRUE( - deno_execute_mod(d, d, "c.js", - "import { retb } from 'b.js'\n" - "import * as deno from 'deno'\n" - "if (retb() != 'b') throw Error('retb');\n" - // " libdeno.print('deno ' + JSON.stringify(deno));\n" - "if (deno.foo != 'bar') throw Error('foo');\n", - false)); - EXPECT_EQ(count, 1); - deno_delete(d); -} diff --git a/libdeno/modules.cc b/libdeno/modules.cc new file mode 100644 index 0000000000..961686acaa --- /dev/null +++ b/libdeno/modules.cc @@ -0,0 +1,206 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +#include "exceptions.h" +#include "internal.h" + +using deno::DenoIsolate; +using deno::HandleException; +using v8::Boolean; +using v8::Context; +using v8::EscapableHandleScope; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::Module; +using v8::Object; +using v8::ScriptCompiler; +using v8::ScriptOrigin; +using v8::String; +using v8::Value; + +std::string BuiltinModuleSrc(Local context, Local specifier) { + auto* isolate = context->GetIsolate(); + DenoIsolate* d = DenoIsolate::FromIsolate(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::EscapableHandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context); + + v8::String::Utf8Value specifier_utf8val(isolate, specifier); + const char* specifier_cstr = *specifier_utf8val; + + auto builtin_modules = d->GetBuiltinModules(); + auto val = builtin_modules->Get(context, specifier).ToLocalChecked(); + CHECK(val->IsObject()); + auto obj = val->ToObject(isolate); + + // In order to export obj as a module, we must iterate over its properties + // and export them each individually. + // TODO(ry) Find a better way to do this. + std::string src = "let globalEval = eval\nlet g = globalEval('this');\n"; + auto names = obj->GetOwnPropertyNames(context).ToLocalChecked(); + for (uint32_t i = 0; i < names->Length(); i++) { + auto name = names->Get(context, i).ToLocalChecked(); + v8::String::Utf8Value name_utf8val(isolate, name); + const char* name_cstr = *name_utf8val; + // TODO(ry) use format string. + src.append("export const "); + src.append(name_cstr); + src.append(" = g.libdeno.builtinModules."); + src.append(specifier_cstr); + src.append("."); + src.append(name_cstr); + src.append(";\n"); + } + return src; +} + +v8::MaybeLocal ResolveCallback(Local context, + Local specifier, + Local referrer) { + auto* isolate = context->GetIsolate(); + v8::Isolate::Scope isolate_scope(isolate); + v8::Locker locker(isolate); + + DenoIsolate* d = DenoIsolate::FromIsolate(isolate); + + v8::EscapableHandleScope handle_scope(isolate); + + auto builtin_modules = d->GetBuiltinModules(); + + deno_mod referrer_id = referrer->GetIdentityHash(); + auto* referrer_info = d->GetModuleInfo(referrer_id); + CHECK_NE(referrer_info, nullptr); + + for (int i = 0; i < referrer->GetModuleRequestsLength(); i++) { + Local req = referrer->GetModuleRequest(i); + + if (req->Equals(context, specifier).ToChecked()) { + v8::String::Utf8Value req_utf8(isolate, req); + std::string req_str(*req_utf8); + + deno_mod id = 0; + { + bool has_builtin = builtin_modules->Has(context, specifier).ToChecked(); + if (has_builtin) { + auto it = d->mods_by_name_.find(req_str.c_str()); + if (it != d->mods_by_name_.end()) { + id = it->second; + } else { + std::string src = BuiltinModuleSrc(context, specifier); + id = d->RegisterModule(req_str.c_str(), src.c_str()); + } + } else { + id = d->resolve_cb_(d->user_data_, req_str.c_str(), referrer_id); + } + } + + // Note: id might be zero, in which case GetModuleInfo will return + // nullptr. + auto* info = d->GetModuleInfo(id); + if (info == nullptr) { + char buf[64 * 1024]; + snprintf(buf, sizeof(buf), "Cannot resolve module \"%s\" from \"%s\"", + req_str.c_str(), referrer_info->name.c_str()); + isolate->ThrowException(deno::v8_str(buf, true)); + break; + } else { + Local child_mod = info->handle.Get(isolate); + return handle_scope.Escape(child_mod); + } + } + } + + return v8::MaybeLocal(); // Error +} + +extern "C" { + +deno_mod deno_mod_new(Deno* d_, const char* name_cstr, + const char* source_cstr) { + auto* d = unwrap(d_); + return d->RegisterModule(name_cstr, source_cstr); +} + +const char* deno_mod_name(Deno* d_, deno_mod id) { + auto* d = unwrap(d_); + auto* info = d->GetModuleInfo(id); + return info->name.c_str(); +} + +size_t deno_mod_imports_len(Deno* d_, deno_mod id) { + auto* d = unwrap(d_); + auto* info = d->GetModuleInfo(id); + return info->import_specifiers.size(); +} + +const char* deno_mod_imports_get(Deno* d_, deno_mod id, size_t index) { + auto* d = unwrap(d_); + auto* info = d->GetModuleInfo(id); + if (info == nullptr || index >= info->import_specifiers.size()) { + return nullptr; + } else { + return info->import_specifiers[index].c_str(); + } +} + +void deno_mod_instantiate(Deno* d_, void* user_data, deno_mod id, + deno_resolve_cb cb) { + auto* d = unwrap(d_); + deno::UserDataScope user_data_scope(d, user_data); + + auto* isolate = d->isolate_; + v8::Isolate::Scope isolate_scope(isolate); + v8::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + auto context = d->context_.Get(d->isolate_); + v8::Context::Scope context_scope(context); + + v8::TryCatch try_catch(isolate); + { + CHECK_EQ(nullptr, d->resolve_cb_); + d->resolve_cb_ = cb; + { + auto* info = d->GetModuleInfo(id); + if (info == nullptr) { + return; + } + Local module = info->handle.Get(isolate); + if (module->GetStatus() == Module::kErrored) { + return; + } + auto maybe_ok = module->InstantiateModule(context, ResolveCallback); + CHECK(maybe_ok.IsJust() || try_catch.HasCaught()); + } + d->resolve_cb_ = nullptr; + } + + if (try_catch.HasCaught()) { + HandleException(context, try_catch.Exception()); + } +} + +void deno_mod_evaluate(Deno* d_, void* user_data, deno_mod id) { + auto* d = unwrap(d_); + deno::UserDataScope user_data_scope(d, user_data); + + auto* isolate = d->isolate_; + v8::Isolate::Scope isolate_scope(isolate); + v8::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + auto context = d->context_.Get(d->isolate_); + v8::Context::Scope context_scope(context); + + auto* info = d->GetModuleInfo(id); + Local module = info->handle.Get(isolate); + + CHECK_EQ(Module::kInstantiated, module->GetStatus()); + + auto maybe_result = module->Evaluate(context); + if (maybe_result.IsEmpty()) { + CHECK_EQ(Module::kErrored, module->GetStatus()); + HandleException(context, module->GetException()); + } +} + +} // extern "C" diff --git a/libdeno/modules_test.cc b/libdeno/modules_test.cc new file mode 100644 index 0000000000..f637ae0707 --- /dev/null +++ b/libdeno/modules_test.cc @@ -0,0 +1,268 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +#include "test.h" + +static int exec_count = 0; +void recv_cb(void* user_data, int req_id, deno_buf buf, deno_buf data_buf) { + // We use this to check that scripts have executed. + EXPECT_EQ(1u, buf.data_len); + EXPECT_EQ(buf.data_ptr[0], 4); + exec_count++; +} + +TEST(ModulesTest, Resolution) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + static deno_mod a = deno_mod_new(d, "a.js", + "import { b } from 'b.js'\n" + "if (b() != 'b') throw Error();\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + const char* b_src = "export function b() { return 'b' }"; + static deno_mod b = deno_mod_new(d, "b.js", b_src); + EXPECT_NE(b, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + EXPECT_EQ(0, exec_count); + + EXPECT_EQ(1u, deno_mod_imports_len(d, a)); + EXPECT_EQ(0u, deno_mod_imports_len(d, b)); + + EXPECT_STREQ("b.js", deno_mod_imports_get(d, a, 0)); + EXPECT_EQ(nullptr, deno_mod_imports_get(d, a, 1)); + EXPECT_EQ(nullptr, deno_mod_imports_get(d, b, 0)); + + static int resolve_count = 0; + auto resolve_cb = [](void* user_data, const char* specifier, + deno_mod referrer) { + EXPECT_EQ(referrer, a); + EXPECT_STREQ(specifier, "b.js"); + resolve_count++; + return b; + }; + + deno_mod_instantiate(d, d, b, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(0, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_instantiate(d, d, a, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_evaluate(d, d, a); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(1, exec_count); + + deno_delete(d); +} + +TEST(ModulesTest, BuiltinModules) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + deno_execute(d, d, "setup.js", + "libdeno.builtinModules['deno'] = { foo: 'bar' };"); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + static deno_mod a = + deno_mod_new(d, "a.js", + "import { b } from 'b.js'\n" + "import * as deno from 'deno'\n" + "if (b() != 'b') throw Error('b');\n" + "if (deno.foo != 'bar') throw Error('foo');\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + const char* b_src = "export function b() { return 'b' }"; + static deno_mod b = deno_mod_new(d, "b.js", b_src); + EXPECT_NE(b, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + EXPECT_EQ(0, exec_count); + + EXPECT_EQ(2u, deno_mod_imports_len(d, a)); + EXPECT_EQ(0u, deno_mod_imports_len(d, b)); + + EXPECT_STREQ("b.js", deno_mod_imports_get(d, a, 0)); + EXPECT_STREQ("deno", deno_mod_imports_get(d, a, 1)); + EXPECT_EQ(nullptr, deno_mod_imports_get(d, a, 2)); + EXPECT_EQ(nullptr, deno_mod_imports_get(d, b, 0)); + + static int resolve_count = 0; + auto resolve_cb = [](void* user_data, const char* specifier, + deno_mod referrer) { + EXPECT_EQ(referrer, a); + EXPECT_STREQ(specifier, "b.js"); + resolve_count++; + return b; + }; + + deno_mod_instantiate(d, d, b, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(0, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_instantiate(d, d, a, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_evaluate(d, d, a); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(1, exec_count); + + deno_delete(d); +} + +TEST(ModulesTest, BuiltinModules2) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + deno_execute(d, d, "setup.js", + "libdeno.builtinModules['builtin1'] = { foo: 'bar' }; \n" + "libdeno.builtinModules['builtin2'] = { hello: 'world' }; \n"); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + static deno_mod a = + deno_mod_new(d, "a.js", + "import * as b1 from 'builtin1'\n" + "import * as b2 from 'builtin2'\n" + "if (b1.foo != 'bar') throw Error('bad1');\n" + "if (b2.hello != 'world') throw Error('bad2');\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + EXPECT_EQ(2u, deno_mod_imports_len(d, a)); + EXPECT_STREQ("builtin1", deno_mod_imports_get(d, a, 0)); + EXPECT_STREQ("builtin2", deno_mod_imports_get(d, a, 1)); + + deno_mod_instantiate(d, d, a, nullptr); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(0, exec_count); + + deno_mod_evaluate(d, d, a); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, exec_count); + + deno_delete(d); +} + +TEST(ModulesTest, BuiltinModules3) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + deno_execute(d, d, "setup.js", + "libdeno.builtinModules['builtin'] = { foo: 'bar' };"); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + static deno_mod a = + deno_mod_new(d, "a.js", + "import * as b1 from 'builtin'\n" + "import * as b2 from 'b.js'\n" + "if (b1.foo != 'bar') throw Error('bad1');\n" + "if (b2.bar() != 'bar') throw Error('bad2');\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + EXPECT_EQ(2u, deno_mod_imports_len(d, a)); + EXPECT_STREQ("builtin", deno_mod_imports_get(d, a, 0)); + EXPECT_STREQ("b.js", deno_mod_imports_get(d, a, 1)); + + static deno_mod b = deno_mod_new(d, "b.js", + "import { foo } from 'builtin';\n" + "export function bar() { return foo }\n"); + EXPECT_NE(b, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + static int resolve_count = 0; + auto resolve_cb = [](void* user_data, const char* specifier, + deno_mod referrer) { + EXPECT_EQ(referrer, a); + EXPECT_STREQ(specifier, "b.js"); + resolve_count++; + return b; + }; + + deno_mod_instantiate(d, d, a, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_instantiate(d, d, b, resolve_cb); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_mod_evaluate(d, d, a); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, exec_count); + + deno_delete(d); +} + +TEST(ModulesTest, ResolutionError) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + static deno_mod a = deno_mod_new(d, "a.js", + "import 'bad'\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + EXPECT_EQ(0, exec_count); + + EXPECT_EQ(1u, deno_mod_imports_len(d, a)); + EXPECT_STREQ("bad", deno_mod_imports_get(d, a, 0)); + + static int resolve_count = 0; + auto resolve_cb = [](void* user_data, const char* specifier, + deno_mod referrer) { + EXPECT_EQ(referrer, a); + EXPECT_STREQ(specifier, "bad"); + resolve_count++; + return 0; + }; + + deno_mod_instantiate(d, d, a, resolve_cb); + EXPECT_NE(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, resolve_count); + EXPECT_EQ(0, exec_count); + + deno_delete(d); +} + +TEST(ModulesTest, ImportMetaUrl) { + exec_count = 0; // Reset + Deno* d = deno_new(deno_config{0, empty, empty, recv_cb}); + EXPECT_EQ(0, exec_count); + + static deno_mod a = + deno_mod_new(d, "a.js", + "if ('a.js' != import.meta.url) throw 'hmm'\n" + "libdeno.send(new Uint8Array([4]));"); + EXPECT_NE(a, 0); + EXPECT_EQ(nullptr, deno_last_exception(d)); + + deno_mod_instantiate(d, d, a, nullptr); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(0, exec_count); + + deno_mod_evaluate(d, d, a); + EXPECT_EQ(nullptr, deno_last_exception(d)); + EXPECT_EQ(1, exec_count); +} diff --git a/libdeno/snapshot_creator.cc b/libdeno/snapshot_creator.cc index 6a83f963a5..d38b6c075e 100644 --- a/libdeno/snapshot_creator.cc +++ b/libdeno/snapshot_creator.cc @@ -23,7 +23,7 @@ int main(int argc, char** argv) { CHECK(deno::ReadFileToString(js_fn, &js_source)); deno_init(); - deno_config config = {1, deno::empty_buf, deno::empty_buf, nullptr, nullptr}; + deno_config config = {1, deno::empty_buf, deno::empty_buf, nullptr}; Deno* d = deno_new(config); int r = deno_execute(d, nullptr, js_fn, js_source.c_str()); diff --git a/src/deno_dir.rs b/src/deno_dir.rs index 5c7e2f9e8d..6fbe90f680 100644 --- a/src/deno_dir.rs +++ b/src/deno_dir.rs @@ -355,9 +355,8 @@ impl DenoDir { } } - // Prototype: https://github.com/denoland/deno/blob/golang/os.go#L70-L98 - // Returns (module name, local filename) - fn resolve_module( + /// Returns (module name, local filename) + pub fn resolve_module( self: &Self, specifier: &str, referrer: &str, diff --git a/src/errors.rs b/src/errors.rs index 0855e0079d..74502acc50 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::js_errors::JSError; pub use crate::msg::ErrorKind; use crate::resolve_addr::ResolveAddrError; @@ -178,3 +179,20 @@ pub fn permission_denied() -> DenoError { String::from("permission denied"), ) } + +pub enum RustOrJsError { + Rust(DenoError), + Js(JSError), +} + +impl From for RustOrJsError { + fn from(e: DenoError) -> Self { + RustOrJsError::Rust(e) + } +} + +impl From for RustOrJsError { + fn from(e: JSError) -> Self { + RustOrJsError::Js(e) + } +} diff --git a/src/isolate.rs b/src/isolate.rs index 7d1fdf71d1..39136daff8 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -3,11 +3,14 @@ // TODO Currently this module uses Tokio, but it would be nice if they were // decoupled. +#![allow(dead_code)] + use crate::compiler::compile_sync; use crate::compiler::CodeFetchOutput; use crate::deno_dir; use crate::errors::DenoError; use crate::errors::DenoResult; +use crate::errors::RustOrJsError; use crate::flags; use crate::js_errors::JSError; use crate::libdeno; @@ -21,6 +24,7 @@ use libc::c_char; use libc::c_void; use std; use std::cell::Cell; +use std::collections::HashMap; use std::env; use std::ffi::CStr; use std::ffi::CString; @@ -48,6 +52,10 @@ pub type Dispatch = fn(isolate: &Isolate, buf: libdeno::deno_buf, data_buf: libdeno::deno_buf) -> (bool, Box); +pub struct ModuleInfo { + name: String, +} + pub struct Isolate { libdeno_isolate: *const libdeno::isolate, dispatch: Dispatch, @@ -55,6 +63,8 @@ pub struct Isolate { tx: mpsc::Sender<(i32, Buf)>, ntasks: Cell, timeout_due: Cell>, + pub modules: HashMap, + pub modules_by_name: HashMap, pub state: Arc, } @@ -155,6 +165,7 @@ pub struct Metrics { pub bytes_sent_control: AtomicUsize, pub bytes_sent_data: AtomicUsize, pub bytes_received: AtomicUsize, + pub resolve_count: AtomicUsize, } static DENO_INIT: Once = ONCE_INIT; @@ -173,7 +184,6 @@ impl Isolate { load_snapshot: snapshot, shared: libdeno::deno_buf::empty(), // TODO Use for message passing. recv_cb: pre_dispatch, - resolve_cb, }; let libdeno_isolate = unsafe { libdeno::deno_new(config) }; // This channel handles sending async messages back to the runtime. @@ -186,6 +196,8 @@ impl Isolate { tx, ntasks: Cell::new(0), timeout_due: Cell::new(None), + modules: HashMap::new(), + modules_by_name: HashMap::new(), state, } } @@ -254,34 +266,156 @@ impl Isolate { Ok(()) } + pub fn mod_new( + &mut self, + name: String, + source: String, + ) -> Result { + let name_ = CString::new(name.clone()).unwrap(); + let name_ptr = name_.as_ptr() as *const i8; + + let source_ = CString::new(source.clone()).unwrap(); + let source_ptr = source_.as_ptr() as *const i8; + + let id = unsafe { + libdeno::deno_mod_new(self.libdeno_isolate, name_ptr, source_ptr) + }; + if let Some(js_error) = self.last_exception() { + assert_eq!(id, 0); + return Err(js_error); + } + + let name2 = name.clone(); + self.modules.insert(id, ModuleInfo { name }); + + debug!("modules_by_name insert {}", name2); + self.modules_by_name.insert(name2, id); + + Ok(id) + } + + // TODO(ry) This should be private... + pub fn resolve_cb( + &self, + specifier: &str, + referrer: libdeno::deno_mod, + ) -> libdeno::deno_mod { + self + .state + .metrics + .resolve_count + .fetch_add(1, Ordering::Relaxed); + + debug!("resolve_cb {}", specifier); + + let r = self.modules.get(&referrer); + if r.is_none() { + debug!("cant find referrer {}", referrer); + return 0; + } + let referrer_name = &r.unwrap().name; + let r = self.state.dir.resolve_module(specifier, referrer_name); + if let Err(err) = r { + debug!("potentially swallowed err: {}", err); + return 0; + } + let (name, _local_filename) = r.unwrap(); + + if let Some(id) = self.modules_by_name.get(&name) { + return *id; + } else { + return 0; + } + } + + // TODO(ry) make this return a future. + pub fn mod_load_deps( + &mut self, + id: libdeno::deno_mod, + ) -> Result<(), RustOrJsError> { + // basically iterate over the imports, start loading them. + + let referrer = self.modules.get(&id).unwrap().clone(); + let referrer_name = referrer.name.clone(); + let len = + unsafe { libdeno::deno_mod_imports_len(self.libdeno_isolate, id) }; + + for i in 0..len { + let specifier_ptr = + unsafe { libdeno::deno_mod_imports_get(self.libdeno_isolate, id, i) }; + let specifier_c: &CStr = unsafe { CStr::from_ptr(specifier_ptr) }; + let specifier: &str = specifier_c.to_str().unwrap(); + + // TODO(ry) This shouldn't be necessary here. builtin modules should be + // taken care of at the libdeno level. + if specifier == "deno" { + continue; + } + + let (name, _local_filename) = self + .state + .dir + .resolve_module(specifier, &referrer_name) + .map_err(DenoError::from) + .map_err(RustOrJsError::from)?; + + debug!("mod_load_deps {} {}", i, name); + + if None == self.modules_by_name.get(&name) { + let out = + code_fetch_and_maybe_compile(&self.state, specifier, &referrer_name)?; + let child_id = + self.mod_new(out.module_name.clone(), out.js_source())?; + + self.mod_load_deps(child_id)?; + } + } + + Ok(()) + } + + pub fn mod_instantiate(&self, id: libdeno::deno_mod) -> Result<(), JSError> { + unsafe { + libdeno::deno_mod_instantiate( + self.libdeno_isolate, + self.as_raw_ptr(), + id, + resolve_cb, + ) + }; + if let Some(js_error) = self.last_exception() { + return Err(js_error); + } + + Ok(()) + } + + pub fn mod_evaluate(&self, id: libdeno::deno_mod) -> Result<(), JSError> { + unsafe { + libdeno::deno_mod_evaluate(self.libdeno_isolate, self.as_raw_ptr(), id) + }; + if let Some(js_error) = self.last_exception() { + return Err(js_error); + } + Ok(()) + } + /// Executes the provided JavaScript module. pub fn execute_mod( - &self, + &mut self, js_filename: &str, is_prefetch: bool, ) -> Result<(), JSError> { let out = code_fetch_and_maybe_compile(&self.state, js_filename, ".").unwrap(); - let filename = CString::new(out.filename.clone()).unwrap(); - let filename_ptr = filename.as_ptr() as *const i8; + let id = self.mod_new(out.filename.clone(), out.js_source())?; - let js_source = CString::new(out.js_source().clone()).unwrap(); - let js_source = CString::new(js_source).unwrap(); - let js_source_ptr = js_source.as_ptr() as *const i8; + self.mod_load_deps(id).ok(); - let r = unsafe { - libdeno::deno_execute_mod( - self.libdeno_isolate, - self.as_raw_ptr(), - filename_ptr, - js_source_ptr, - if is_prefetch { 1 } else { 0 }, - ) - }; - if r == 0 { - let js_error = self.last_exception().unwrap(); - return Err(js_error); + self.mod_instantiate(id)?; + if !is_prefetch { + self.mod_evaluate(id)?; } Ok(()) } @@ -393,40 +527,12 @@ fn code_fetch_and_maybe_compile( extern "C" fn resolve_cb( user_data: *mut c_void, specifier_ptr: *const c_char, - referrer_ptr: *const c_char, -) { + referrer: libdeno::deno_mod, +) -> libdeno::deno_mod { + let isolate = unsafe { Isolate::from_raw_ptr(user_data) }; let specifier_c: &CStr = unsafe { CStr::from_ptr(specifier_ptr) }; let specifier: &str = specifier_c.to_str().unwrap(); - - let referrer_c: &CStr = unsafe { CStr::from_ptr(referrer_ptr) }; - let referrer: &str = referrer_c.to_str().unwrap(); - - debug!("module_resolve callback {} {}", specifier, referrer); - let isolate = unsafe { Isolate::from_raw_ptr(user_data) }; - - let maybe_out = - code_fetch_and_maybe_compile(&isolate.state, specifier, referrer); - - if maybe_out.is_err() { - // Resolution failure - return; - } - - let out = maybe_out.unwrap(); - - let filename = CString::new(out.filename.clone()).unwrap(); - let filename_ptr = filename.as_ptr() as *const i8; - - let js_source = CString::new(out.js_source().clone()).unwrap(); - let js_source_ptr = js_source.as_ptr() as *const i8; - - unsafe { - libdeno::deno_resolve_ok( - isolate.libdeno_isolate, - filename_ptr, - js_source_ptr, - ) - }; + return isolate.resolve_cb(specifier, referrer); } // Dereferences the C pointer into the Rust Isolate object. @@ -673,12 +779,37 @@ mod tests { let state = Arc::new(IsolateState::new(flags, rest_argv, None)); let snapshot = libdeno::deno_buf::empty(); - let isolate = Isolate::new(snapshot, state, dispatch_sync); + let mut isolate = Isolate::new(snapshot, state, dispatch_sync); tokio_util::init(|| { isolate .execute_mod(filename, false) .expect("execute_mod error"); isolate.event_loop().ok(); }); + + let metrics = &isolate.state.metrics; + assert_eq!(metrics.resolve_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn execute_mod_circular() { + let filename = std::env::current_dir().unwrap().join("tests/circular1.js"); + let filename = filename.to_str().unwrap(); + + let argv = vec![String::from("./deno"), String::from(filename)]; + let (flags, rest_argv, _) = flags::set_flags(argv).unwrap(); + + let state = Arc::new(IsolateState::new(flags, rest_argv, None)); + let snapshot = libdeno::deno_buf::empty(); + let mut isolate = Isolate::new(snapshot, state, dispatch_sync); + tokio_util::init(|| { + isolate + .execute_mod(filename, false) + .expect("execute_mod error"); + isolate.event_loop().ok(); + }); + + let metrics = &isolate.state.metrics; + assert_eq!(metrics.resolve_count.load(Ordering::SeqCst), 2); } } diff --git a/src/libdeno.rs b/src/libdeno.rs index f8f8a61e0a..ac1655bb3f 100644 --- a/src/libdeno.rs +++ b/src/libdeno.rs @@ -2,6 +2,7 @@ use libc::c_char; use libc::c_int; use libc::c_void; +use libc::size_t; use std::ops::{Deref, DerefMut}; use std::ptr::null; @@ -110,12 +111,15 @@ type deno_recv_cb = unsafe extern "C" fn( data_buf: deno_buf, ); +#[allow(non_camel_case_types)] +pub type deno_mod = i32; + #[allow(non_camel_case_types)] type deno_resolve_cb = unsafe extern "C" fn( user_data: *mut c_void, specifier: *const c_char, - referrer: *const c_char, -); + referrer: deno_mod, +) -> deno_mod; #[repr(C)] pub struct deno_config { @@ -123,7 +127,6 @@ pub struct deno_config { pub load_snapshot: deno_buf, pub shared: deno_buf, pub recv_cb: deno_recv_cb, - pub resolve_cb: deno_resolve_cb, } extern "C" { @@ -146,16 +149,34 @@ extern "C" { js_filename: *const c_char, js_source: *const c_char, ) -> c_int; - pub fn deno_execute_mod( + + // Modules + + pub fn deno_mod_new( + i: *const isolate, + name: *const c_char, + source: *const c_char, + ) -> deno_mod; + + pub fn deno_mod_imports_len(i: *const isolate, id: deno_mod) -> size_t; + + pub fn deno_mod_imports_get( + i: *const isolate, + id: deno_mod, + index: size_t, + ) -> *const c_char; + + pub fn deno_mod_instantiate( i: *const isolate, user_data: *const c_void, - js_filename: *const c_char, - js_source: *const c_char, - resolve_only: i32, - ) -> c_int; - pub fn deno_resolve_ok( - i: *const isolate, - js_filename: *const c_char, - js_source: *const c_char, + id: deno_mod, + resolve_cb: deno_resolve_cb, ); + + pub fn deno_mod_evaluate( + i: *const isolate, + user_data: *const c_void, + id: deno_mod, + ); + } diff --git a/src/main.rs b/src/main.rs index 89c9563e16..1b93ea86d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,7 +84,7 @@ fn main() { let state = Arc::new(isolate::IsolateState::new(flags, rest_argv, None)); let snapshot = snapshot::deno_snapshot(); - let isolate = isolate::Isolate::new(snapshot, state, ops::dispatch); + let mut isolate = isolate::Isolate::new(snapshot, state, ops::dispatch); tokio_util::init(|| { // Setup runtime. @@ -94,9 +94,9 @@ fn main() { // Execute input file. if isolate.state.argv.len() > 1 { - let input_filename = &isolate.state.argv[1]; + let input_filename = isolate.state.argv[1].clone(); isolate - .execute_mod(input_filename, should_prefetch) + .execute_mod(&input_filename, should_prefetch) .unwrap_or_else(print_err_and_exit); } diff --git a/tests/circular1.js b/tests/circular1.js new file mode 100644 index 0000000000..b166f7e5d3 --- /dev/null +++ b/tests/circular1.js @@ -0,0 +1,2 @@ +import "circular2.js"; +console.log("circular1"); diff --git a/tests/circular1.js.out b/tests/circular1.js.out new file mode 100644 index 0000000000..21f7fd5857 --- /dev/null +++ b/tests/circular1.js.out @@ -0,0 +1,2 @@ +circular2 +circular1 diff --git a/tests/circular1.test b/tests/circular1.test new file mode 100644 index 0000000000..d86a00c31e --- /dev/null +++ b/tests/circular1.test @@ -0,0 +1,2 @@ +args: tests/circular1.js --reload +output: tests/circular1.js.out diff --git a/tests/circular2.js b/tests/circular2.js new file mode 100644 index 0000000000..3d3136a0d0 --- /dev/null +++ b/tests/circular2.js @@ -0,0 +1,2 @@ +import "circular1.js"; +console.log("circular2"); diff --git a/tests/error_009_missing_js_module.js.out b/tests/error_009_missing_js_module.js.out index e0d8cce2ed..ca41435faa 100644 --- a/tests/error_009_missing_js_module.js.out +++ b/tests/error_009_missing_js_module.js.out @@ -1 +1 @@ -Uncaught NotFound: Cannot resolve module "./bad-module.js" from "[WILDCARD]/tests/error_009_missing_js_module.js" +Uncaught Cannot resolve module "./bad-module.js" from "[WILDCARD]error_009_missing_js_module.js" diff --git a/tests/imports_meta.js b/tests/imports_meta.js new file mode 100644 index 0000000000..3361d12377 --- /dev/null +++ b/tests/imports_meta.js @@ -0,0 +1,3 @@ +console.log("imports_meta", import.meta.url); + +import "imports_meta2.js"; diff --git a/tests/imports_meta.js.out b/tests/imports_meta.js.out new file mode 100644 index 0000000000..ec6e7eaecf --- /dev/null +++ b/tests/imports_meta.js.out @@ -0,0 +1,2 @@ +imports_meta2 [WILDCARD]imports_meta2.js +imports_meta [WILDCARD]imports_meta.js diff --git a/tests/imports_meta.test b/tests/imports_meta.test new file mode 100644 index 0000000000..17591ea33e --- /dev/null +++ b/tests/imports_meta.test @@ -0,0 +1,2 @@ +args: tests/imports_meta.js --reload +output: tests/imports_meta.js.out diff --git a/tests/imports_meta2.js b/tests/imports_meta2.js new file mode 100644 index 0000000000..583861e126 --- /dev/null +++ b/tests/imports_meta2.js @@ -0,0 +1 @@ +console.log("imports_meta2", import.meta.url);