Skip to content

Commit

Permalink
src: add embedding helpers to reduce boilerplate code
Browse files Browse the repository at this point in the history
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: nodejs#35597
Reviewed-By: Colin Ihrig <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Denys Otrishko <[email protected]>
  • Loading branch information
addaleax authored and joesepi committed Oct 22, 2020
1 parent 8d03adb commit 25e069f
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 217 deletions.
105 changes: 21 additions & 84 deletions doc/api/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ int main(int argc, char** argv) {
```
### Per-instance state
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35597
description:
The `CommonEnvironmentSetup` and `SpinEventLoop` utilities were added.
-->
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:
Expand Down Expand Up @@ -99,52 +106,26 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
const std::vector<std::string>& args,
const std::vector<std::string>& 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<ArrayBufferAllocator> 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<std::string> errors;
std::unique_ptr<CommonEnvironmentSetup> 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<IsolateData, decltype(&node::FreeIsolateData)> isolate_data(
node::CreateIsolateData(isolate, &loop, platform, allocator.get()),
node::FreeIsolateData);
// Set up a new v8::Context.
HandleScope handle_scope(isolate);
Local<Context> 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<Environment, decltype(&node::FreeEnvironment)> 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
Expand All @@ -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<Value> loadenv_ret = node::LoadEnvironment(
env.get(),
env,
"const publicRequire ="
" require('module').createRequire(process.cwd() + '/');"
"globalThis.require = publicRequire;"
Expand All @@ -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<bool*>(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;
}
```
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,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',
Expand Down
169 changes: 169 additions & 0 deletions src/api/embed_helpers.cc
Original file line number Diff line number Diff line change
@@ -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<int> 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<int>();

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<int>();

env->set_trace_sync_io(false);
env->VerifyNoStrongBaseObjects();
return EmitProcessExit(env);
}

struct CommonEnvironmentSetup::Impl {
MultiIsolatePlatform* platform = nullptr;
uv_loop_t loop;
std::shared_ptr<ArrayBufferAllocator> allocator;
Isolate* isolate = nullptr;
DeleteFnPtr<IsolateData, FreeIsolateData> isolate_data;
DeleteFnPtr<Environment, FreeEnvironment> env;
Global<Context> context;
};

CommonEnvironmentSetup::CommonEnvironmentSetup(
MultiIsolatePlatform* platform,
std::vector<std::string>* errors,
std::function<Environment*(const CommonEnvironmentSetup*)> 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> 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<bool*>(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<ArrayBufferAllocator>
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<v8::Context> CommonEnvironmentSetup::context() const {
return impl_->context.Get(impl_->isolate);
}

} // namespace node
67 changes: 67 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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 <typename... EnvironmentArgs>
static std::unique_ptr<CommonEnvironmentSetup> Create(
MultiIsolatePlatform* platform,
std::vector<std::string>* errors,
EnvironmentArgs&&... env_args);

struct uv_loop_s* event_loop() const;
std::shared_ptr<ArrayBufferAllocator> array_buffer_allocator() const;
v8::Isolate* isolate() const;
IsolateData* isolate_data() const;
Environment* env() const;
v8::Local<v8::Context> 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::string>*,
std::function<Environment*(const CommonEnvironmentSetup*)>);
};

// Implementation for CommonEnvironmentSetup::Create
template <typename... EnvironmentArgs>
std::unique_ptr<CommonEnvironmentSetup> CommonEnvironmentSetup::Create(
MultiIsolatePlatform* platform,
std::vector<std::string>* errors,
EnvironmentArgs&&... env_args) {
auto ret = std::unique_ptr<CommonEnvironmentSetup>(new CommonEnvironmentSetup(
platform, errors,
[&](const CommonEnvironmentSetup* setup) -> Environment* {
return CreateEnvironment(
setup->isolate_data(), setup->context(),
std::forward<EnvironmentArgs>(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<v8::Value> NODE_UNIXTIME_V8(double time) {
Expand Down
Loading

0 comments on commit 25e069f

Please sign in to comment.