Skip to content

Commit

Permalink
add environment preload callback
Browse files Browse the repository at this point in the history
  • Loading branch information
vmoroz committed Sep 4, 2024
1 parent 0bb3b5a commit f05a795
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 21 deletions.
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,7 @@
'test/embedding/embedtest_modules_node_api.cc',
'test/embedding/embedtest_node_api.cc',
'test/embedding/embedtest_node_api.h',
'test/embedding/embedtest_preload_node_api.cc',
'test/embedding/embedtest_snapshot_node_api.cc',
],

Expand Down
106 changes: 87 additions & 19 deletions src/node_api_embedding.cc
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#include <algorithm>
#include <climits> // INT_MAX
#include <cmath>
#define NAPI_EXPERIMENTAL
#include "node_api_embedding.h"

#include "env-inl.h"
#include "js_native_api.h"
#include "js_native_api_v8.h"
#include "node_api_embedding.h"
#include "node_api_internals.h"
#include "simdutf.h"
#include "util-inl.h"

#include <algorithm>
#include <climits> // INT_MAX
#include <cmath>
#include <mutex>

namespace node {

// Declare functions implemented in embed_helpers.cc
Expand All @@ -22,33 +23,43 @@ v8::Maybe<ExitCode> SpinEventLoopWithoutCleanup(
namespace v8impl {
namespace {

// A helper class to convert std::vector<std::string> to an array of C strings.
// If the number of strings is less than kInplaceBufferSize, the strings are
// stored in the inplace_buffer_ array. Otherwise, the strings are stored in the
// allocated_buffer_ array.
// Ideally the class must be allocated on the stack.
// In any case it must not outlive the passed vector since it keeps only the
// string pointers returned by std::stirng::c_str() method.
class CStringArray {
static constexpr size_t kInplaceBufferSize = 32;

public:
explicit CStringArray(const std::vector<std::string>& strings) noexcept
: size_(strings.size()) {
if (size_ < inplace_buffer_.size()) {
cstrings_ = inplace_buffer_.data();
if (size_ <= inplace_buffer_.size()) {
c_strs_ = inplace_buffer_.data();
} else {
allocated_buffer_ = std::make_unique<const char*[]>(size_);
cstrings_ = allocated_buffer_.get();
c_strs_ = allocated_buffer_.get();
}
for (size_t i = 0; i < size_; ++i) {
cstrings_[i] = strings[i].c_str();
c_strs_[i] = strings[i].c_str();
}
}

CStringArray() = delete;
CStringArray(const CStringArray&) = delete;
CStringArray& operator=(const CStringArray&) = delete;

const char** c_strs() const { return c_strs_; }
size_t size() const { return size_; }

const char** argv() const { return c_strs_; }
int32_t argc() const { return static_cast<int>(size_); }
const char** argv() const { return cstrings_; }

private:
const char** cstrings_;
size_t size_;
std::array<const char*, 32> inplace_buffer_;
const char** c_strs_{};
size_t size_{};
std::array<const char*, kInplaceBufferSize> inplace_buffer_;
std::unique_ptr<const char*[]> allocated_buffer_;
};

Expand Down Expand Up @@ -124,6 +135,7 @@ struct EmbeddedEnvironmentOptions {
node_api_env_flags flags_{node_api_env_default_flags};
std::vector<std::string> args_;
std::vector<std::string> exec_args_;
node::EmbedderPreloadCallback preload_cb_{};
node::EmbedderSnapshotData::Pointer snapshot_;
std::function<void(const node::EmbedderSnapshotData*)> create_snapshot_;
node::SnapshotConfig snapshot_config_{};
Expand Down Expand Up @@ -167,6 +179,22 @@ class EmbeddedEnvironment final : public node_napi_env__ {
env_options_(std::move(env_options)),
env_setup_(std::move(env_setup)) {
env_options_->is_frozen_ = true;

std::scoped_lock<std::mutex> lock(shared_mutex_);
node_env_to_node_api_env_.emplace(env_setup_->env(), this);
}

static node_napi_env GetOrCreateNodeApiEnv(node::Environment* node_env) {
std::scoped_lock<std::mutex> lock(shared_mutex_);
auto it = node_env_to_node_api_env_.find(node_env);
if (it != node_env_to_node_api_env_.end()) return it->second;
// TODO: (vmoroz) propagate API version from the root environment.
node_napi_env env = new node_napi_env__(
node_env->context(), "<worker_thread>", NAPI_VERSION_EXPERIMENTAL);
node_env->AddCleanupHook(
[](void* arg) { static_cast<node_napi_env>(arg)->Unref(); }, env);
node_env_to_node_api_env_.try_emplace(node_env, env);
return env;
}

static EmbeddedEnvironment* FromNapiEnv(napi_env env) {
Expand Down Expand Up @@ -198,6 +226,8 @@ class EmbeddedEnvironment final : public node_napi_env__ {

bool IsScopeOpened() const { return isolate_locker_.has_value(); }

const EmbeddedEnvironmentOptions& options() const { return *env_options_; }

const node::EmbedderSnapshotData::Pointer& snapshot() const {
return env_options_->snapshot_;
}
Expand All @@ -211,8 +241,16 @@ class EmbeddedEnvironment final : public node_napi_env__ {
std::unique_ptr<EmbeddedEnvironmentOptions> env_options_;
std::unique_ptr<node::CommonEnvironmentSetup> env_setup_;
std::optional<IsolateLocker> isolate_locker_;

static std::mutex shared_mutex_;
static std::unordered_map<node::Environment*, node_napi_env>
node_env_to_node_api_env_;
};

std::mutex EmbeddedEnvironment::shared_mutex_{};
std::unordered_map<node::Environment*, node_napi_env>
EmbeddedEnvironment::node_env_to_node_api_env_{};

node::ProcessInitializationFlags::Flags GetProcessInitializationFlags(
node_api_platform_flags flags) {
uint32_t result = node::ProcessInitializationFlags::kNoFlags;
Expand Down Expand Up @@ -322,7 +360,7 @@ node_api_initialize_platform(int32_t argc,

if (error_handler != nullptr && !platform_init_result->errors().empty()) {
v8impl::CStringArray errors(platform_init_result->errors());
error_handler(error_handler_data, errors.argv(), errors.size());
error_handler(error_handler_data, errors.c_strs(), errors.size());
}

if (early_return != nullptr) {
Expand Down Expand Up @@ -439,6 +477,34 @@ napi_status NAPI_CDECL node_api_env_options_set_exec_args(
return napi_ok;
}

napi_status NAPI_CDECL
node_api_env_options_set_preload_callback(node_api_env_options options,
node_api_preload_callback preload_cb,
void* cb_data) {
if (options == nullptr) return napi_invalid_arg;

v8impl::EmbeddedEnvironmentOptions* env_options =
reinterpret_cast<v8impl::EmbeddedEnvironmentOptions*>(options);
if (env_options->is_frozen_) return napi_generic_failure;

if (preload_cb != nullptr) {
env_options->preload_cb_ = node::EmbedderPreloadCallback(
[preload_cb, cb_data](node::Environment* node_env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require) {
node_napi_env env =
v8impl::EmbeddedEnvironment::GetOrCreateNodeApiEnv(node_env);
napi_value process_value = v8impl::JsValueFromV8LocalValue(process);
napi_value require_value = v8impl::JsValueFromV8LocalValue(require);
preload_cb(env, process_value, require_value, cb_data);
});
} else {
env_options->preload_cb_ = {};
}

return napi_ok;
}

napi_status NAPI_CDECL
node_api_env_options_use_snapshot(node_api_env_options options,
const char* snapshot_data,
Expand Down Expand Up @@ -525,8 +591,8 @@ node_api_create_env(node_api_env_options options,
}

if (error_handler != nullptr && !errors.empty()) {
v8impl::CStringArray cerrors(errors);
error_handler(error_handler_data, cerrors.argv(), cerrors.size());
v8impl::CStringArray error_arr(errors);
error_handler(error_handler_data, error_arr.c_strs(), error_arr.size());
}

if (env_setup == nullptr) {
Expand Down Expand Up @@ -556,7 +622,9 @@ node_api_create_env(node_api_env_options options,
v8::MaybeLocal<v8::Value> ret =
embedded_env->snapshot()
? node::LoadEnvironment(node_env, node::StartExecutionCallback{})
: node::LoadEnvironment(node_env, std::string_view(main_script));
: node::LoadEnvironment(node_env,
std::string_view(main_script),
embedded_env->options().preload_cb_);

embedded_env.release();

Expand Down
10 changes: 10 additions & 0 deletions src/node_api_embedding.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ typedef void(NAPI_CDECL* node_api_store_blob_callback)(void* cb_data,
const uint8_t* blob,
size_t size);

typedef void(NAPI_CDECL* node_api_preload_callback)(napi_env env,
napi_value process,
napi_value require,
void* cb_data);

typedef bool(NAPI_CDECL* node_api_run_predicate)(void* predicate_data);

NAPI_EXTERN napi_status NAPI_CDECL
Expand Down Expand Up @@ -144,6 +149,11 @@ NAPI_EXTERN napi_status NAPI_CDECL node_api_env_options_set_args(
NAPI_EXTERN napi_status NAPI_CDECL node_api_env_options_set_exec_args(
node_api_env_options options, size_t argc, const char* argv[]);

NAPI_EXTERN napi_status NAPI_CDECL
node_api_env_options_set_preload_callback(node_api_env_options options,
node_api_preload_callback preload_cb,
void* cb_data);

NAPI_EXTERN napi_status NAPI_CDECL
node_api_env_options_use_snapshot(node_api_env_options options,
const char* snapshot_data,
Expand Down
1 change: 1 addition & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For the tests to run on Windows, be sure to clone Node.js source code with the
| `code-cache` | No | Tests for a Node.js binary compiled with V8 code cache. |
| `common` | _N/A_ | Common modules shared among many tests.[^1] |
| `doctool` | Yes | Tests for the documentation generator. |
| `embdedding` | Yes | Test Node.js embedding API |
| `es-module` | Yes | Test ESM module loading. |
| `fixtures` | _N/A_ | Test fixtures used in various tests throughout the test suite. |
| `internet` | No | Tests that make real outbound network connections.[^2] |
Expand Down
3 changes: 3 additions & 0 deletions test/embedding/embedtest_main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern "C" int32_t test_main_modules_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_concurrent_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_multi_env_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_multi_thread_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_preload_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_snapshot_node_api(int32_t argc, char* argv[]);

typedef int32_t (*main_callback)(int32_t argc, char* argv[]);
Expand Down Expand Up @@ -40,6 +41,8 @@ NODE_MAIN(int32_t argc, node::argv_type raw_argv[]) {
return CallWithoutArg1(test_main_multi_thread_node_api, argc, argv);
} else if (strcmp(arg1, "snapshot-node-api") == 0) {
return CallWithoutArg1(test_main_snapshot_node_api, argc, argv);
} else if (strcmp(arg1, "preload-node-api") == 0) {
return CallWithoutArg1(test_main_preload_node_api, argc, argv);
}
}
return test_main_cpp_api(argc, argv);
Expand Down
43 changes: 43 additions & 0 deletions test/embedding/embedtest_preload_node_api.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "embedtest_node_api.h"

#include <mutex>
#include <thread>

static const char* main_script =
"globalThis.require = require('module').createRequire(process.execPath);\n"
"require('vm').runInThisContext(process.argv[1]);";

// Test the preload callback being called.
extern "C" int32_t test_main_preload_node_api(int32_t argc, char* argv[]) {
CHECK(node_api_initialize_platform(argc,
argv,
node_api_platform_no_flags,
nullptr,
nullptr,
nullptr,
nullptr));

node_api_env_options options;
CHECK(node_api_create_env_options(&options));
CHECK(node_api_env_options_set_preload_callback(
options,
[](napi_env env,
napi_value /*process*/,
napi_value /*require*/,
void* /*cb_data*/) {
napi_value global, value;
napi_get_global(env, &global);
napi_create_int32(env, 42, &value);
napi_set_named_property(env, global, "preloadValue", value);
},
nullptr));
napi_env env;
CHECK(node_api_create_env(
options, nullptr, nullptr, main_script, NAPI_VERSION, &env));

CHECK(node_api_delete_env(env, nullptr));

CHECK(node_api_dispose_platform());

return 0;
}
18 changes: 18 additions & 0 deletions test/embedding/preload-with-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Print the globalThis.preloadValue set by the preload script.
const mainPreloadValue = globalThis.preloadValue;

// Test that the preload script is executed in the worker thread.
const { Worker } = require('worker_threads');
const worker = new Worker(
'require("worker_threads").parentPort.postMessage({value: globalThis.preloadValue})',
{ eval: true }
);

const messages = [];
worker.on('message', (message) => messages.push(message));

process.on('beforeExit', () => {
console.log(
`preloadValue=${mainPreloadValue}; worker preloadValue=${messages[0].value}`
);
});
17 changes: 15 additions & 2 deletions test/embedding/test-embedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const {
} = require('../common/child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');

tmpdir.refresh();
common.allowGlobals(global.require);
Expand Down Expand Up @@ -120,7 +119,8 @@ function runCommonApiTests(apiType) {
{
status: 9,
signal: null,
stderr: `${binary}: NODE_REPL_EXTERNAL_MODULE can't be used with kDisableNodeOptionsEnv${os.EOL}`,
trim: true,
stderr: `${binary}: NODE_REPL_EXTERNAL_MODULE can't be used with kDisableNodeOptionsEnv`,
}
);
}
Expand Down Expand Up @@ -310,6 +310,19 @@ runSnapshotTests('snapshot-node-api');
stdout: '5',
}
);

const preloadScriptPath = path.join(__dirname, 'preload-with-worker.js');

runTest(
'preload-node-api: run preload callback',
spawnSyncAndAssert,
['preload-node-api', `eval(${getReadFileCodeForPath(preloadScriptPath)})`],
{
cwd: __dirname,
trim: true,
stdout: `preloadValue=42; worker preloadValue=42`,
}
);
}

/*
Expand Down

0 comments on commit f05a795

Please sign in to comment.