From 9fd6122659c4067b0d8bd2c590f4ba01b48c93a3 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 11 Oct 2020 15:13:19 +0200 Subject: [PATCH] src: add embedding helpers to reduce boilerplate code Provide helpers for a) spinning the event loop and b) setting up and tearing down the objects involved in a single Node.js instance, as they would typically be used. The former helper is also usable inside Node.js itself, for both Worker and main threads. PR-URL: https://github.com/nodejs/node/pull/35597 Reviewed-By: Colin Ihrig Reviewed-By: James M Snell Reviewed-By: Denys Otrishko --- doc/api/embedding.md | 105 +++++----------------- node.gyp | 1 + src/api/embed_helpers.cc | 169 ++++++++++++++++++++++++++++++++++++ src/node.h | 67 ++++++++++++++ src/node_main_instance.cc | 33 +------ src/node_worker.cc | 40 ++------- test/embedding/embedtest.cc | 80 +++-------------- 7 files changed, 278 insertions(+), 217 deletions(-) create mode 100644 src/api/embed_helpers.cc diff --git a/doc/api/embedding.md b/doc/api/embedding.md index f38d5a7cabc8ea..82c87f5890f181 100644 --- a/doc/api/embedding.md +++ b/doc/api/embedding.md @@ -67,6 +67,13 @@ int main(int argc, char** argv) { ``` ### Per-instance state + Node.js has a concept of a “Node.js instance”, that is commonly being referred to as `node::Environment`. Each `node::Environment` is associated with: @@ -99,52 +106,26 @@ int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& args, const std::vector& exec_args) { int exit_code = 0; - // Set up a libuv event loop. - uv_loop_t loop; - int ret = uv_loop_init(&loop); - if (ret != 0) { - fprintf(stderr, "%s: Failed to initialize loop: %s\n", - args[0].c_str(), - uv_err_name(ret)); - return 1; - } - std::shared_ptr allocator = - ArrayBufferAllocator::Create(); - - Isolate* isolate = NewIsolate(allocator, &loop, platform); - if (isolate == nullptr) { - fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str()); + // Setup up a libuv event loop, v8::Isolate, and Node.js Environment. + std::vector errors; + std::unique_ptr setup = + CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); + if (!setup) { + for (const std::string& err : errors) + fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); return 1; } + Isolate* isolate = setup->isolate(); + Environment* env = setup->env(); + { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); - - // Create a node::IsolateData instance that will later be released using - // node::FreeIsolateData(). - std::unique_ptr isolate_data( - node::CreateIsolateData(isolate, &loop, platform, allocator.get()), - node::FreeIsolateData); - - // Set up a new v8::Context. - HandleScope handle_scope(isolate); - Local context = node::NewContext(isolate); - if (context.IsEmpty()) { - fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str()); - return 1; - } - // The v8::Context needs to be entered when node::CreateEnvironment() and // node::LoadEnvironment() are being called. - Context::Scope context_scope(context); - - // Create a node::Environment instance that will later be released using - // node::FreeEnvironment(). - std::unique_ptr env( - node::CreateEnvironment(isolate_data.get(), context, args, exec_args), - node::FreeEnvironment); + Context::Scope context_scope(setup->context()); // Set up the Node.js instance for execution, and run code inside of it. // There is also a variant that takes a callback and provides it with @@ -156,7 +137,7 @@ int RunNodeInstance(MultiIsolatePlatform* platform, // load files from the disk, and uses the standard CommonJS file loader // instead of the internal-only `require` function. MaybeLocal loadenv_ret = node::LoadEnvironment( - env.get(), + env, "const publicRequire =" " require('module').createRequire(process.cwd() + '/');" "globalThis.require = publicRequire;" @@ -165,58 +146,14 @@ int RunNodeInstance(MultiIsolatePlatform* platform, if (loadenv_ret.IsEmpty()) // There has been a JS exception. return 1; - { - // SealHandleScope protects against handle leaks from callbacks. - SealHandleScope seal(isolate); - bool more; - do { - uv_run(&loop, UV_RUN_DEFAULT); - - // V8 tasks on background threads may end up scheduling new tasks in the - // foreground, which in turn can keep the event loop going. For example, - // WebAssembly.compile() may do so. - platform->DrainTasks(isolate); - - // If there are new tasks, continue. - more = uv_loop_alive(&loop); - if (more) continue; - - // node::EmitProcessBeforeExit() is used to emit the 'beforeExit' event - // on the `process` object. - if (node::EmitProcessBeforeExit(env.get()).IsNothing()) - break; - - // 'beforeExit' can also schedule new work that keeps the event loop - // running. - more = uv_loop_alive(&loop); - } while (more == true); - } - - // node::EmitProcessExit() returns the current exit code. - exit_code = node::EmitProcessExit(env.get()).FromMaybe(1); + exit_code = node::SpinEventLoop(env).FromMaybe(1); // node::Stop() can be used to explicitly stop the event loop and keep // further JavaScript from running. It can be called from any thread, // and will act like worker.terminate() if called from another thread. - node::Stop(env.get()); + node::Stop(env); } - // Unregister the Isolate with the platform and add a listener that is called - // when the Platform is done cleaning up any state it had associated with - // the Isolate. - bool platform_finished = false; - platform->AddIsolateFinishedCallback(isolate, [](void* data) { - *static_cast(data) = true; - }, &platform_finished); - platform->UnregisterIsolate(isolate); - isolate->Dispose(); - - // Wait until the platform has cleaned up all relevant resources. - while (!platform_finished) - uv_run(&loop, UV_RUN_ONCE); - int err = uv_loop_close(&loop); - assert(err == 0); - return exit_code; } ``` diff --git a/node.gyp b/node.gyp index 264a2cb408ed8b..a96162d10790e0 100644 --- a/node.gyp +++ b/node.gyp @@ -575,6 +575,7 @@ 'sources': [ 'src/api/async_resource.cc', 'src/api/callback.cc', + 'src/api/embed_helpers.cc', 'src/api/encoding.cc', 'src/api/environment.cc', 'src/api/exceptions.cc', diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc new file mode 100644 index 00000000000000..998d7507fc0eba --- /dev/null +++ b/src/api/embed_helpers.cc @@ -0,0 +1,169 @@ +#include "node.h" +#include "env-inl.h" +#include "debug_utils-inl.h" + +using v8::Context; +using v8::Global; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::Maybe; +using v8::Nothing; +using v8::SealHandleScope; + +namespace node { + +Maybe SpinEventLoop(Environment* env) { + CHECK_NOT_NULL(env); + MultiIsolatePlatform* platform = GetMultiIsolatePlatform(env); + CHECK_NOT_NULL(platform); + + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + SealHandleScope seal(env->isolate()); + + if (env->is_stopping()) return Nothing(); + + env->set_trace_sync_io(env->options()->trace_sync_io); + { + bool more; + env->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); + do { + if (env->is_stopping()) break; + uv_run(env->event_loop(), UV_RUN_DEFAULT); + if (env->is_stopping()) break; + + platform->DrainTasks(env->isolate()); + + more = uv_loop_alive(env->event_loop()); + if (more && !env->is_stopping()) continue; + + if (EmitProcessBeforeExit(env).IsNothing()) + break; + + // Emit `beforeExit` if the loop became alive either after emitting + // event, or after running some callbacks. + more = uv_loop_alive(env->event_loop()); + } while (more == true && !env->is_stopping()); + env->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); + } + if (env->is_stopping()) return Nothing(); + + env->set_trace_sync_io(false); + env->VerifyNoStrongBaseObjects(); + return EmitProcessExit(env); +} + +struct CommonEnvironmentSetup::Impl { + MultiIsolatePlatform* platform = nullptr; + uv_loop_t loop; + std::shared_ptr allocator; + Isolate* isolate = nullptr; + DeleteFnPtr isolate_data; + DeleteFnPtr env; + Global context; +}; + +CommonEnvironmentSetup::CommonEnvironmentSetup( + MultiIsolatePlatform* platform, + std::vector* errors, + std::function make_env) + : impl_(new Impl()) { + CHECK_NOT_NULL(platform); + CHECK_NOT_NULL(errors); + + impl_->platform = platform; + uv_loop_t* loop = &impl_->loop; + // Use `data` to tell the destructor whether the loop was initialized or not. + loop->data = nullptr; + int ret = uv_loop_init(loop); + if (ret != 0) { + errors->push_back( + SPrintF("Failed to initialize loop: %s", uv_err_name(ret))); + return; + } + loop->data = this; + + impl_->allocator = ArrayBufferAllocator::Create(); + impl_->isolate = NewIsolate(impl_->allocator, &impl_->loop, platform); + Isolate* isolate = impl_->isolate; + + { + Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + impl_->isolate_data.reset(CreateIsolateData( + isolate, loop, platform, impl_->allocator.get())); + + HandleScope handle_scope(isolate); + Local context = NewContext(isolate); + impl_->context.Reset(isolate, context); + if (context.IsEmpty()) { + errors->push_back("Failed to initialize V8 Context"); + return; + } + + Context::Scope context_scope(context); + impl_->env.reset(make_env(this)); + } +} + +CommonEnvironmentSetup::~CommonEnvironmentSetup() { + if (impl_->isolate != nullptr) { + Isolate* isolate = impl_->isolate; + { + Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + + impl_->context.Reset(); + impl_->env.reset(); + impl_->isolate_data.reset(); + } + + bool platform_finished = false; + impl_->platform->AddIsolateFinishedCallback(isolate, [](void* data) { + *static_cast(data) = true; + }, &platform_finished); + impl_->platform->UnregisterIsolate(isolate); + isolate->Dispose(); + + // Wait until the platform has cleaned up all relevant resources. + while (!platform_finished) + uv_run(&impl_->loop, UV_RUN_ONCE); + } + + if (impl_->isolate || impl_->loop.data != nullptr) + CheckedUvLoopClose(&impl_->loop); + + delete impl_; +} + + +uv_loop_t* CommonEnvironmentSetup::event_loop() const { + return &impl_->loop; +} + +std::shared_ptr +CommonEnvironmentSetup::array_buffer_allocator() const { + return impl_->allocator; +} + +Isolate* CommonEnvironmentSetup::isolate() const { + return impl_->isolate; +} + +IsolateData* CommonEnvironmentSetup::isolate_data() const { + return impl_->isolate_data.get(); +} + +Environment* CommonEnvironmentSetup::env() const { + return impl_->env.get(); +} + +v8::Local CommonEnvironmentSetup::context() const { + return impl_->context.Get(impl_->isolate); +} + +} // namespace node diff --git a/src/node.h b/src/node.h index f09c979a4becb1..6f93fc89e1ff82 100644 --- a/src/node.h +++ b/src/node.h @@ -553,6 +553,73 @@ NODE_EXTERN void RunAtExit(Environment* env); // with a Node instance. NODE_EXTERN struct uv_loop_s* GetCurrentEventLoop(v8::Isolate* isolate); +// Runs the main loop for a given Environment. This roughly performs the +// following steps: +// 1. Call uv_run() on the event loop until it is drained. +// 2. Call platform->DrainTasks() on the associated platform/isolate. +// 3. If the event loop is alive again, go to Step 1. +// 4. Call EmitProcessBeforeExit(). +// 5. If the event loop is alive again, go to Step 1. +// 6. Call EmitProcessExit() and forward the return value. +// If at any point node::Stop() is called, the function will attempt to return +// as soon as possible, returning an empty `Maybe`. +// This function only works if `env` has an associated `MultiIsolatePlatform`. +NODE_EXTERN v8::Maybe SpinEventLoop(Environment* env); + +class NODE_EXTERN CommonEnvironmentSetup { + public: + ~CommonEnvironmentSetup(); + + // Create a new CommonEnvironmentSetup, that is, a group of objects that + // together form the typical setup for a single Node.js Environment instance. + // If any error occurs, `*errors` will be populated and the returned pointer + // will be empty. + // env_args will be passed through as arguments to CreateEnvironment(), after + // `isolate_data` and `context`. + template + static std::unique_ptr Create( + MultiIsolatePlatform* platform, + std::vector* errors, + EnvironmentArgs&&... env_args); + + struct uv_loop_s* event_loop() const; + std::shared_ptr array_buffer_allocator() const; + v8::Isolate* isolate() const; + IsolateData* isolate_data() const; + Environment* env() const; + v8::Local context() const; + + CommonEnvironmentSetup(const CommonEnvironmentSetup&) = delete; + CommonEnvironmentSetup& operator=(const CommonEnvironmentSetup&) = delete; + CommonEnvironmentSetup(CommonEnvironmentSetup&&) = delete; + CommonEnvironmentSetup& operator=(CommonEnvironmentSetup&&) = delete; + + private: + struct Impl; + Impl* impl_; + CommonEnvironmentSetup( + MultiIsolatePlatform*, + std::vector*, + std::function); +}; + +// Implementation for CommonEnvironmentSetup::Create +template +std::unique_ptr CommonEnvironmentSetup::Create( + MultiIsolatePlatform* platform, + std::vector* errors, + EnvironmentArgs&&... env_args) { + auto ret = std::unique_ptr(new CommonEnvironmentSetup( + platform, errors, + [&](const CommonEnvironmentSetup* setup) -> Environment* { + return CreateEnvironment( + setup->isolate_data(), setup->context(), + std::forward(env_args)...); + })); + if (!errors->empty()) ret.reset(); + return ret; +} + /* Converts a unixtime to V8 Date */ NODE_DEPRECATED("Use v8::Date::New() directly", inline v8::Local NODE_UNIXTIME_V8(double time) { diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index 597ce4c96e06f8..d406bf15444bf8 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -23,7 +23,6 @@ using v8::Isolate; using v8::Local; using v8::Locker; using v8::Object; -using v8::SealHandleScope; std::unique_ptr NodeMainInstance::registry_ = nullptr; @@ -140,37 +139,7 @@ int NodeMainInstance::Run(const EnvSerializeInfo* env_info) { if (exit_code == 0) { LoadEnvironment(env.get()); - env->set_trace_sync_io(env->options()->trace_sync_io); - - { - SealHandleScope seal(isolate_); - bool more; - env->performance_state()->Mark( - node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); - do { - uv_run(env->event_loop(), UV_RUN_DEFAULT); - - per_process::v8_platform.DrainVMTasks(isolate_); - - more = uv_loop_alive(env->event_loop()); - if (more && !env->is_stopping()) continue; - - if (!uv_loop_alive(env->event_loop())) { - if (EmitProcessBeforeExit(env.get()).IsNothing()) - break; - } - - // Emit `beforeExit` if the loop became alive either after emitting - // event, or after running some callbacks. - more = uv_loop_alive(env->event_loop()); - } while (more == true && !env->is_stopping()); - env->performance_state()->Mark( - node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); - } - - env->set_trace_sync_io(false); - if (!env->is_stopping()) env->VerifyNoStrongBaseObjects(); - exit_code = EmitProcessExit(env.get()).FromMaybe(1); + exit_code = SpinEventLoop(env.get()).FromMaybe(1); } ResetStdio(); diff --git a/src/node_worker.cc b/src/node_worker.cc index cdaeefb7897794..2006380cd411dc 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -27,6 +27,7 @@ using v8::Integer; using v8::Isolate; using v8::Local; using v8::Locker; +using v8::Maybe; using v8::MaybeLocal; using v8::Null; using v8::Number; @@ -332,45 +333,14 @@ void Worker::Run() { Debug(this, "Loaded environment for worker %llu", thread_id_.id); } - - if (is_stopped()) return; - { - SealHandleScope seal(isolate_); - bool more; - env_->performance_state()->Mark( - node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); - do { - if (is_stopped()) break; - uv_run(&data.loop_, UV_RUN_DEFAULT); - if (is_stopped()) break; - - platform_->DrainTasks(isolate_); - - more = uv_loop_alive(&data.loop_); - if (more && !is_stopped()) continue; - - if (EmitProcessBeforeExit(env_.get()).IsNothing()) - break; - - // Emit `beforeExit` if the loop became alive either after emitting - // event, or after running some callbacks. - more = uv_loop_alive(&data.loop_); - } while (more == true && !is_stopped()); - env_->performance_state()->Mark( - node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); - } } { - int exit_code; - bool stopped = is_stopped(); - if (!stopped) { - env_->VerifyNoStrongBaseObjects(); - exit_code = EmitProcessExit(env_.get()).FromMaybe(1); - } + Maybe exit_code = SpinEventLoop(env_.get()); Mutex::ScopedLock lock(mutex_); - if (exit_code_ == 0 && !stopped) - exit_code_ = exit_code; + if (exit_code_ == 0 && exit_code.IsJust()) { + exit_code_ = exit_code.FromJust(); + } Debug(this, "Exiting thread for worker %llu with exit code %d", thread_id_.id, exit_code_); diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index fece8924ad6471..b0cf9d5f9738b3 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -5,17 +5,14 @@ // Note: This file is being referred to from doc/api/embedding.md, and excerpts // from it are included in the documentation. Try to keep these in sync. -using node::ArrayBufferAllocator; +using node::CommonEnvironmentSetup; using node::Environment; -using node::IsolateData; using node::MultiIsolatePlatform; using v8::Context; using v8::HandleScope; using v8::Isolate; -using v8::Local; using v8::Locker; using v8::MaybeLocal; -using v8::SealHandleScope; using v8::V8; using v8::Value; @@ -51,46 +48,27 @@ int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& args, const std::vector& exec_args) { int exit_code = 0; - uv_loop_t loop; - int ret = uv_loop_init(&loop); - if (ret != 0) { - fprintf(stderr, "%s: Failed to initialize loop: %s\n", - args[0].c_str(), - uv_err_name(ret)); - return 1; - } - std::shared_ptr allocator = - ArrayBufferAllocator::Create(); - - Isolate* isolate = NewIsolate(allocator, &loop, platform); - if (isolate == nullptr) { - fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str()); + std::vector errors; + std::unique_ptr setup = + CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); + if (!setup) { + for (const std::string& err : errors) + fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); return 1; } + Isolate* isolate = setup->isolate(); + Environment* env = setup->env(); + { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); - - std::unique_ptr isolate_data( - node::CreateIsolateData(isolate, &loop, platform, allocator.get()), - node::FreeIsolateData); - HandleScope handle_scope(isolate); - Local context = node::NewContext(isolate); - if (context.IsEmpty()) { - fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str()); - return 1; - } - - Context::Scope context_scope(context); - std::unique_ptr env( - node::CreateEnvironment(isolate_data.get(), context, args, exec_args), - node::FreeEnvironment); + Context::Scope context_scope(setup->context()); MaybeLocal loadenv_ret = node::LoadEnvironment( - env.get(), + env, "const publicRequire =" " require('module').createRequire(process.cwd() + '/');" "globalThis.require = publicRequire;" @@ -100,40 +78,10 @@ int RunNodeInstance(MultiIsolatePlatform* platform, if (loadenv_ret.IsEmpty()) // There has been a JS exception. return 1; - { - SealHandleScope seal(isolate); - bool more; - do { - uv_run(&loop, UV_RUN_DEFAULT); - - platform->DrainTasks(isolate); - more = uv_loop_alive(&loop); - if (more) continue; - - if (node::EmitProcessBeforeExit(env.get()).IsNothing()) - break; + exit_code = node::SpinEventLoop(env).FromMaybe(1); - more = uv_loop_alive(&loop); - } while (more == true); - } - - exit_code = node::EmitProcessExit(env.get()).FromMaybe(1); - - node::Stop(env.get()); + node::Stop(env); } - bool platform_finished = false; - platform->AddIsolateFinishedCallback(isolate, [](void* data) { - *static_cast(data) = true; - }, &platform_finished); - platform->UnregisterIsolate(isolate); - isolate->Dispose(); - - // Wait until the platform has cleaned up all relevant resources. - while (!platform_finished) - uv_run(&loop, UV_RUN_ONCE); - int err = uv_loop_close(&loop); - assert(err == 0); - return exit_code; }