diff --git a/Makefile b/Makefile index 7bd80d06c17125..79495cf7c6ac68 100644 --- a/Makefile +++ b/Makefile @@ -286,7 +286,9 @@ coverage-report-js: # Runs the C++ tests using the built `cctest` executable. cctest: all @out/$(BUILDTYPE)/$@ --gtest_filter=$(GTEST_FILTER) - $(NODE) ./test/embedding/test-embedding.js + @out/$(BUILDTYPE)/embedtest "require('./test/embedding/test-embedding.js')" + @out/$(BUILDTYPE)/napi_embedding "require('./test/embedding/test-napi-embedding.js')" + @out/$(BUILDTYPE)/napi_modules ../../test/embedding/cjs.cjs ../../test/embedding/es6.mjs .PHONY: list-gtests list-gtests: @@ -565,7 +567,9 @@ test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tes $(PYTHON) tools/test.py $(PARALLEL_ARGS) -p tap --logfile test.tap \ --mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \ $(TEST_CI_ARGS) $(CI_JS_SUITES) $(CI_NATIVE_SUITES) $(CI_DOC) - $(NODE) ./test/embedding/test-embedding.js + out/Release/embedtest 'require("./test/embedding/test-embedding.js")' + out/Release/napi_embedding 'require("./test/embedding/test-napi-embedding.js")' + out/Release/napi_modules ../../test/embedding/cjs.cjs ../../test/embedding/es6.mjs $(info Clean up any leftover processes, error if found.) ps awwx | grep Release/node | grep -v grep | cat @PS_OUT=`ps awwx | grep Release/node | grep -v grep | awk '{print $$1}'`; \ diff --git a/doc/api/embedding.md b/doc/api/embedding.md index 9f831b342c2705..4d6b737b9953be 100644 --- a/doc/api/embedding.md +++ b/doc/api/embedding.md @@ -166,8 +166,47 @@ int RunNodeInstance(MultiIsolatePlatform* platform, } ``` +## Node-API Embedding + + + +As an alternative, an embedded Node.js can also be fully controlled through +Node-API. This API supports both C and C++ through [node-addon-api][]. + +An example can be found [in the Node.js source tree][napi_embedding.c]. + +```c + napi_platform platform; + napi_env env; + const char *main_script = "console.log('hello world')"; + + if (napi_create_platform(0, NULL, NULL, &platform) != napi_ok) { + fprintf(stderr, "Failed creating the platform\n"); + return -1; + } + + if (napi_create_environment(platform, NULL, main_script, + (napi_stdio){NULL, NULL, NULL}, NAPI_VERSION, &env) != napi_ok) { + fprintf(stderr, "Failed running JS\n"); + return -1; + } + + // Here you can interact with the environment through Node-API env + + if (napi_destroy_environment(env, NULL) != napi_ok) { + return -1; + } + + if (napi_destroy_platform(platform) != napi_ok) { + fprintf(stderr, "Failed destroying the platform\n"); + return -1; + } +``` + [CLI options]: cli.md [`process.memoryUsage()`]: process.md#processmemoryusage [deprecation policy]: deprecations.md [embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc +[napi_embedding.c]: https://github.com/nodejs/node/blob/HEAD/test/embedding/napi_embedding.c +[node-addon-api]: https://github.com/nodejs/node-addon-api [src/node.h]: https://github.com/nodejs/node/blob/HEAD/src/node.h diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 503df17be005c6..6f0d244498a80f 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -6581,6 +6581,145 @@ idempotent. This API may only be called from the main thread. +## Using embedded Node.js + +### `napi_create_platform` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_create_platform(int argc, + char** argv, + napi_error_message_handler err_handler, + napi_platform* result); +``` + +* `[in] argc`: CLI argument count, pass 0 for autofilling. +* `[in] argv`: CLI arguments, pass NULL for autofilling. +* `[in] err_handler`: If different than NULL, will be called back with each + error message. There can be multiple error messages but the API guarantees + that no calls will be made after the `napi_create_platform` has returned. + In practice, the implementation of graceful recovery is still in progress, + and many errors will be fatal, resulting in an `abort()`. +* `[out] result`: A `napi_platform` result. + +This function must be called once to initialize V8 and Node.js when using as a +shared library. + +### `napi_destroy_platform` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_destroy_platform(napi_platform platform); +``` + +* `[in] platform`: platform handle. + +Destroy the Node.js / V8 processes. + +### `napi_create_environment` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_create_environment(napi_platform platform, + napi_error_message_handler err_handler, + const char* main_script, + int32_t api_version, + napi_env* result); +``` + +* `[in] platform`: platform handle. +* `[in] err_handler`: If different than NULL, will be called back with each + error message. There can be multiple error messages but the API guarantees + that no calls will be made after the `napi_create_platform` has returned. + In practice, the implementation of graceful recovery is still in progress, + and many errors will be fatal, resulting in an `abort()`. +* `[in] main_script`: If different than NULL, custom JavaScript to run in + addition to the default bootstrap that creates an empty + ready-to-use CJS/ES6 environment with `global.require()` and + `global.import()` functions that resolve modules from the directory of + the compiled binary. + It can be used to redirect `process.stdin`/ `process.stdout` streams + since Node.js might switch these file descriptors to non-blocking mode. +* `[in] api_version`: Node-API version to conform to, pass `NAPI_VERSION` + for the latest available. +* `[out] result`: A `napi_env` result. + +Initialize a new environment. A single platform can hold multiple Node.js +environments that will run in a separate V8 isolate each. If the returned +value is `napi_ok` or `napi_pending_exception`, the environment must be +destroyed with `napi_destroy_environment` to free all allocated memory. + +### `napi_run_environment` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_run_environment(napi_env env); +``` + +* `[in] env`: environment handle. + +Iterate the event loop of the environment. Executes all pending +JavaScript callbacks. Cannot be called with JavaScript on the +stack (ie in a JavaScript callback). + +### `napi_await_promise` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_await_promise(napi_env env, + napi_value promise, + napi_value *result); +``` + +* `[in] env`: environment handle. +* `[in] promise`: JS Promise. +* `[out] result`: Will receive the value that the Promise resolved with. + +Iterate the event loop of the environment until the `promise` has been +resolved. Returns `napi_pending_exception` on rejection. Cannot be called +with JavaScript on the stack (ie in a JavaScript callback). + +### `napi_destroy_environment` + + + +> Stability: 1 - Experimental + +```c +napi_status napi_destroy_environment(napi_env env); +``` + +* `[in] env`: environment handle. + +Destroy the Node.js environment / V8 isolate. + ## Miscellaneous utilities ### `node_api_get_module_file_name` diff --git a/lib/internal/bootstrap/switches/is_embedded_env.js b/lib/internal/bootstrap/switches/is_embedded_env.js new file mode 100644 index 00000000000000..004a424f8537de --- /dev/null +++ b/lib/internal/bootstrap/switches/is_embedded_env.js @@ -0,0 +1,20 @@ +'use strict'; + +// This is the bootstrapping code used when creating a new environment +// through Node-API + +// Set up globalThis.require and globalThis.import so that they can +// be easily accessed from C/C++ + +/* global primordials */ + +const { globalThis } = primordials; +const cjsLoader = require('internal/modules/cjs/loader'); +const esmLoader = require('internal/process/esm_loader').esmLoader; + +globalThis.module = new cjsLoader.Module(); +globalThis.require = require('module').createRequire(process.execPath); + +const parent_path = require('url').pathToFileURL(process.execPath); +globalThis.import = (mod) => esmLoader.import(mod, parent_path, { __proto__: null }); +globalThis.import.meta = { url: parent_path }; diff --git a/node.gyp b/node.gyp index e5ca012a822f34..ccfaed1814a239 100644 --- a/node.gyp +++ b/node.gyp @@ -94,6 +94,7 @@ 'src/module_wrap.cc', 'src/node.cc', 'src/node_api.cc', + 'src/node_api_embedding.cc', 'src/node_binding.cc', 'src/node_blob.cc', 'src/node_buffer.cc', @@ -1188,6 +1189,110 @@ ], }, # embedtest + { + 'target_name': 'napi_embedding', + 'type': 'executable', + + 'dependencies': [ + '<(node_lib_target_name)', + 'deps/histogram/histogram.gyp:histogram', + 'deps/uvwasi/uvwasi.gyp:uvwasi', + ], + + 'includes': [ + 'node.gypi' + ], + + 'include_dirs': [ + 'src', + 'tools/msvs/genfiles', + 'deps/v8/include', + 'deps/cares/include', + 'deps/uv/include', + 'deps/uvwasi/include', + 'test/embedding', + ], + + 'sources': [ + 'src/node_snapshot_stub.cc', + 'test/embedding/napi_embedding.c', + ], + + 'conditions': [ + ['OS=="solaris"', { + 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] + }], + # Skip cctest while building shared lib node for Windows + [ 'OS=="win" and node_shared=="true"', { + 'type': 'none', + }], + [ 'node_shared=="true"', { + 'xcode_settings': { + 'OTHER_LDFLAGS': [ '-Wl,-rpath,@loader_path', ], + }, + }], + ['OS=="win"', { + 'libraries': [ + 'Dbghelp.lib', + 'winmm.lib', + 'Ws2_32.lib', + ], + }], + ], + }, # napi_embedding + + { + 'target_name': 'napi_modules', + 'type': 'executable', + + 'dependencies': [ + '<(node_lib_target_name)', + 'deps/histogram/histogram.gyp:histogram', + 'deps/uvwasi/uvwasi.gyp:uvwasi', + ], + + 'includes': [ + 'node.gypi' + ], + + 'include_dirs': [ + 'src', + 'tools/msvs/genfiles', + 'deps/v8/include', + 'deps/cares/include', + 'deps/uv/include', + 'deps/uvwasi/include', + 'test/embedding', + ], + + 'sources': [ + 'src/node_snapshot_stub.cc', + 'test/embedding/napi_modules.c', + ], + + 'conditions': [ + ['OS=="solaris"', { + 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] + }], + # Skip cctest while building shared lib node for Windows + [ 'OS=="win" and node_shared=="true"', { + 'type': 'none', + }], + [ 'node_shared=="true"', { + 'xcode_settings': { + 'OTHER_LDFLAGS': [ '-Wl,-rpath,@loader_path', ], + }, + }], + ['OS=="win"', { + 'libraries': [ + 'Dbghelp.lib', + 'winmm.lib', + 'Ws2_32.lib', + ], + }], + ], + }, # napi_modules + { 'target_name': 'overlapped-checker', 'type': 'executable', diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index 6fac48d1b534d2..609415fce5e04b 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -19,7 +19,16 @@ using v8::TryCatch; namespace node { -Maybe SpinEventLoopInternal(Environment* env) { +static const auto AlwaysTrue = []() { return true; }; + +/** + * Spin the event loop until there are no pending callbacks or + * the condition returns false. + * Returns an error if the environment died and no failure if the environment is + * reusable. + */ +Maybe SpinEventLoopWithoutCleanupInternal( + Environment* env, const std::function& condition) { CHECK_NOT_NULL(env); MultiIsolatePlatform* platform = GetMultiIsolatePlatform(env); CHECK_NOT_NULL(platform); @@ -32,23 +41,54 @@ Maybe SpinEventLoopInternal(Environment* env) { 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); + bool more; + env->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); + do { + if (env->is_stopping()) return Nothing(); + int loop; do { - if (env->is_stopping()) break; - uv_run(env->event_loop(), UV_RUN_DEFAULT); - if (env->is_stopping()) break; + loop = uv_run(env->event_loop(), UV_RUN_ONCE); + } while (loop && condition() && !env->is_stopping()); + if (env->is_stopping()) return Nothing(); - platform->DrainTasks(isolate); + platform->DrainTasks(isolate); - more = uv_loop_alive(env->event_loop()); - if (more && !env->is_stopping()) continue; + more = uv_loop_alive(env->event_loop()); + } while (more); + env->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); + env->set_trace_sync_io(false); + return Just(ExitCode::kNoFailure); +} + +/** + * Spin the event loop until there are no pending callbacks and + * then shutdown the environment. Returns a reference to the + * exit value or an empty reference on unexpected exit. + */ +Maybe SpinEventLoopInternal(Environment* env) { + CHECK_NOT_NULL(env); + MultiIsolatePlatform* platform = GetMultiIsolatePlatform(env); + CHECK_NOT_NULL(platform); + + Isolate* isolate = env->isolate(); + HandleScope handle_scope(isolate); + Context::Scope context_scope(env->context()); + SealHandleScope seal(isolate); - if (EmitProcessBeforeExit(env).IsNothing()) + if (env->is_stopping()) return Nothing(); + + env->set_trace_sync_io(env->options()->trace_sync_io); + { + bool more; + + do { + if (SpinEventLoopWithoutCleanupInternal(env, AlwaysTrue).IsNothing()) break; + if (EmitProcessBeforeExit(env).IsNothing()) break; + { HandleScope handle_scope(isolate); if (env->RunSnapshotSerializeCallback().IsEmpty()) { @@ -56,16 +96,12 @@ Maybe SpinEventLoopInternal(Environment* env) { } } - // Emit `beforeExit` if the loop became alive either after emitting - // event, or after running some callbacks. + // Loop if after `beforeExit` the loop became alive 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); // Clear the serialize callback even though the JS-land queue should // be empty this point so that the deserialized instance won't // attempt to call into JS again. @@ -256,6 +292,17 @@ Maybe SpinEventLoop(Environment* env) { return Just(static_cast(result.FromJust())); } +Maybe SpinEventLoopWithoutCleanup(Environment* env) { + return SpinEventLoopWithoutCleanup(env, AlwaysTrue); +} +Maybe SpinEventLoopWithoutCleanup( + Environment* env, const std::function& condition) { + Maybe result = SpinEventLoopWithoutCleanupInternal(env, condition); + if (result.IsNothing()) { + return Nothing(); + } + return Just(static_cast(result.FromJust())); +} uv_loop_t* CommonEnvironmentSetup::event_loop() const { return &impl_->loop; } diff --git a/src/env-inl.h b/src/env-inl.h index 666dad97b021f4..efd672a022a8c1 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -642,6 +642,10 @@ inline bool Environment::is_main_thread() const { return worker_context() == nullptr; } +inline bool Environment::is_embedded_env() const { + return embedded_ != nullptr; +} + inline bool Environment::no_native_addons() const { return (flags_ & EnvironmentFlags::kNoNativeAddons) || !options_->allow_native_addons; @@ -812,6 +816,13 @@ void Environment::set_process_exit_handler( process_exit_handler_ = std::move(handler); } +inline EmbeddedEnvironment* Environment::get_embedded() { + return embedded_; +} +inline void Environment::set_embedded(EmbeddedEnvironment* env) { + embedded_ = env; +} + #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) diff --git a/src/env.h b/src/env.h index ff09da28b2cadc..f58a46d3dfacb2 100644 --- a/src/env.h +++ b/src/env.h @@ -588,11 +588,18 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code); v8::Maybe SpinEventLoopInternal(Environment* env); v8::Maybe EmitProcessExitInternal(Environment* env); +/** + * EmbeddedEnvironment is the JavaScript engine-neutral part of an + * embedded environment controlled by a C/C++ caller of libnode + */ +class EmbeddedEnvironment {}; + /** * Environment is a per-isolate data structure that represents an execution * environment. Each environment has a principal realm. An environment can * create multiple subsidiary synthetic realms. */ + class Environment : public MemoryRetainer { public: Environment(const Environment&) = delete; @@ -793,6 +800,7 @@ class Environment : public MemoryRetainer { inline void set_has_serialized_options(bool has_serialized_options); inline bool is_main_thread() const; + inline bool is_embedded_env() const; inline bool no_native_addons() const; inline bool should_not_register_esm_loader() const; inline bool should_create_inspector() const; @@ -1008,6 +1016,9 @@ class Environment : public MemoryRetainer { inline void set_process_exit_handler( std::function&& handler); + inline EmbeddedEnvironment* get_embedded(); + inline void set_embedded(EmbeddedEnvironment* env); + void RunAndClearNativeImmediates(bool only_refed = false); void RunAndClearInterrupts(); @@ -1216,6 +1227,9 @@ class Environment : public MemoryRetainer { // track of the BackingStore for a given pointer. std::unordered_map> released_allocated_buffers_; + + // Used for embedded instances + EmbeddedEnvironment* embedded_; }; } // namespace node diff --git a/src/node.h b/src/node.h index b041a20318145b..473e6b62ded6c3 100644 --- a/src/node.h +++ b/src/node.h @@ -860,14 +860,26 @@ 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. +// 1. Call uv_run() on the event loop until it is drained or the optional +// condition returns false. // 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. +// Returns false if the environment died and true if it can be reused. +// This function only works if `env` has an associated `MultiIsolatePlatform`. +NODE_EXTERN v8::Maybe SpinEventLoopWithoutCleanup( + Environment* env, const std::function& condition); +NODE_EXTERN v8::Maybe SpinEventLoopWithoutCleanup(Environment* env); + +// Runs the main loop for a given Environment and performs environment +// shutdown when the loop exits. This roughly performs the +// following steps: +// 1. Call SpinEventLoopWithoutCleanup() +// 2. Call EmitProcessBeforeExit(). +// 3. If the event loop is alive again, go to Step 1. +// 4. 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`. +// as soon as possible, returning an empty `Maybe`. Otherwise it will return +// a reference to the exit value. // This function only works if `env` has an associated `MultiIsolatePlatform`. NODE_EXTERN v8::Maybe SpinEventLoop(Environment* env); diff --git a/src/node_api.cc b/src/node_api.cc index 5b5f6a55a0fc93..2624489e3f9631 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -4,6 +4,7 @@ #include "js_native_api_v8.h" #include "memory_tracker-inl.h" #include "node_api.h" +#include "node_api_embedding.h" #include "node_api_internals.h" #include "node_binding.h" #include "node_buffer.h" diff --git a/src/node_api_embedding.cc b/src/node_api_embedding.cc new file mode 100644 index 00000000000000..5e4b1ede5fa840 --- /dev/null +++ b/src/node_api_embedding.cc @@ -0,0 +1,252 @@ +#include +#include // INT_MAX +#include +#define NAPI_EXPERIMENTAL +#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" + +namespace v8impl { +namespace { + +class EmbeddedEnvironment : public node::EmbeddedEnvironment { + public: + explicit EmbeddedEnvironment( + std::unique_ptr&& setup, + const std::shared_ptr& main_resource) + : main_resource_(main_resource), + setup_(std::move(setup)), + locker_(setup_->isolate()), + isolate_scope_(setup_->isolate()), + handle_scope_(setup_->isolate()), + context_scope_(setup_->context()), + seal_scope_(nullptr) {} + + inline node::CommonEnvironmentSetup* setup() { return setup_.get(); } + inline void seal() { + seal_scope_ = + std::make_unique(setup_->isolate()); + } + + private: + // The pointer to the UTF-16 main script convertible to V8 UnionBytes resource + // This must be constructed first and destroyed last because the isolate + // references it + std::shared_ptr main_resource_; + + std::unique_ptr setup_; + v8::Locker locker_; + v8::Isolate::Scope isolate_scope_; + v8::HandleScope handle_scope_; + v8::Context::Scope context_scope_; + // As this handle scope will remain open for the lifetime + // of the environment, we seal it to prevent it from + // becoming everyone's favorite trash bin + std::unique_ptr seal_scope_; +}; + +} // end of anonymous namespace +} // end of namespace v8impl + +napi_status NAPI_CDECL +napi_create_platform(int argc, + char** argv, + napi_error_message_handler err_handler, + napi_platform* result) { + argv = uv_setup_args(argc, argv); + std::vector args(argv, argv + argc); + if (args.size() < 1) args.push_back("libnode"); + + std::unique_ptr node_platform = + node::InitializeOncePerProcess( + args, + {node::ProcessInitializationFlags::kNoInitializeV8, + node::ProcessInitializationFlags::kNoInitializeNodeV8Platform}); + + for (const std::string& error : node_platform->errors()) { + if (err_handler != nullptr) { + err_handler(error.c_str()); + } else { + fprintf(stderr, "%s\n", error.c_str()); + } + } + if (node_platform->early_return() != 0) { + return napi_generic_failure; + } + + int thread_pool_size = + static_cast(node::per_process::cli_options->v8_thread_pool_size); + std::unique_ptr v8_platform = + node::MultiIsolatePlatform::Create(thread_pool_size); + v8::V8::InitializePlatform(v8_platform.get()); + v8::V8::Initialize(); + reinterpret_cast(node_platform.get()) + ->platform_ = v8_platform.release(); + *result = reinterpret_cast(node_platform.release()); + return napi_ok; +} + +napi_status NAPI_CDECL napi_destroy_platform(napi_platform platform) { + auto wrapper = reinterpret_cast(platform); + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + node::TearDownOncePerProcess(); + delete wrapper->platform(); + delete wrapper; + return napi_ok; +} + +napi_status NAPI_CDECL +napi_create_environment(napi_platform platform, + napi_error_message_handler err_handler, + const char* main_script, + int32_t api_version, + napi_env* result) { + auto wrapper = reinterpret_cast(platform); + std::vector errors_vec; + + auto setup = node::CommonEnvironmentSetup::Create( + wrapper->platform(), &errors_vec, wrapper->args(), wrapper->exec_args()); + + for (const std::string& error : errors_vec) { + if (err_handler != nullptr) { + err_handler(error.c_str()); + } else { + fprintf(stderr, "%s\n", error.c_str()); + } + } + if (setup == nullptr) { + return napi_generic_failure; + } + + std::shared_ptr main_resource = nullptr; + if (main_script != nullptr) { + // We convert the user-supplied main_script to a UTF-16 resource + // and we store its shared_ptr in the environment + size_t u8_length = strlen(main_script); + size_t expected_u16_length = + simdutf::utf16_length_from_utf8(main_script, u8_length); + auto out = std::make_shared>(expected_u16_length); + size_t u16_length = simdutf::convert_utf8_to_utf16( + main_script, u8_length, reinterpret_cast(out->data())); + out->resize(u16_length); + main_resource = std::make_shared( + out->data(), out->size(), out); + } + + auto emb_env = + new v8impl::EmbeddedEnvironment(std::move(setup), main_resource); + + std::string filename = + wrapper->args().size() > 1 ? wrapper->args()[1] : ""; + auto env__ = + new node_napi_env__(emb_env->setup()->context(), filename, api_version); + emb_env->setup()->env()->set_embedded(emb_env); + env__->node_env()->AddCleanupHook( + [](void* arg) { static_cast(arg)->Unref(); }, + static_cast(env__)); + *result = env__; + + auto env = emb_env->setup()->env(); + + auto ret = node::LoadEnvironment( + env, + [env, resource = main_resource.get()]( + const node::StartExecutionCallbackInfo& info) + -> v8::MaybeLocal { + node::Realm* realm = env->principal_realm(); + auto ret = realm->ExecuteBootstrapper( + "internal/bootstrap/switches/is_embedded_env"); + if (ret.IsEmpty()) return ret; + + std::string name = + "embedder_main_napi_" + std::to_string(env->thread_id()); + if (resource != nullptr) { + env->builtin_loader()->Add(name.c_str(), node::UnionBytes(resource)); + return realm->ExecuteBootstrapper(name.c_str()); + } else { + return v8::Undefined(env->isolate()); + } + }); + if (ret.IsEmpty()) return napi_pending_exception; + + emb_env->seal(); + + return napi_ok; +} + +napi_status NAPI_CDECL napi_destroy_environment(napi_env env, int* exit_code) { + CHECK_ARG(env, env); + node_napi_env node_env = reinterpret_cast(env); + + int r = node::SpinEventLoop(node_env->node_env()).FromMaybe(1); + if (exit_code != nullptr) *exit_code = r; + node::Stop(node_env->node_env()); + + auto emb_env = reinterpret_cast( + node_env->node_env()->get_embedded()); + node_env->node_env()->set_embedded(nullptr); + // This deletes the uniq_ptr to node::CommonEnvironmentSetup + // and the v8::locker + delete emb_env; + + return napi_ok; +} + +napi_status NAPI_CDECL napi_run_environment(napi_env env) { + CHECK_ARG(env, env); + node_napi_env node_env = reinterpret_cast(env); + + if (node::SpinEventLoopWithoutCleanup(node_env->node_env()).IsNothing()) + return napi_closing; + + return napi_ok; +} + +static void napi_promise_error_handler( + const v8::FunctionCallbackInfo& info) { + return; +} + +napi_status napi_await_promise(napi_env env, + napi_value promise, + napi_value* result) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::EscapableHandleScope scope(env->isolate); + node_napi_env node_env = reinterpret_cast(env); + + v8::Local promise_value = v8impl::V8LocalValueFromJsValue(promise); + if (promise_value.IsEmpty() || !promise_value->IsPromise()) + return napi_invalid_arg; + v8::Local promise_object = promise_value.As(); + + v8::Local rejected = v8::Boolean::New(env->isolate, false); + v8::Local err_handler = + v8::Function::New(env->context(), napi_promise_error_handler, rejected) + .ToLocalChecked(); + + if (promise_object->Catch(env->context(), err_handler).IsEmpty()) + return napi_pending_exception; + + if (node::SpinEventLoopWithoutCleanup( + node_env->node_env(), + [&promise_object]() { + return promise_object->State() == + v8::Promise::PromiseState::kPending; + }) + .IsNothing()) + return napi_closing; + + *result = + v8impl::JsValueFromV8LocalValue(scope.Escape(promise_object->Result())); + if (promise_object->State() == v8::Promise::PromiseState::kRejected) + return napi_pending_exception; + + return napi_ok; +} diff --git a/src/node_api_embedding.h b/src/node_api_embedding.h new file mode 100644 index 00000000000000..2d2afcab8749a3 --- /dev/null +++ b/src/node_api_embedding.h @@ -0,0 +1,48 @@ +#ifndef SRC_NODE_API_EMBEDDING_H_ +#define SRC_NODE_API_EMBEDDING_H_ + +#include "js_native_api.h" +#include "js_native_api_types.h" + +#ifdef __cplusplus +#define EXTERN_C_START extern "C" { +#define EXTERN_C_END } +#else +#define EXTERN_C_START +#define EXTERN_C_END +#endif + +typedef struct napi_platform__* napi_platform; + +EXTERN_C_START + +typedef void (*napi_error_message_handler)(const char* msg); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_platform(int argc, + char** argv, + napi_error_message_handler err_handler, + napi_platform* result); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_destroy_platform(napi_platform platform); + +NAPI_EXTERN napi_status NAPI_CDECL +napi_create_environment(napi_platform platform, + napi_error_message_handler err_handler, + const char* main_script, + int32_t api_version, + napi_env* result); + +NAPI_EXTERN napi_status NAPI_CDECL napi_run_environment(napi_env env); + +NAPI_EXTERN napi_status NAPI_CDECL napi_await_promise(napi_env env, + napi_value promise, + napi_value* result); + +NAPI_EXTERN napi_status NAPI_CDECL napi_destroy_environment(napi_env env, + int* exit_code); + +EXTERN_C_END + +#endif // SRC_NODE_API_EMBEDDING_H_ diff --git a/src/node_builtins.cc b/src/node_builtins.cc index bbb63df7899d4b..30c7a346af1afe 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -404,12 +404,10 @@ MaybeLocal BuiltinLoader::LookupAndCompile(Local context, strlen("internal/bootstrap/")) == 0) { // internal/main/*, internal/bootstrap/*: process, require, // internalBinding, primordials - parameters = { - FIXED_ONE_BYTE_STRING(isolate, "process"), - FIXED_ONE_BYTE_STRING(isolate, "require"), - FIXED_ONE_BYTE_STRING(isolate, "internalBinding"), - FIXED_ONE_BYTE_STRING(isolate, "primordials"), - }; + parameters = {FIXED_ONE_BYTE_STRING(isolate, "process"), + FIXED_ONE_BYTE_STRING(isolate, "require"), + FIXED_ONE_BYTE_STRING(isolate, "internalBinding"), + FIXED_ONE_BYTE_STRING(isolate, "primordials")}; } else { // others: exports, require, module, process, internalBinding, primordials parameters = { @@ -457,11 +455,17 @@ MaybeLocal BuiltinLoader::CompileAndCall(Local context, "internal/bootstrap/", strlen("internal/bootstrap/")) == 0) { // internal/main/*, internal/bootstrap/*: process, require, - // internalBinding, primordials + // internalBinding, primordials, arguments = {realm->process_object(), realm->builtin_module_require(), realm->internal_binding_loader(), realm->primordials()}; + } else if (strncmp(id, "embedder_main_", strlen("embedder_main_")) == 0) { + // Synthetic embedder main scripts from LoadEnvironment(): process, require + arguments = { + realm->process_object(), + realm->builtin_module_require(), + }; } else { // This should be invoked with the other CompileAndCall() methods, as // we are unable to generate the arguments. diff --git a/test/embedding/.eslintrc.yaml b/test/embedding/.eslintrc.yaml new file mode 100644 index 00000000000000..b3981bdd272eca --- /dev/null +++ b/test/embedding/.eslintrc.yaml @@ -0,0 +1,3 @@ +rules: + node-core/required-modules: off + node-core/require-common-first: off diff --git a/test/embedding/cjs.cjs b/test/embedding/cjs.cjs new file mode 100644 index 00000000000000..df0ddbf40cf291 --- /dev/null +++ b/test/embedding/cjs.cjs @@ -0,0 +1,3 @@ +module.exports = { + value: "original" +}; diff --git a/test/embedding/es6.mjs b/test/embedding/es6.mjs new file mode 100644 index 00000000000000..01b505b2c5fb19 --- /dev/null +++ b/test/embedding/es6.mjs @@ -0,0 +1 @@ +export const value = 'genuine'; diff --git a/test/embedding/napi_embedding.c b/test/embedding/napi_embedding.c new file mode 100644 index 00000000000000..618f8d53b46505 --- /dev/null +++ b/test/embedding/napi_embedding.c @@ -0,0 +1,257 @@ +#define NAPI_EXPERIMENTAL +#include +#include +#include + +#include +#include + +// 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. + +static int RunNodeInstance(napi_platform platform); + +const char* main_script = + "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };"; + +#define CHECK(test, msg) \ + if (test != napi_ok) { \ + fprintf(stderr, "%s\n", msg); \ + goto fail; \ + } + +int main(int argc, char** argv) { + napi_platform platform; + + CHECK(napi_create_platform(argc, argv, NULL, &platform), + "Failed creating the platform"); + + int exit_code = RunNodeInstance(platform); + + CHECK(napi_destroy_platform(platform), "Failed destroying the platform"); + + return exit_code; +fail: + return -1; +} + +int callMe(napi_env env) { + napi_handle_scope scope; + napi_value global; + napi_value cb; + napi_value key; + + napi_open_handle_scope(env, &scope); + + CHECK(napi_get_global(env, &global), "Failed accessing the global object"); + + CHECK(napi_create_string_utf8(env, "callMe", strlen("callMe"), &key), + "create string"); + + CHECK(napi_get_property(env, global, key, &cb), + "Failed accessing the global object"); + + napi_valuetype cb_type; + CHECK(napi_typeof(env, cb, &cb_type), "Failed accessing the global object"); + + if (cb_type == napi_function) { + napi_value undef; + napi_get_undefined(env, &undef); + napi_value arg; + napi_create_string_utf8(env, "called", strlen("called"), &arg); + napi_value result; + napi_call_function(env, undef, cb, 1, &arg, &result); + + char buf[32]; + size_t len; + napi_get_value_string_utf8(env, result, buf, 32, &len); + if (strncmp(buf, "called you", strlen("called you"))) { + fprintf(stderr, "Invalid value received: %s\n", buf); + goto fail; + } + printf("%s", buf); + } else if (cb_type != napi_undefined) { + fprintf(stderr, "Invalid callMe value\n"); + goto fail; + } + + napi_value object; + CHECK(napi_create_object(env, &object), "Failed creating an object\n"); + + napi_close_handle_scope(env, scope); + return 0; + +fail: + napi_close_handle_scope(env, scope); + return -1; +} + +char callback_buf[32]; +size_t callback_buf_len; +napi_value c_cb(napi_env env, napi_callback_info info) { + napi_handle_scope scope; + size_t argc = 1; + napi_value arg; + napi_value undef; + + napi_open_handle_scope(env, &scope); + napi_get_cb_info(env, info, &argc, &arg, NULL, NULL); + + napi_get_value_string_utf8(env, arg, callback_buf, 32, &callback_buf_len); + napi_get_undefined(env, &undef); + napi_close_handle_scope(env, scope); + return undef; +} + +int waitMe(napi_env env) { + napi_handle_scope scope; + napi_value global; + napi_value cb; + napi_value key; + + napi_open_handle_scope(env, &scope); + + CHECK(napi_get_global(env, &global), "Failed accessing the global object"); + + napi_create_string_utf8(env, "waitMe", strlen("waitMe"), &key); + + CHECK(napi_get_property(env, global, key, &cb), + "Failed accessing the global object"); + + napi_valuetype cb_type; + CHECK(napi_typeof(env, cb, &cb_type), "Failed accessing the global object"); + + if (cb_type == napi_function) { + napi_value undef; + napi_get_undefined(env, &undef); + napi_value args[2]; + napi_create_string_utf8(env, "waited", strlen("waited"), &args[0]); + CHECK(napi_create_function( + env, "wait_cb", strlen("wait_cb"), c_cb, NULL, &args[1]), + "Failed creating function"); + + napi_value result; + memset(callback_buf, 0, 32); + napi_call_function(env, undef, cb, 2, args, &result); + if (!strncmp(callback_buf, "waited you", strlen("waited you"))) { + fprintf(stderr, "Anachronism detected: %s\n", callback_buf); + goto fail; + } + + CHECK(napi_run_environment(env), "Failed spinning the event loop"); + + if (strncmp(callback_buf, "waited you", strlen("waited you"))) { + fprintf(stderr, "Invalid value received: %s\n", callback_buf); + goto fail; + } + printf("%s", callback_buf); + } else if (cb_type != napi_undefined) { + fprintf(stderr, "Invalid waitMe value\n"); + goto fail; + } + + napi_close_handle_scope(env, scope); + return 0; + +fail: + napi_close_handle_scope(env, scope); + return -1; +} + +int waitMeWithCheese(napi_env env) { + napi_handle_scope scope; + napi_value global; + napi_value cb; + napi_value key; + + napi_open_handle_scope(env, &scope); + + CHECK(napi_get_global(env, &global), "Failed accessing the global object"); + + napi_create_string_utf8(env, "waitPromise", strlen("waitPromise"), &key); + + CHECK(napi_get_property(env, global, key, &cb), + "Failed accessing the global object"); + + napi_valuetype cb_type; + CHECK(napi_typeof(env, cb, &cb_type), "Failed accessing the global object"); + + if (cb_type == napi_function) { + napi_value undef; + napi_get_undefined(env, &undef); + napi_value arg; + bool result_type; + + napi_create_string_utf8(env, "waited", strlen("waited"), &arg); + + memset(callback_buf, 0, 32); + napi_value promise; + napi_value result; + CHECK(napi_call_function(env, undef, cb, 1, &arg, &promise), + "Failed evaluating the function"); + + if (!strncmp( + callback_buf, "waited with cheese", strlen("waited with cheese"))) { + fprintf(stderr, "Anachronism detected: %s\n", callback_buf); + goto fail; + } + + CHECK(napi_is_promise(env, promise, &result_type), + "Failed evaluating the result"); + + if (!result_type) { + fprintf(stderr, "Result is not a Promise\n"); + goto fail; + } + + napi_status r = napi_await_promise(env, promise, &result); + if (r != napi_ok && r != napi_pending_exception) { + fprintf(stderr, "Failed awaiting promise: %d\n", r); + goto fail; + } + + const char* expected; + if (r == napi_ok) + expected = "waited with cheese"; + else + expected = "waited without cheese"; + + napi_get_value_string_utf8( + env, result, callback_buf, 32, &callback_buf_len); + if (strncmp(callback_buf, expected, strlen(expected))) { + fprintf(stderr, "Invalid value received: %s\n", callback_buf); + goto fail; + } + printf("%s", callback_buf); + } else if (cb_type != napi_undefined) { + fprintf(stderr, "Invalid waitPromise value\n"); + goto fail; + } + + napi_close_handle_scope(env, scope); + return 0; + +fail: + napi_close_handle_scope(env, scope); + return -1; +} + +int RunNodeInstance(napi_platform platform) { + napi_env env; + int exit_code; + + CHECK( + napi_create_environment(platform, NULL, main_script, NAPI_VERSION, &env), + "Failed running JS"); + + if (callMe(env) != 0) exit_code = -1; + if (waitMe(env) != 0) exit_code = -1; + if (waitMeWithCheese(env) != 0) exit_code = -1; + + CHECK(napi_destroy_environment(env, &exit_code), "napi_destroy_environment"); + + return exit_code; + +fail: + return -1; +} diff --git a/test/embedding/napi_modules.c b/test/embedding/napi_modules.c new file mode 100644 index 00000000000000..840c22dc38587f --- /dev/null +++ b/test/embedding/napi_modules.c @@ -0,0 +1,81 @@ +#include +#include +#define NAPI_EXPERIMENTAL +#include +#include + +#define CHECK(op, msg) \ + if (op != napi_ok) { \ + fprintf(stderr, "Failed: %s\n", msg); \ + return -1; \ + } + +int main(int argc, char* argv[]) { + napi_platform platform; + + if (argc < 3) { + fprintf(stderr, "napi_modules \n"); + return -2; + } + + CHECK(napi_create_platform(0, NULL, NULL, &platform), + "Failed creating the platform"); + + napi_env env; + CHECK(napi_create_environment(platform, NULL, NULL, NAPI_VERSION, &env), + "Failed running JS"); + + napi_handle_scope scope; + CHECK(napi_open_handle_scope(env, &scope), "Failed creating a scope"); + + napi_value global, import_name, require_name, import, require, cjs, es6, + value; + CHECK(napi_get_global(env, &global), "napi_get_global"); + CHECK(napi_create_string_utf8(env, "import", strlen("import"), &import_name), + "create_string"); + CHECK( + napi_create_string_utf8(env, "require", strlen("require"), &require_name), + "create_string"); + CHECK(napi_get_property(env, global, import_name, &import), "import"); + CHECK(napi_get_property(env, global, require_name, &require), "require"); + + CHECK(napi_create_string_utf8(env, argv[1], strlen(argv[1]), &cjs), + "create_string"); + CHECK(napi_create_string_utf8(env, argv[2], strlen(argv[2]), &es6), + "create_string"); + CHECK(napi_create_string_utf8(env, "value", strlen("value"), &value), + "create_string"); + + napi_value es6_module, es6_promise, cjs_module, es6_result, cjs_result; + char buffer[32]; + size_t bufferlen; + + CHECK(napi_call_function(env, global, import, 1, &es6, &es6_promise), + "import"); + CHECK(napi_await_promise(env, es6_promise, &es6_module), "await"); + + CHECK(napi_get_property(env, es6_module, value, &es6_result), "value"); + CHECK(napi_get_value_string_utf8( + env, es6_result, buffer, sizeof(buffer), &bufferlen), + "string"); + if (strncmp(buffer, "genuine", bufferlen)) { + fprintf(stderr, "Unexpected value: %s\n", buffer); + return -1; + } + + CHECK(napi_call_function(env, global, require, 1, &cjs, &cjs_module), + "require"); + CHECK(napi_get_property(env, cjs_module, value, &cjs_result), "value"); + CHECK(napi_get_value_string_utf8( + env, cjs_result, buffer, sizeof(buffer), &bufferlen), + "string"); + if (strncmp(buffer, "original", bufferlen)) { + fprintf(stderr, "Unexpected value: %s\n", buffer); + return -1; + } + + CHECK(napi_close_handle_scope(env, scope), "Failed destroying handle scope"); + CHECK(napi_destroy_environment(env, NULL), "destroy"); + CHECK(napi_destroy_platform(platform), "Failed destroying the platform"); + return 0; +} diff --git a/test/embedding/test-napi-embedding.js b/test/embedding/test-napi-embedding.js new file mode 100644 index 00000000000000..f854f25b230719 --- /dev/null +++ b/test/embedding/test-napi-embedding.js @@ -0,0 +1,75 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const child_process = require('child_process'); +const path = require('path'); + +common.allowGlobals(global.require); +common.allowGlobals(global.embedVars); +common.allowGlobals(global.import); +common.allowGlobals(global.module); +let binary = `out/${common.buildType}/napi_embedding`; +if (common.isWindows) { + binary += '.exe'; +} +binary = path.resolve(__dirname, '..', '..', binary); + +assert.strictEqual( + child_process.spawnSync(binary, ['console.log(42)']) + .stdout.toString().trim(), + '42'); + +assert.strictEqual( + child_process.spawnSync(binary, ['console.log(embedVars.nön_ascıı)']) + .stdout.toString().trim(), + '🏳️‍🌈'); + +assert.strictEqual( + child_process.spawnSync(binary, ['console.log(42)']) + .stdout.toString().trim(), + '42'); + +assert.strictEqual( + child_process.spawnSync(binary, ['throw new Error()']).status, + 1); + +assert.strictEqual( + child_process.spawnSync(binary, ['process.exitCode = 8']).status, + 8); + + +const fixturePath = JSON.stringify(fixtures.path('exit.js')); +assert.strictEqual( + child_process.spawnSync(binary, [`require(${fixturePath})`, 92]).status, + 92); + +assert.strictEqual( + child_process.spawnSync(binary, ['function callMe(text) { return text + " you"; }']) + .stdout.toString().trim(), + 'called you'); + +assert.strictEqual( + child_process.spawnSync(binary, ['function waitMe(text, cb) { setTimeout(() => cb(text + " you"), 1); }']) + .stdout.toString().trim(), + 'waited you'); + +assert.strictEqual( + child_process.spawnSync(binary, + ['function waitPromise(text)' + + '{ return new Promise((res) => setTimeout(() => res(text + " with cheese"), 1)); }']) + .stdout.toString().trim(), + 'waited with cheese'); + +assert.strictEqual( + child_process.spawnSync(binary, + ['function waitPromise(text)' + + '{ return new Promise((res, rej) => setTimeout(() => rej(text + " without cheese"), 1)); }']) + .stdout.toString().trim(), + 'waited without cheese'); + +assert.match( + child_process.spawnSync(binary, + ['0syntax_error']) + .stderr.toString().trim(), + /SyntaxError: Invalid or unexpected token/); diff --git a/test/parallel/test-http-client-response-timeout.js b/test/parallel/test-http-client-response-timeout.js index 7e44d83a831143..6f9b490e551e66 100644 --- a/test/parallel/test-http-client-response-timeout.js +++ b/test/parallel/test-http-client-response-timeout.js @@ -9,6 +9,6 @@ server.listen(common.mustCall(() => { http.get({ port: server.address().port }, common.mustCall((res) => { res.on('timeout', common.mustCall(() => req.destroy())); res.setTimeout(1); - server.close(); + setTimeout(() => server.close(), 2); })); }));