diff --git a/node.gyp b/node.gyp index 3d5957fb631e3d..405693c620f4cc 100644 --- a/node.gyp +++ b/node.gyp @@ -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', ], diff --git a/src/node_api_embedding.cc b/src/node_api_embedding.cc index f21bb57ee2133e..06403aca2d5883 100644 --- a/src/node_api_embedding.cc +++ b/src/node_api_embedding.cc @@ -1,15 +1,16 @@ -#include -#include // INT_MAX -#include #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 +#include // INT_MAX +#include +#include + namespace node { // Declare functions implemented in embed_helpers.cc @@ -22,33 +23,43 @@ v8::Maybe SpinEventLoopWithoutCleanup( namespace v8impl { namespace { +// A helper class to convert std::vector 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& 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(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(size_); } - const char** argv() const { return cstrings_; } private: - const char** cstrings_; - size_t size_; - std::array inplace_buffer_; + const char** c_strs_{}; + size_t size_{}; + std::array inplace_buffer_; std::unique_ptr allocated_buffer_; }; @@ -124,6 +135,7 @@ struct EmbeddedEnvironmentOptions { node_api_env_flags flags_{node_api_env_default_flags}; std::vector args_; std::vector exec_args_; + node::EmbedderPreloadCallback preload_cb_{}; node::EmbedderSnapshotData::Pointer snapshot_; std::function create_snapshot_; node::SnapshotConfig snapshot_config_{}; @@ -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 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 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(), "", NAPI_VERSION_EXPERIMENTAL); + node_env->AddCleanupHook( + [](void* arg) { static_cast(arg)->Unref(); }, env); + node_env_to_node_api_env_.try_emplace(node_env, env); + return env; } static EmbeddedEnvironment* FromNapiEnv(napi_env env) { @@ -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_; } @@ -211,8 +241,16 @@ class EmbeddedEnvironment final : public node_napi_env__ { std::unique_ptr env_options_; std::unique_ptr env_setup_; std::optional isolate_locker_; + + static std::mutex shared_mutex_; + static std::unordered_map + node_env_to_node_api_env_; }; +std::mutex EmbeddedEnvironment::shared_mutex_{}; +std::unordered_map + EmbeddedEnvironment::node_env_to_node_api_env_{}; + node::ProcessInitializationFlags::Flags GetProcessInitializationFlags( node_api_platform_flags flags) { uint32_t result = node::ProcessInitializationFlags::kNoFlags; @@ -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) { @@ -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(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 process, + v8::Local 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, @@ -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) { @@ -556,7 +622,9 @@ node_api_create_env(node_api_env_options options, v8::MaybeLocal 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(); diff --git a/src/node_api_embedding.h b/src/node_api_embedding.h index affb82562ed7a4..ff9495f3ef32c4 100644 --- a/src/node_api_embedding.h +++ b/src/node_api_embedding.h @@ -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 @@ -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, diff --git a/test/README.md b/test/README.md index 1a3eec09373412..9252f5c9496ac6 100644 --- a/test/README.md +++ b/test/README.md @@ -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] | diff --git a/test/embedding/embedtest_main.cc b/test/embedding/embedtest_main.cc index 9e11bd54c2f98a..f2aa1305854b85 100644 --- a/test/embedding/embedtest_main.cc +++ b/test/embedding/embedtest_main.cc @@ -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[]); @@ -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); diff --git a/test/embedding/embedtest_preload_node_api.cc b/test/embedding/embedtest_preload_node_api.cc new file mode 100644 index 00000000000000..18694b663f267d --- /dev/null +++ b/test/embedding/embedtest_preload_node_api.cc @@ -0,0 +1,43 @@ +#include "embedtest_node_api.h" + +#include +#include + +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; +} diff --git a/test/embedding/preload-with-worker.js b/test/embedding/preload-with-worker.js new file mode 100644 index 00000000000000..d53fe5a894cfbc --- /dev/null +++ b/test/embedding/preload-with-worker.js @@ -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}` + ); +}); diff --git a/test/embedding/test-embedding.js b/test/embedding/test-embedding.js index df17a82d8d7f3a..fc325c530bb192 100644 --- a/test/embedding/test-embedding.js +++ b/test/embedding/test-embedding.js @@ -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); @@ -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`, } ); } @@ -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`, + } + ); } /*