diff --git a/.clang-format b/.clang-format index 4aad29c328abd4..d53efb21fce1ca 100644 --- a/.clang-format +++ b/.clang-format @@ -79,6 +79,10 @@ JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: '' MacroBlockEnd: '' +Macros: + - "NODE_ENUM(a,b) = enum class b" + - "NODE_ENUM_FLAGS(a,b) = enum class b" + - "NODE_ENUM_ITEM (a,b) = b" MaxEmptyLinesToKeep: 1 NamespaceIndentation: None ObjCBlockIndentWidth: 2 diff --git a/doc/api/embedding.md b/doc/api/embedding.md index 114f1128af0a42..63c27f8073ce97 100644 --- a/doc/api/embedding.md +++ b/doc/api/embedding.md @@ -27,8 +27,8 @@ The full code can be found [in the Node.js source tree][embedtest.cc]. Node.js requires some per-process state management in order to run: -* Arguments parsing for Node.js [CLI options][], -* V8 per-process requirements, such as a `v8::Platform` instance. +- Arguments parsing for Node.js [CLI options][], +- V8 per-process requirements, such as a `v8::Platform` instance. The following example shows how these can be set up. Some class names are from the `node` and `v8` C++ namespaces, respectively. @@ -167,8 +167,1327 @@ int RunNodeInstance(MultiIsolatePlatform* platform, } ``` +# C embedder API + + + +While Node.js provides an extensive C++ embedding API that can be used from C++ +applications, the C-based API is useful when Node.js is embedded as a shared +libnode library into C++ or non-C++ applications. + +The C embedding API is defined in [src/node_embedding_api.h][] in the Node.js +source tree. + +## API design overview + +One of the goals for the C based embedder API is to be ABI stable. It means that +applications must be able to use newer libnode versions without recompilation. +The following design principles are targeting to achieve that goal. + +- Follow the best practices for the [node-api][] design and build on top of + the [node-api][]. +- Use the [Builder pattern][] as the way to configure the global platform and + the instance environments. It enables us incrementally add new flags, + settings, callbacks, and behavior without changing the existing + functions. +- Use the API version as a way to add new or change existing behavior. +- Make the common scenarios simple and the complex scenarios possible. In some + cases we may provide some "shortcut" APIs that combine calls to multiple other + APIs to simplify some common scenarios. + +The C embedder API has the four major API function groups: + +- **Global platform APIs.** These are the global settings and initializations + that are done once per process. They include parsing CLI arguments, setting + the V8 platform, V8 thread pool, and initializing V8. +- **Runtime instance APIs.** This is the main Node.js working environment that + combines V8 `Isolate`, `Context`, and a UV loop. It is used run the Node.js + JavaScript code and modules. Each process we may have one or more runtime + environments. Its behavior is based on the global platform API. +- **Event loop APIs.** The event loop is one of the key concepts of Node.js. It + handles IO callbacks, timer jobs, and Promise continuations. These APIs are + related to a specific Node.js runtime instance and control handling of the + event loop tasks. The event loop tasks can be executed in the chosen thread. + The API controls how many tasks executed at one: all, one-by-one, or until a + predicate becomes false. We can also choose if the even loop must block the + current thread while waiting for a new task to arrive. +- **JavaScript/Native interop APIs.** They rely on the existing [node-api][] + set of functions. The embedding APIs provide access to functions that + retrieve or create `napi_env` instances related to a runtime instance. + +## API reference + +The C embedder API is split up by the four major groups described above. + +### Global platform APIs + +#### Data types + +##### `node_embedding_platform` + + + +> Stability: 1 - Experimental + +This is an opaque pointer that represents a Node.js platform instance. +Node.js allows only a single platform instance per process. + +##### `node_embedding_platform_config` + + + +> Stability: 1 - Experimental + +This is an opaque pointer that represents a Node.js platform configuration +instance. + +##### `node_embedding_status` + + + +> Stability: 1 - Experimental + +The status code returned from the C Node.js embedding APIs. + +```c +typedef enum { + node_embedding_status_ok = 0, + node_embedding_status_generic_error = 1, + node_embedding_status_null_arg = 2, + node_embedding_status_bad_arg = 3, + node_embedding_status_out_of_memory = 4, + node_embedding_status_error_exit_code = 512, +} node_embedding_status; +``` + +- `node_embedding_status_ok` - No issues. +- `node_embedding_status_generic_error` - Generic error code. +- `node_embedding_status_null_arg` - One of non-optional arguments passed as + NULL value. +- `node_embedding_status_bad_arg` - One of the arguments has wrong value. +- `node_embedding_status_out_of_memory` - Failed to allocate memory. +- `node_embedding_status_error_exit_code` - A bit flag added to the Node.js exit + code value if the error status is associated with an error code. + +##### `node_embedding_platform_flags` + + + +> Stability: 1 - Experimental + +Flags are used to initialize a Node.js platform instance. + +```c +typedef enum { + node_embedding_platform_flags_none = 0, + node_embedding_platform_flags_enable_stdio_inheritance = 1 << 0, + node_embedding_platform_flags_disable_node_options_env = 1 << 1, + node_embedding_platform_flags_disable_cli_options = 1 << 2, + node_embedding_platform_flags_no_icu = 1 << 3, + node_embedding_platform_flags_no_stdio_initialization = 1 << 4, + node_embedding_platform_flags_no_default_signal_handling = 1 << 5, + node_embedding_platform_flags_no_init_openssl = 1 << 8, + node_embedding_platform_flags_no_parse_global_debug_variables = 1 << 9, + node_embedding_platform_flags_no_adjust_resource_limits = 1 << 10, + node_embedding_platform_flags_no_use_large_pages = 1 << 11, + node_embedding_platform_flags_no_print_help_or_version_output = 1 << 12, + node_embedding_platform_flags_generate_predictable_snapshot = 1 << 14, +} node_embedding_platform_flags; +``` + +These flags match to the C++ `node::ProcessInitializationFlags` and control the +Node.js platform initialization. + +- `node_embedding_platform_flags_none` - The default flags. +- `node_embedding_platform_flags_enable_stdio_inheritance` - Enable `stdio` + inheritance, which is disabled by default. This flag is also implied by the + `node_embedding_platform_flags_no_stdio_initialization`. +- `node_embedding_platform_flags_disable_node_options_env` - Disable reading the + `NODE_OPTIONS` environment variable. +- `node_embedding_platform_flags_disable_cli_options` - Do not parse CLI + options. +- `node_embedding_platform_flags_no_icu` - Do not initialize ICU. +- `node_embedding_platform_flags_no_stdio_initialization` - Do not modify + `stdio` file descriptor or TTY state. +- `node_embedding_platform_flags_no_default_signal_handling` - Do not register + Node.js-specific signal handlers and reset other signal handlers to + default state. +- `node_embedding_platform_flags_no_init_openssl` - Do not initialize OpenSSL + config. +- `node_embedding_platform_flags_no_parse_global_debug_variables` - Do not + initialize Node.js debugging based on environment variables. +- `node_embedding_platform_flags_no_adjust_resource_limits` - Do not adjust OS + resource limits for this process. +- `node_embedding_platform_flags_no_use_large_pages` - Do not map code segments + into large pages for this process. +- `node_embedding_platform_flags_no_print_help_or_version_output` - Skip + printing output for `--help`, `--version`, `--v8-options`. +- `node_embedding_platform_flags_generate_predictable_snapshot` - Initialize the + process for predictable snapshot generation. + +#### Callback types + +##### `node_embedding_handle_error_callback` + + + +> Stability: 1 - Experimental + +```c +typedef node_embedding_status(NAPI_CDECL* node_embedding_data_release_callback)( + void* data); +``` + +Function pointer type to release data. + +The callback parameters: + +- `[in] data`: The data to be released. + +##### `node_embedding_platform_configure_callback` + + + +> Stability: 1 - Experimental + +```c +typedef node_embedding_status( + NAPI_CDECL* node_embedding_platform_configure_callback)( + void* cb_data, + node_embedding_platform_config platform_config); +``` + +Function pointer type for user-provided native function that configures the +platform. + +The callback parameters: + +- `[in] cb_data`: The data associated with the callback. +- `[in] platform_config`: The platform configuration. + +#### Functions + +##### `node_embedding_last_error_message_get` + + + +> Stability: 1 - Experimental + +Gets the last error message for the current thread. + +```c +const char* NAPI_CDECL +node_embedding_last_error_message_get(); +``` + +Returns a pointer to a non-null string that contains the last error message +in the current thread or to empty string if there were no error messages. + +The value of the string should be copied or processed immediately since it can +be overridden by upcoming embedding API calls. + +##### `node_embedding_last_error_message_set` + + + +> Stability: 1 - Experimental + +Sets the last error message for the current thread. + +```c +const char* NAPI_CDECL +node_embedding_last_error_message_set(const char* message); +``` + +- `[in] message`: The new last error message for the current thread. + +The stored value can be retrieved by the `node_embedding_last_error_message_get` +function. + +The last error message can be removed by passing null to this function. + +##### `node_embedding_main_run` + + + +> Stability: 1 - Experimental + +Runs Node.js main function as if it is invoked from Node.js CLI. +It allows to customize the platform and the runtime behavior. + +```c +node_embedding_status NAPI_CDECL node_embedding_main_run( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); +``` + +- `[in] embedding_api_version`: The version of the embedding API. +- `[in] argc`: Number of items in the `argv` array. +- `[in] argv`: CLI arguments as an array of zero terminated strings. +- `[in] configure_platform`: Optional. A callback to configure the platform. +- `[in] configure_platform_data`: Optional. Additional data for the + `configure_platform` callback. +- `[in] configure_runtime`: Optional. A callback to configure the main runtime. +- `[in] configure_runtime_data`: Optional. Additional data for the + `configure_runtime` callback. + +Returns `node_embedding_status_ok` if there were no issues. + +In case of early return when Node.js prints help, version info, or V8 options, +the output text can be accessed by calling the +`node_embedding_last_error_message_get` while the returned status is +`node_embedding_status_ok`. + +##### `node_embedding_platform_create` + + + +> Stability: 1 - Experimental + +Creates new Node.js platform instance. + +```c +node_embedding_status NAPI_CDECL node_embedding_platform_create( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_platform* result); +``` + +- `[in] embedding_api_version`: The version of the embedding API. +- `[in] argc`: Number of items in the `argv` array. +- `[in] argv`: CLI arguments as an array of zero terminating strings. +- `[in] configure_platform`: Optional. A callback to configure the platform. +- `[in] configure_platform_data`: Optional. Additional data for the + `configure_platform` callback. +- `[out] result`: New Node.js platform instance. + +Returns `node_embedding_status_ok` if there were no issues. + +Node.js allows only a single platform instance per process. + +In case of early return when Node.js prints help, version info, or V8 options, +the output text can be accessed by calling the +`node_embedding_last_error_message_get` while the returned status is +`node_embedding_status_ok` and the `result` is null. + +##### `node_embedding_platform_delete` + + + +> Stability: 1 - Experimental + +Deletes Node.js platform instance. + +```c +node_embedding_status NAPI_CDECL +node_embedding_platform_delete(node_embedding_platform platform); +``` + +- `[in] platform`: The Node.js platform instance to delete. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_platform_config_set_flags` + + + +> Stability: 1 - Experimental + +Sets the Node.js platform instance flags. + +```c +node_embedding_status NAPI_CDECL +node_embedding_platform_config_set_flags( + node_embedding_platform_config platform_config, + node_embedding_platform_flags flags); +``` + +- `[in] platform_config`: The Node.js platform configuration. +- `[in] flags`: The platform flags that control the platform behavior. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_platform_get_parsed_args` + + + +> Stability: 1 - Experimental + +Gets the parsed list of non-Node.js arguments. + +```c +node_embedding_status NAPI_CDECL +node_embedding_platform_get_parsed_args( + node_embedding_platform platform, + int32_t* args_count, + const char** args[], + int32_t* runtime_args_count, + const char** runtime_args[]); +``` + +- `[in] platform`: The Node.js platform instance. +- `[out] args_count`: Optional. Receives the non-Node.js argument count. +- `[out] args`: Optional. Receives a pointer to an array of zero-terminated + strings with the non-Node.js arguments. +- `[out] runtime_args_count`: Optional. Receives the Node.js argument count. +- `[out] runtime_args`: Optional. Receives a pointer to an array of + zero-terminated strings with the Node.js arguments. + +Returns `node_embedding_status_ok` if there were no issues. + +### Runtime instance APIs + +#### Data types + +##### `node_embedding_runtime` + + + +> Stability: 1 - Experimental + +This is an opaque pointer that represents a Node.js runtime instance. +It wraps up the C++ `node::Environment`. +There can be one or more runtime instances in the process. + +##### `node_embedding_runtime_config` + + + +> Stability: 1 - Experimental + +This is an opaque pointer that represents a Node.js runtime configuration. + +##### `node_embedding_runtime_flags` + + + +> Stability: 1 - Experimental + +Flags are used to initialize a Node.js runtime instance. + +```c +typedef enum { + node_embedding_runtime_flags_none = 0, + node_embedding_runtime_flags_default = 1 << 0, + node_embedding_runtime_flags_owns_process_state = 1 << 1, + node_embedding_runtime_flags_owns_inspector = 1 << 2, + node_embedding_runtime_flags_no_register_esm_loader = 1 << 3, + node_embedding_runtime_flags_track_unmanaged_fds = 1 << 4, + node_embedding_runtime_flags_hide_console_windows = 1 << 5, + node_embedding_runtime_flags_no_native_addons = 1 << 6, + node_embedding_runtime_flags_no_global_search_paths = 1 << 7, + node_embedding_runtime_flags_no_browser_globals = 1 << 8, + node_embedding_runtime_flags_no_create_inspector = 1 << 9, + node_embedding_runtime_flags_no_start_debug_signal_handler = 1 << 10, + node_embedding_runtime_flags_no_wait_for_inspector_frontend = 1 << 11 +} node_embedding_runtime_flags; +``` + +These flags match to the C++ `node::EnvironmentFlags` and control the +Node.js runtime initialization. + +- `node_embedding_runtime_flags_none` - No flags set. +- `node_embedding_runtime_flags_default` - Use the default behavior for + Node.js instances. +- `node_embedding_runtime_flags_owns_process_state` - Controls whether this + runtime is allowed to affect per-process state (e.g. cwd, process title, + uid, etc.). This is set when using `node_embedding_runtime_flags_default`. +- `node_embedding_runtime_flags_owns_inspector` - Set if this runtime instance + is associated with the global inspector handling code (i.e. listening + on `SIGUSR1`). + This is set when using `node_embedding_runtime_flags_default`. +- `node_embedding_runtime_flags_no_register_esm_loader` - Set if Node.js should + not run its own esm loader. This is needed by some embedders, because it's + possible for the Node.js esm loader to conflict with another one in an + embedder environment, e.g. Blink's in Chromium. +- `node_embedding_runtime_flags_track_unmanaged_fds` - Set this flag to make + Node.js track "raw" file descriptors, i.e. managed by `fs.open()` and + `fs.close()`, and close them during the `node_embedding_runtime_delete` call. +- `node_embedding_runtime_flags_hide_console_windows` - Set this flag to force + hiding console windows when spawning child processes. This is usually used + when embedding Node.js in GUI programs on Windows. +- `node_embedding_runtime_flags_no_native_addons` - Set this flag to disable + loading native addons via `process.dlopen`. This runtime flag is especially + important for worker threads so that a worker thread can't load a native addon + even if `execArgv` is overwritten and `--no-addons` is not specified but was + specified for this runtime instance. +- `node_embedding_runtime_flags_no_global_search_paths` - Set this flag to + disable searching modules from global paths like `$HOME/.node_modules` and + `$NODE_PATH`. This is used by standalone apps that do not expect to have their + behaviors changed because of globally installed modules. +- `node_embedding_runtime_flags_no_browser_globals` - Do not export browser + globals like setTimeout, console, etc. +- `node_embedding_runtime_flags_no_create_inspector` - Controls whether or not + the runtime should call `V8Inspector::create()`. This control is needed by + embedders who may not want to initialize the V8 inspector in situations where + one has already been created, e.g. Blink's in Chromium. +- `node_embedding_runtime_flags_no_start_debug_signal_handler` - Controls + whether or not the `InspectorAgent` for this runtime should call + `StartDebugSignalHandler`. This control is needed by embedders who may not + want to allow other processes to start the V8 inspector. +- `node_embedding_runtime_flags_no_wait_for_inspector_frontend` - Controls + whether the `InspectorAgent` created for this runtime waits for Inspector + frontend events during the runtime creation. It's used to call + `node::Stop(env)` on a Worker thread that is waiting for the events. + +#### Callback types + +##### `node_embedding_runtime_configure_callback` + + + +> Stability: 1 - Experimental + +```c +typedef node_embedding_status( + NAPI_CDECL* node_embedding_runtime_configure_callback)( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config); +``` + +Function pointer type for user-provided native function that configures +the runtime. + +The callback parameters: + +- `[in] cb_data`: The data associated with this callback. +- `[in] platform`: The platform for the runtime. +- `[in] runtime_config`: The runtime configuration. + +##### `node_embedding_runtime_preload_callback` + + + +> Stability: 1 - Experimental + +```c +typedef void(NAPI_CDECL* node_embedding_runtime_preload_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require); +``` + +Function pointer type for user-provided native function that is called before +the runtime loads any JavaScript code. + +The callback parameters: + +- `[in] cb_data`: The data associated with this callback. +- `[in] runtime`: The runtime owning the callback. +- `[in] env`: Node-API environment. +- `[in] process`: The Node.js `process` object. +- `[in] require`: The internal `require` function. + +##### `node_embedding_runtime_loading_callback` + + + +> Stability: 1 - Experimental + +```c +typedef napi_value(NAPI_CDECL* node_embedding_runtime_loading_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs); +``` + +Function pointer type for user-provided native function that is called when the +runtime loads the main JavaScript and starts the runtime execution. + +The callback parameters: + +- `[in] cb_data`: The data associated with this callback. +- `[in] runtime`: The runtime owning the callback. +- `[in] env`: Node-API environment. +- `[in] process`: The Node.js `process` object. +- `[in] require`: The internal `require` function. +- `[in] run_cjs`: The internal function that runs Common JS modules. + +It returns `napi_value` with the result of the main script run. + +##### `node_embedding_runtime_loaded_callback` + + + +> Stability: 1 - Experimental + +```c +typedef void(NAPI_CDECL* node_embedding_runtime_loaded_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value load_result); +``` + +Function pointer type for user-provided native function that is called after +the runtime loaded the main JavaScript. It can help handing the loading result. + +The callback parameters: + +- `[in] cb_data`: The data associated with this callback. +- `[in] runtime`: The runtime owning the callback. +- `[in] env`: Node-API environment. +- `[in] load_result`: The result of loading of the main script. + +##### `node_embedding_module_initialize_callback` + + + +> Stability: 1 - Experimental + +```c +typedef napi_value(NAPI_CDECL* node_embedding_module_initialize_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + const char* module_name, + napi_value exports); +``` + +Function pointer type for initializing linked native module that can be defined +in the embedder executable. + +The callback parameters: + +- `[in] cb_data`: The user data associated with this callback. +- `[in] runtime`: The runtime owning the callback. +- `[in] env`: Node API environment. +- `[in] module_name`: Name of the module. +- `[in] exports`: The `exports` module object. + +All module exports must be added as properties to the `exports` object. +As an alternative a new object or another value can be created in the callback +and returned. + +#### Functions + +##### `node_embedding_runtime_run` + + + +> Stability: 1 - Experimental + +Runs Node.js runtime environment. It allows the runtime customization. + +```c +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_runtime_run( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); +``` + +- `[in] platform`: The platform instance for the runtime. +- `[in] configure_runtime`: A callback to configure the runtime. +- `[in] configure_runtime_data`: Additional data for the `configure_runtime` + callback. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_create` + + + +> Stability: 1 - Experimental + +Creates new Node.js runtime instance. + +```c +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_runtime_create( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data, + node_embedding_runtime* result); +``` + +- `[in] platform`: An initialized Node.js platform instance. +- `[in] configure_runtime`: A callback to configure the runtime. +- `[in] configure_runtime_data`: Additional data for the `configure_runtime` + callback. +- `[out] result`: Upon return has a new Node.js runtime instance. + +Returns `node_embedding_status_ok` if there were no issues. + +Creates new Node.js runtime instance for the provided platform instance. + +##### `node_embedding_runtime_delete` + + + +> Stability: 1 - Experimental + +Deletes Node.js runtime instance. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_delete(node_embedding_runtime runtime); +``` + +- `[in] runtime`: The Node.js runtime instance to delete. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_config_set_node_api_version` + + + +> Stability: 1 - Experimental + +Sets the Node.js runtime instance flags. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_node_api_version( + node_embedding_runtime_config runtime_config, + int32_t node_api_version); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] node_api_version`: The Node-API version to use by the runtime. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_config_set_flags` + + + +> Stability: 1 - Experimental + +Sets the Node.js runtime instance flags. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_flags( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_flags flags); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] flags`: The runtime flags that control the runtime behavior. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_config_set_args` + +Sets the non-Node.js arguments for the Node.js runtime instance. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_args( + node_embedding_runtime_config runtime_config, + int32_t argc, + const char* argv[], + int32_t runtime_argc, + const char* runtime_argv[]); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] argc`: Number of items in the `argv` array. +- `[in] argv`: non-Node.js arguments as an array of zero terminating strings. +- `[in] runtime_argc`: Number of items in the `runtime_argv` array. +- `[in] runtime_argv`: Node.js runtime arguments as an array of zero + terminated strings. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_config_on_preload` + + + +> Stability: 1 - Experimental + +Sets a preload callback to call before Node.js runtime instance is loaded. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_preload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback preload, + void* preload_data, + node_embedding_data_release_callback release_preload_data); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] preload`: The preload callback to be called before Node.js runtime + instance is loaded. +- `[in] preload_data`: Optional. The preload callback data that will be + passed to the `preload` callback. +- `[in] release_preload_data`: Optional. A callback to call to delete + the `preload_data` when the runtime is destroyed. + +Returns `node_embedding_status_ok` if there were no issues. + +This callback is called for the Node.js environment associated with the runtime +and before each worker thread start. + +##### `node_embedding_runtime_config_on_loading` + + + +> Stability: 1 - Experimental + +Sets a callback responsible for the loading of the main script by Node.js. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_loading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback run_load, + void* load_data, + node_embedding_data_release_callback release_load_data); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] run_load`: The callback to be called when Node.js runtime loads + the main script. +- `[in] load_data`: Optional. The data associated with the `run_load` callback. +- `[in] release_load_data`: Optional. A callback to release the `load_data` + when the runtime does not need it anymore. + +Returns `node_embedding_status_ok` if there were no issues. + +This callback is only called for a runtime only once. It is not called for the +runtime worker threads. The callback must take care about the thread safety. + +##### `node_embedding_runtime_config_on_loaded` + + + +> Stability: 1 - Experimental + +Sets a callback that helps processing result from loading the main script +for the runtime. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_loaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_loaded, + void* handle_loaded_data, + node_embedding_data_release_callback release_handle_loaded_data); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] handle_loaded`: The callback to be called after Node.js runtime + finished loading the main script. +- `[in] handle_loaded_data`: Optional. The data associated with the + `handle_loaded` callback. +- `[in] release_handle_loaded_data`: Optional. A callback to release the + `handle_loaded_data` when the runtime does not need it anymore. + +Returns `node_embedding_status_ok` if there were no issues. + +This callback is only called for a runtime only once. It is not called for the +runtime worker threads. + +##### `node_embedding_runtime_config_add_module` + + + +> Stability: 1 - Experimental + +Adds a linked module for the Node.js runtime instance. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_add_module( + node_embedding_runtime_config runtime_config, + const char* module_name, + node_embedding_module_initialize_callback init_module, + void* init_module_data, + node_embedding_data_release_callback release_init_module_data, + int32_t module_node_api_version); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] module_name`: The name of the module. +- `[in] init_module`: Module initialization callback. It is called for the + main and worker threads. The callback must take care about the thread safety. +- `[in] init_module_data`: Optional. The data for the `init_module` callback. +- `[in] release_init_module_data`: Optional. A callback to release the + `init_module_data` when the runtime does not need it anymore. +- `[in] module_node_api_version`: The Node API version used by the module. + +Returns `node_embedding_status_ok` if there were no issues. + +The registered module can be accessed in JavaScript as +`process._linkedBinding(module_name)` in the main JS and in the related +worker threads. + +##### `node_embedding_runtime_user_data_set` + + + +> Stability: 1 - Experimental + +Associates or clears user data for the Node.js runtime. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_user_data_set( + node_embedding_runtime runtime, + void* user_data, + node_embedding_data_release_callback release_user_data); +``` + +- `[in] runtime`: The Node.js runtime. +- `[in] user_data`: The user data to associate with the runtime. It it null + then it clears previously associated data without calling its + `release_user_data` callback. +- `[in] release_user_data`: Optional. A callback to release the + `user_data` when the runtime destructor is called. + +Returns `node_embedding_status_ok` if there were no issues. + +Note that this method does not call `release_user_data` for the previously +assigned data. + +##### `node_embedding_runtime_user_data_get` + + + +> Stability: 1 - Experimental + +Gets the user data associated with the Node.js runtime. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_user_data_get( + node_embedding_runtime runtime, + void** user_data); +``` + +- `[in] runtime`: The Node.js runtime. +- `[out] user_data`: The user data associated with the runtime. + +Returns `node_embedding_status_ok` if there were no issues. + +### Event loop APIs + +#### Callback types + +##### `node_embedding_task_post_callback` + + + +> Stability: 1 - Experimental + +```c +typedef node_embedding_status(NAPI_CDECL* node_embedding_task_post_callback)( + void* cb_data, + node_embedding_task_run_callback run_task, + void* task_data, + node_embedding_data_release_callback release_task_data, + bool* is_posted); +``` + +Function pointer type for the task runner callback. +The task runner enables running the JavaScript and Node.js event loop in an +existing dispatcher queue or UI event loop instead of the current thread. + +The callback parameters: + +- `[in] cb_data`: The data associated with the task runner that the post + callback calls. +- `[in] run_task`: The task to run in the task runner. +- `[in] task_data`: Optional. The data associated with the task. +- `[in] release_task_data`: Optional. The callback to call when the `task_data` + is not needed anymore by the task runner. +- `[out] is_posted`: Optional. Returns `true` if the task was successfully + posted for execution by the task runner. It should return `false` if the task + runner is already shutdown. + +The callback returns `node_embedding_status_ok` if there were no errors. + +##### `node_embedding_task_run_callback` + + + +> Stability: 1 - Experimental + +```c +typedef node_embedding_status(NAPI_CDECL* node_embedding_task_run_callback)( + void* cb_data); +``` + +Function pointer type for the task to be run in the task runner. + +The callback parameters: + +- `[in] cb_data`: The data associated with the callback. + +The callback returns `node_embedding_status_ok` if there were no errors. + +#### Functions + +##### `node_embedding_runtime_config_set_task_runner` + + + +> Stability: 1 - Experimental + +Sets the task runner for the Node.js runtime. +It enables running Node.js event loop as a part of application UI event loop or +a dispatcher queue. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_task_runner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + void* post_task_data, + node_embedding_data_release_callback release_post_task_data); +``` + +- `[in] runtime_config`: The Node.js runtime configuration. +- `[in] post_task`: The callback that is called by the Node.js runtime to run + event loop iteration. +- `[in] post_task_data`: The data associated with the callback. +- `[in] release_post_task_data`: The callback to be called to release the + `post_task_data` when the task runner is not needed any more. + +Returns `node_embedding_status_ok` if there were no issues. + +This function enables running Node.js runtime event loop from the host +application UI event loop or a dispatcher queue. Internally it creates a thread +that observes new events to process by the event loop. Then, it posts a task +to run the event loop once by using the `post_task` callback and passing there +the `post_task_data`. The `post_task` call is made from the helper observer +queue. The goal of the `pots_task` is to schedule the task in the UI queue or +the dispatcher queue used as a task runner. + +Note that running the event loop in a dedicated thread is more efficient. +This method can be used in scenarios when we must combine the Node.js event +loop processing with the application UI event loop. It may be required when +the application wants to run JavaScript code from the UI thread to interop with +the native UI components. + +Using the task runner is an alternative to calling the +`node_embedding_runtime_event_loop_run` function that processes all Node.js +tasks in the current thread. + +The `post_task_data` may outlive the lifetime of the runtime it is associated +with and released much later. + +##### `node_embedding_runtime_event_loop_run` + + + +> Stability: 1 - Experimental + +Runs Node.js runtime instance event loop in the default mode. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run( + node_embedding_runtime runtime); +``` + +- `[in] runtime`: The Node.js runtime instance. + +Returns `node_embedding_status_ok` if there were no issues. + +The function processes all Node.js tasks and emits `beforeExit` event. +If new tasks are added, then it completes them and raises the `beforeExit` event +again. The process repeats until the `beforeExit` event stops producing new +tasks. After that it emits the `exit` event and ends the event loop processing. +No new tasks can be added in the `exit` event handler or after that. + +##### `node_embedding_runtime_event_loop_terminate` + + + +> Stability: 1 - Experimental + +Terminates the Node.js runtime instance event loop. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_terminate( + node_embedding_runtime runtime); +``` + +- `[in] runtime`: The Node.js runtime instance. + +Returns `node_embedding_status_ok` if there were no issues. + +The event loop is stopped and cannot be resumed. +No `beforeExit` or `exit` events are not going to be raised. + +##### `node_embedding_runtime_event_loop_run_once` + + + +> Stability: 1 - Experimental + +Executes a single event loop iteration. It may block the current thread while +waiting for the new events from the asynchronous operations. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run_once( + node_embedding_runtime runtime, + bool* has_more_work); +``` + +- `[in] runtime`: The Node.js runtime instance. +- `[out] has_more_work`: Optional. Returns true if the event loop has more + work to do. + +Returns `node_embedding_status_ok` if there were no issues. + +##### `node_embedding_runtime_event_loop_run_no_wait` + + + +> Stability: 1 - Experimental + +Executes a single event loop iteration. It does not block the current thread +while to wait on pending asynchronous operations. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run_no_wait( + node_embedding_runtime runtime, + bool* has_more_work); +``` + +- `[in] runtime`: The Node.js runtime instance. +- `[out] has_more_work`: Optional. Returns true if the event loop has more + work to do. + +Returns `node_embedding_status_ok` if there were no issues. + +### JavaScript/Native interop APIs + +#### Data types + +##### `node_embedding_node_api_scope` + + + +> Stability: 1 - Experimental + +This is an opaque pointer for the Node-API scope where the provided `napi_env` +is valid. + +#### Callback types + +##### `node_embedding_run_node_api_callback` + + + +> Stability: 1 - Experimental + +```c +typedef void(NAPI_CDECL* node_embedding_run_node_api_callback)( + void* cb_data, + napi_env env); +``` + +Function pointer type for callback that invokes Node-API code. + +The callback parameters: + +- `[in] cb_data`: The user data associated with this callback. +- `[in] env`: Node-API environment. + +#### Functions + +##### `node_embedding_runtime_node_api_run` + + + +> Stability: 1 - Experimental + +Runs Node-API code. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_run( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + void* run_node_api_data); +``` + +- `[in] runtime`: The Node.js embedding_runtime instance. +- `[in] run_node_api`: The callback to invoke Node-API code. +- `[in] run_node_api_data`: Optional. The data associated with the + `run_node_api` callback. + +Returns `node_embedding_status_ok` if there were no issues. + +The function invokes the callback that runs Node-API code in the Node-API scope. +It triggers the uncaught exception handler if there were any unhandled +JavaScript errors. + +##### `node_embedding_runtime_node_api_scope_open` + + + +> Stability: 1 - Experimental + +Opens scope to run Node-API code. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_scope_open( + node_embedding_runtime runtime, + node_embedding_node_api_scope* node_api_scope, + napi_env* env); +``` + +- `[in] runtime`: The Node.js embedding_runtime instance. +- `[out] node_api_scope`: The opened Node-API scope. +- `[out] env`: The Node-API environment that can be used in the scope. + +Returns `node_embedding_status_ok` if there were no issues. + +The function opens up V8 Isolate, V8 handle, and V8 context scopes where it is +safe to run the Node-API environment. + +Using the `node_embedding_runtime_node_api_scope_open` and `node_embedding_runtime_node_api_scope_close` is an alternative to the +`node_embedding_runtime_node_api_run` call. + +##### `node_embedding_runtime_node_api_scope_close` + + + +> Stability: 1 - Experimental + +Closes the Node-API invocation scope. + +```c +node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_scope_close( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope); +``` + +- `[in] runtime`: The Node.js embedding_runtime instance. +- `[in] node_api_scope`: The Node-API scope to close. + +Returns `node_embedding_status_ok` if there were no issues. + +The function closes the V8 Isolate, V8 handle, and V8 context scopes. +Then, it triggers the uncaught exception handler if there were any +unhandled errors. + +## Examples + +The examples listed here are part of the Node.js +[embedding unit tests][test_embedding]. + +```c + // TODO: add example here. +``` + +[Builder pattern]: https://en.wikipedia.org/wiki/Builder_pattern [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 +[test_embedding]: https://github.com/nodejs/node/blob/HEAD/test/embedding +[node-api]: n-api.md [src/node.h]: https://github.com/nodejs/node/blob/HEAD/src/node.h diff --git a/doc/api/index.md b/doc/api/index.md index 3ca2a91cd8fbe5..59950391342b84 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -16,7 +16,7 @@ * [Buffer](buffer.md) * [C++ addons](addons.md) * [C/C++ addons with Node-API](n-api.md) -* [C++ embedder API](embedding.md) +* [C/C++ embedder API](embedding.md) * [Child processes](child_process.md) * [Cluster](cluster.md) * [Command-line options](cli.md) diff --git a/node.gyp b/node.gyp index 5aed08ee4164b8..1ddfe710c119ac 100644 --- a/node.gyp +++ b/node.gyp @@ -110,6 +110,7 @@ 'src/node_debug.cc', 'src/node_dir.cc', 'src/node_dotenv.cc', + 'src/node_embedding_api.cc', 'src/node_env_var.cc', 'src/node_errors.cc', 'src/node_external_reference.cc', @@ -223,6 +224,7 @@ 'src/module_wrap.h', 'src/node.h', 'src/node_api.h', + 'src/node_api_internals.h', 'src/node_api_types.h', 'src/node_binding.h', 'src/node_blob.h', @@ -234,6 +236,8 @@ 'src/node_debug.h', 'src/node_dir.h', 'src/node_dotenv.h', + 'src/node_embedding_api.h', + 'src/node_embedding_api_cpp.h', 'src/node_errors.h', 'src/node_exit_code.h', 'src/node_external_reference.h', @@ -1260,6 +1264,23 @@ 'sources': [ 'src/node_snapshot_stub.cc', 'test/embedding/embedtest.cc', + 'test/embedding/embedtest_c_api_common.c', + 'test/embedding/embedtest_c_api_common.h', + 'test/embedding/embedtest_c_api_env.c', + 'test/embedding/embedtest_c_api_modules.c', + 'test/embedding/embedtest_c_api_preload.c', + 'test/embedding/embedtest_c_api_run_main.c', + 'test/embedding/embedtest_c_api_threading.c', + 'test/embedding/embedtest_c_api.c', + 'test/embedding/embedtest_c_cpp_api_common.cc', + 'test/embedding/embedtest_c_cpp_api_common.h', + 'test/embedding/embedtest_c_cpp_api_env.cc', + 'test/embedding/embedtest_c_cpp_api_modules.cc', + 'test/embedding/embedtest_c_cpp_api_preload.cc', + 'test/embedding/embedtest_c_cpp_api_run_main.cc', + 'test/embedding/embedtest_c_cpp_api_threading.cc', + 'test/embedding/embedtest_c_cpp_api.cc', + 'test/embedding/embedtest_main.cc', ], 'conditions': [ diff --git a/src/api/embed_helpers.cc b/src/api/embed_helpers.cc index 34de89a8dc0398..ab24d32ba3884d 100644 --- a/src/api/embed_helpers.cc +++ b/src/api/embed_helpers.cc @@ -19,7 +19,24 @@ using v8::TryCatch; namespace node { -Maybe SpinEventLoopInternal(Environment* env) { +enum class SpinEventLoopCleanupMode { + kNormal, + kNoCleanup, +}; + +/** + * Spin the event loop with the provided run_mode. + * For the UV_RUN_DEFAULT it will spin it 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. If the cleanup_mode is kNoCleanup, + * then the environment will not be cleaned up. + * For the UV_RUN_ONCE and UV_RUN_NOWAIT modes, the cleanup_mode is ignored and + * the environment is not cleaned up. + */ +template < + uv_run_mode run_mode = UV_RUN_DEFAULT, + SpinEventLoopCleanupMode cleanup_mode = SpinEventLoopCleanupMode::kNormal> +Maybe SpinEventLoopInternalImpl(Environment* env) { CHECK_NOT_NULL(env); MultiIsolatePlatform* platform = GetMultiIsolatePlatform(env); CHECK_NOT_NULL(platform); @@ -31,62 +48,93 @@ Maybe SpinEventLoopInternal(Environment* env) { if (env->is_stopping()) return Nothing(); - env->set_trace_sync_io(env->options()->trace_sync_io); { - bool more; + env->set_trace_sync_io(env->options()->trace_sync_io); + auto clear_set_trace_sync_io = + OnScopeLeave([env] { env->set_trace_sync_io(false); }); + env->performance_state()->Mark( node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); + auto mark_loop_exit = OnScopeLeave([env] { + env->performance_state()->Mark( + node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); + }); + + bool more; do { if (env->is_stopping()) break; - uv_run(env->event_loop(), UV_RUN_DEFAULT); + uv_run(env->event_loop(), run_mode); if (env->is_stopping()) break; + if constexpr (run_mode != UV_RUN_DEFAULT) { + break; + } + platform->DrainTasks(isolate); more = uv_loop_alive(env->event_loop()); - if (more && !env->is_stopping()) continue; + if constexpr (cleanup_mode == SpinEventLoopCleanupMode::kNormal) { + if (more && !env->is_stopping()) continue; - if (EmitProcessBeforeExit(env).IsNothing()) - break; + if (EmitProcessBeforeExit(env).IsNothing()) break; - { - HandleScope handle_scope(isolate); - if (env->RunSnapshotSerializeCallback().IsEmpty()) { - break; + { + HandleScope handle_scope(isolate); + if (env->RunSnapshotSerializeCallback().IsEmpty()) { + break; + } } - } - // Emit `beforeExit` if the loop became alive either after emitting - // event, or after running some callbacks. - more = uv_loop_alive(env->event_loop()); + // Emit `beforeExit` again 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); - // 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. - env->set_snapshot_serialize_callback(Local()); - - env->PrintInfoForSnapshotIfDebug(); - env->ForEachRealm([](Realm* realm) { realm->VerifyNoStrongBaseObjects(); }); - Maybe exit_code = EmitProcessExitInternal(env); - if (exit_code.FromMaybe(ExitCode::kGenericUserError) != - ExitCode::kNoFailure) { - return exit_code; + if constexpr (run_mode == UV_RUN_DEFAULT && + cleanup_mode == SpinEventLoopCleanupMode::kNormal) { + // 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. + env->set_snapshot_serialize_callback(Local()); + + env->PrintInfoForSnapshotIfDebug(); + env->ForEachRealm([](Realm* realm) { realm->VerifyNoStrongBaseObjects(); }); + return EmitProcessExitInternal(env); } + return Just(ExitCode::kNoFailure); +} + +v8::Maybe SpinEventLoopInternal(Environment* env) { + return SpinEventLoopInternalImpl(env); +} + +Maybe ExitCodeToInt(Maybe value) { + if (value.IsNothing()) return Nothing(); + return Just(static_cast(value.FromJust())); +} + +Maybe SpinEventLoop(Environment* env) { + return ExitCodeToInt(SpinEventLoopInternalImpl(env)); +} - auto unsettled_tla = env->CheckUnsettledTopLevelAwait(); - if (unsettled_tla.IsNothing()) { +v8::Maybe SpinEventLoopWithoutCleanup(Environment* env, + uv_run_mode run_mode) { + if (run_mode == UV_RUN_DEFAULT) { + return SpinEventLoopInternalImpl(env); + } else if (run_mode == UV_RUN_ONCE) { + return SpinEventLoopInternalImpl(env); + } else if (run_mode == UV_RUN_NOWAIT) { + return SpinEventLoopInternalImpl(env); + } else { + CHECK(false && "Invalid run_mode"); return Nothing(); } - if (!unsettled_tla.FromJust()) { - return Just(ExitCode::kUnsettledTopLevelAwait); - } - return Just(ExitCode::kNoFailure); } struct CommonEnvironmentSetup::Impl { @@ -229,9 +277,10 @@ CommonEnvironmentSetup::~CommonEnvironmentSetup() { } bool platform_finished = false; - impl_->platform->AddIsolateFinishedCallback(isolate, [](void* data) { - *static_cast(data) = true; - }, &platform_finished); + impl_->platform->AddIsolateFinishedCallback( + isolate, + [](void* data) { *static_cast(data) = true; }, + &platform_finished); impl_->platform->UnregisterIsolate(isolate); if (impl_->snapshot_creator.has_value()) impl_->snapshot_creator.reset(); @@ -239,8 +288,7 @@ CommonEnvironmentSetup::~CommonEnvironmentSetup() { isolate->Dispose(); // Wait until the platform has cleaned up all relevant resources. - while (!platform_finished) - uv_run(&impl_->loop, UV_RUN_ONCE); + while (!platform_finished) uv_run(&impl_->loop, UV_RUN_ONCE); } if (impl_->isolate || impl_->loop.data != nullptr) @@ -261,14 +309,6 @@ EmbedderSnapshotData::Pointer CommonEnvironmentSetup::CreateSnapshot() { return result; } -Maybe SpinEventLoop(Environment* env) { - Maybe result = SpinEventLoopInternal(env); - 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/js_native_api.h b/src/js_native_api.h index 8ef079b5158249..39c2247f4b7fb2 100644 --- a/src/js_native_api.h +++ b/src/js_native_api.h @@ -2,8 +2,6 @@ #define SRC_JS_NATIVE_API_H_ // This file needs to be compatible with C compilers. -#include // NOLINT(modernize-deprecated-headers) -#include // NOLINT(modernize-deprecated-headers) // Use INT_MAX, this should only be consumed by the pre-processor anyway. #define NAPI_VERSION_EXPERIMENTAL 2147483647 @@ -13,7 +11,7 @@ #else // The baseline version for N-API. // The NAPI_VERSION controls which version will be used by default when -// compilling a native addon. If the addon developer specifically wants to use +// compiling a native addon. If the addon developer specifically wants to use // functions available in a new version of N-API that is not yet ported in all // LTS versions, they can set NAPI_VERSION knowing that they have specifically // depended on that version. diff --git a/src/js_native_api_types.h b/src/js_native_api_types.h index 43e7bb77ff94e7..842f18b1de11e6 100644 --- a/src/js_native_api_types.h +++ b/src/js_native_api_types.h @@ -4,8 +4,14 @@ // This file needs to be compatible with C compilers. // This is a public include file, and these includes have essentially // became part of it's API. -#include // NOLINT(modernize-deprecated-headers) -#include // NOLINT(modernize-deprecated-headers) +#ifdef __cplusplus +#include +#include +#else +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) +#endif #if !defined __cplusplus || (defined(_MSC_VER) && _MSC_VER < 1900) typedef uint16_t char16_t; diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index 27aeac589b19cd..7d2d6a24dbb163 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -85,14 +85,31 @@ struct napi_env__ { // v8 uses a special exception to indicate termination, the // `handle_exception` callback should identify such case using // terminatedOrTerminating() before actually handle the exception - template - inline void CallIntoModule(T&& call, U&& handle_exception = HandleThrow) { - int open_handle_scopes_before = open_handle_scopes; - int open_callback_scopes_before = open_callback_scopes; - napi_clear_last_error(this); + template + inline void CallIntoModule( + Call&& call, JSExceptionHandler&& handle_exception = HandleThrow) { + CallModuleScopeData scope_data = OpenCallModuleScope(); + auto onModuleScopeLeave = node::OnScopeLeave( + [&] { CloseCallModuleScope(scope_data, handle_exception); }); call(this); - CHECK_EQ(open_handle_scopes, open_handle_scopes_before); - CHECK_EQ(open_callback_scopes, open_callback_scopes_before); + } + + struct CallModuleScopeData { + int open_handle_scopes_before; + int open_callback_scopes_before; + }; + + inline CallModuleScopeData OpenCallModuleScope() { + napi_clear_last_error(this); + return {open_handle_scopes, open_callback_scopes}; + } + + template + inline void CloseCallModuleScope( + const CallModuleScopeData& scope_data, + JSExceptionHandler&& handle_exception = HandleThrow) { + CHECK_EQ(open_handle_scopes, scope_data.open_handle_scopes_before); + CHECK_EQ(open_callback_scopes, scope_data.open_callback_scopes_before); if (!last_exception.IsEmpty()) { handle_exception(this, last_exception.Get(this->isolate)); last_exception.Reset(); diff --git a/src/node_api.cc b/src/node_api.cc index 1638d096969826..9ffeeb34878cc9 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -164,9 +164,11 @@ void ThrowNodeApiVersionError(node::Environment* node_env, node_env->ThrowError(error_message.c_str()); } -inline napi_env NewEnv(v8::Local context, - const std::string& module_filename, - int32_t module_api_version) { +} // namespace + +napi_env NewEnv(v8::Local context, + const std::string& module_filename, + int32_t module_api_version) { node_napi_env result; // Validate module_api_version. @@ -196,6 +198,8 @@ inline napi_env NewEnv(v8::Local context, return result; } +namespace { + class ThreadSafeFunction : public node::AsyncResource { public: ThreadSafeFunction(v8::Local func, diff --git a/src/node_embedding_api.cc b/src/node_embedding_api.cc new file mode 100644 index 00000000000000..41c47af046089f --- /dev/null +++ b/src/node_embedding_api.cc @@ -0,0 +1,1777 @@ +#include "node_version.h" // define NODE_VERSION first + +#include "node_embedding_api_cpp.h" + +#include "env-inl.h" +#include "js_native_api_v8.h" +#include "node_api_internals.h" +#include "util-inl.h" +#include "uv.h" + +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#elif defined(__linux) +#include +#endif + +// Use macros to handle errors since they can record the failing argument name +// or expression and their location in the source code. + +#define CAST_NOT_NULL_TO(value, type) \ + (value) == nullptr ? EmbeddedErrorHandling::HandleError( \ + NodeStatus::kNullArg, \ + "Argument must not be null: " #value, \ + __FILE__, \ + __LINE__) \ + : reinterpret_cast(value) + +#define EMBEDDED_PLATFORM(platform) CAST_NOT_NULL_TO(platform, EmbeddedPlatform) + +#define EMBEDDED_RUNTIME(runtime) CAST_NOT_NULL_TO(runtime, EmbeddedRuntime) + +#define ASSERT_ARG_NOT_NULL(arg) \ + do { \ + if ((arg) == nullptr) { \ + return EmbeddedErrorHandling::HandleError( \ + NodeStatus::kNullArg, \ + "Argument must not be null: " #arg, \ + __FILE__, \ + __LINE__); \ + } \ + } while (false) + +#define ASSERT_ARG(arg, expr) \ + do { \ + if (!(expr)) { \ + return EmbeddedErrorHandling::HandleError(NodeStatus::kBadArg, \ + "Arg: " #arg \ + " failed: " #expr, \ + __FILE__, \ + __LINE__); \ + } \ + } while (false) + +#define ASSERT_EXPR(expr) \ + do { \ + if (!(expr)) { \ + return EmbeddedErrorHandling::HandleError( \ + NodeStatus::kGenericError, \ + "Expression returned false: " #expr, \ + __FILE__, \ + __LINE__); \ + } \ + } while (false) + +#define CHECK_STATUS(expr) \ + do { \ + if (NodeStatus status = (expr); status != NodeStatus::kOk) { \ + return status; \ + } \ + } while (false) + +namespace v8impl { + +// Creates new Node-API environment. It is defined in node_api.cc. +napi_env NewEnv(v8::Local context, + const std::string& module_filename, + int32_t module_api_version); + +} // namespace v8impl + +namespace node { + +// Declare functions implemented in embed_helpers.cc +v8::Maybe SpinEventLoopWithoutCleanup(Environment* env, + uv_run_mode run_mode); +} // namespace node + +namespace node::embedding { + +//------------------------------------------------------------------------------ +// Convenience functor struct adapter for C++ function object or lambdas. +//------------------------------------------------------------------------------ + +template +std::shared_ptr> MakeSharedFunctorPtr( + TCallback callback, + void* callback_data, + node_embedding_data_release_callback release_callback_data) { + return callback ? std::make_shared>( + callback, callback_data, release_callback_data) + : nullptr; +} + +// Stack implementation that works only with trivially constructible, +// destructible, and copyable types. It uses the small value optimization where +// several elements are stored in the in-place array. +template +class SmallTrivialStack { + static_assert(std::is_trivially_constructible_v, + "T must be trivially constructible"); + static_assert(std::is_trivially_destructible_v, + "T must be trivially destructible"); + static_assert(std::is_trivially_copyable_v, + "T must be trivially copyable"); + + public: + SmallTrivialStack() noexcept : stack_(this->inplace_entries_.data()) {} + + void Push(T&& value) { + EnsureCapacity(size_ + 1); + stack_[size_++] = std::move(value); + } + + void Pop() { + CHECK_GT(size_, 0); + --size_; + } + + size_t size() const { return size_; } + + const T& top() const { + CHECK_GT(size_, 0); + return stack_[size_ - 1]; + } + + SmallTrivialStack(const SmallTrivialStack&) = delete; + SmallTrivialStack& operator=(const SmallTrivialStack&) = delete; + + private: + void EnsureCapacity(size_t new_size) { + if (new_size <= capacity_) { + return; + } + + size_t new_capacity = capacity_ + capacity_ / 2; + std::unique_ptr new_allocated_entries = + std::make_unique(new_capacity); + std::memcpy(new_allocated_entries.get(), stack_, size_ * sizeof(T)); + allocated_entries_ = std::move(new_allocated_entries); + stack_ = allocated_entries_.get(); + capacity_ = new_capacity; + } + + private: + std::array inplace_entries_; + T* stack_{}; // Points to either inplace_entries_ or allocated_entries_. + size_t size_{}; // Number of elements in the stack. + size_t capacity_{kInplaceEntryCount}; + std::unique_ptr allocated_entries_; +}; + +class EmbeddedErrorHandling { + public: + static NodeStatus HandleError(NodeStatus status, std::string_view message); + + static NodeStatus HandleError(NodeStatus status, + const std::vector& messages); + + static NodeStatus HandleError(NodeStatus status, + std::string_view message, + std::string_view filename, + int32_t line); + + static std::string FormatString(const char* format, ...); + + static const char* GetLastErrorMessage(); + static void SetLastErrorMessage(std::string message); + static void ClearLastErrorMessage(); + + static NodeStatus ExitCodeToStatus(int32_t exit_code); + + private: + enum class ErrorMessageAction { + kGet, + kSet, + kClear, + }; + + private: + static const char* DoErrorMessage(ErrorMessageAction action, + std::string message); +}; + +class EmbeddedPlatform { + public: + EmbeddedPlatform(int32_t argc, const char* argv[]) noexcept + : args_(argv, argv + argc) {} + + EmbeddedPlatform(const EmbeddedPlatform&) = delete; + EmbeddedPlatform& operator=(const EmbeddedPlatform&) = delete; + + static NodeStatus RunMain( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); + + static NodeStatus Create( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_platform* result); + + NodeStatus DeleteMe(); + + NodeStatus SetApiVersion(int32_t embedding_api_version); + + NodeStatus SetFlags(NodePlatformFlags flags); + + NodeStatus Initialize( + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + bool* early_return); + + NodeStatus GetParsedArgs(int32_t* args_count, + const char** args[], + int32_t* runtime_args_count, + const char** runtime_args[]); + + node::InitializationResult* init_result() { return init_result_.get(); } + + node::MultiIsolatePlatform* get_v8_platform() { return v8_platform_.get(); } + + int32_t embedding_api_version() { return embedding_api_version_; } + + private: + static node::ProcessInitializationFlags::Flags GetProcessInitializationFlags( + NodePlatformFlags flags); + + private: + int32_t embedding_api_version_{0}; + bool is_initialized_{false}; + bool v8_is_initialized_{false}; + bool v8_is_uninitialized_{false}; + NodePlatformFlags flags_; + std::vector args_; + struct { + bool flags : 1; + } optional_bits_{}; + + std::shared_ptr init_result_; + std::unique_ptr v8_platform_; + NodeCStringArray<> c_args_; + NodeCStringArray<> c_runtime_args_; +}; + +class EmbeddedRuntime { + public: + explicit EmbeddedRuntime(EmbeddedPlatform* platform); + + EmbeddedRuntime(const EmbeddedRuntime&) = delete; + EmbeddedRuntime& operator=(const EmbeddedRuntime&) = delete; + + static NodeStatus Run( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); + + static NodeStatus Create( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data, + node_embedding_runtime* result); + + NodeStatus DeleteMe(); + + NodeStatus SetNodeApiVersion(int32_t node_api_version); + + NodeStatus SetFlags(NodeRuntimeFlags flags); + + NodeStatus SetArgs(int32_t argc, + const char* argv[], + int32_t exec_argc, + const char* exec_argv[]); + + NodeStatus OnPreload( + node_embedding_runtime_preload_callback run_preload, + void* preload_data, + node_embedding_data_release_callback release_preload_data); + + NodeStatus OnStartExecution( + node_embedding_runtime_loading_callback start_execution, + void* start_execution_data, + node_embedding_data_release_callback release_start_execution_data); + + NodeStatus OnHandleExecutionResult( + node_embedding_runtime_loaded_callback handle_result, + void* handle_result_data, + node_embedding_data_release_callback release_handle_result_data); + + NodeStatus AddModule( + const char* module_name, + node_embedding_module_initialize_callback init_module, + void* init_module_data, + node_embedding_data_release_callback release_init_module_data, + int32_t module_node_api_version); + + NodeStatus Initialize( + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); + + NodeStatus SetTaskRunner( + node_embedding_task_post_callback post_task, + void* post_task_data, + node_embedding_data_release_callback release_post_task_data); + + NodeStatus SetUserData( + void* user_data, node_embedding_data_release_callback release_user_data); + + NodeStatus GetUserData(void** user_data); + + NodeStatus RunEventLoop(); + + NodeStatus TerminateEventLoop(); + + NodeStatus RunEventLoopOnce(bool* has_more_work); + + NodeStatus RunEventLoopNoWait(bool* has_more_work); + + NodeStatus RunNodeApi(node_embedding_node_api_run_callback run_node_api, + void* run_node_api_data); + + NodeStatus OpenNodeApiScope(node_embedding_node_api_scope* node_api_scope, + napi_env* env); + NodeStatus CloseNodeApiScope(node_embedding_node_api_scope node_api_scope); + bool IsNodeApiScopeOpened() const; + + napi_env GetOrCreateNodeApiEnv(node::Environment* node_env, + const std::string& module_filename); + + size_t OpenV8Scope(); + void CloseV8Scope(size_t nest_level); + + private: + static void TriggerFatalException(napi_env env, + v8::Local local_err); + static node::EnvironmentFlags::Flags GetEnvironmentFlags( + NodeRuntimeFlags flags); + + void RegisterModules(); + + static void RegisterModule(v8::Local exports, + v8::Local module, + v8::Local context, + void* priv); + + uv_loop_t* EventLoop(); + void InitializePollingThread(); + void DestroyPollingThread(); + void WakeupPollingThread(); + static void RunPollingThread(void* data); + void PollEvents(); + + private: + struct ModuleInfo { + node_embedding_runtime runtime; + std::string module_name; + NodeInitializeModuleCallback init_module; + int32_t module_node_api_version; + }; + + struct V8ScopeLocker { + explicit V8ScopeLocker(EmbeddedRuntime& runtime) + : runtime_(runtime), nest_level_(runtime_.OpenV8Scope()) {} + + ~V8ScopeLocker() { runtime_.CloseV8Scope(nest_level_); } + + V8ScopeLocker(const V8ScopeLocker&) = delete; + V8ScopeLocker& operator=(const V8ScopeLocker&) = delete; + + private: + EmbeddedRuntime& runtime_; + size_t nest_level_; + }; + + struct V8ScopeData { + V8ScopeData(node::CommonEnvironmentSetup* env_setup) + : isolate_(env_setup->isolate()), + v8_locker_(env_setup->isolate()), + isolate_scope_(env_setup->isolate()), + handle_scope_(env_setup->isolate()), + context_scope_(env_setup->context()) {} + + bool IsLocked() const { return v8::Locker::IsLocked(isolate_); } + + size_t IncrementNestLevel() { return ++nest_level_; } + + bool DecrementNestLevel() { return --nest_level_ == 0; } + + size_t nest_level() const { return nest_level_; } + + private: + int32_t nest_level_{1}; + v8::Isolate* isolate_; + v8::Locker v8_locker_; // TODO(vmoroz): can we remove it? + v8::Isolate::Scope isolate_scope_; + v8::HandleScope handle_scope_; + v8::Context::Scope context_scope_; + }; + + struct NodeApiScopeData { + napi_env__::CallModuleScopeData module_scope_data_; + size_t v8_scope_nest_level_; + }; + + class ReleaseCallbackDeleter { + public: + explicit ReleaseCallbackDeleter( + node_embedding_data_release_callback release_callback) + : release_callback_(release_callback) {} + + void operator()(void* data) { + if (release_callback_) { + release_callback_(data); + } + } + + private: + node_embedding_data_release_callback release_callback_; + }; + + private: + EmbeddedPlatform* platform_; + bool is_initialized_{false}; + int32_t node_api_version_{NODE_API_DEFAULT_MODULE_API_VERSION}; + NodeRuntimeFlags flags_{NodeRuntimeFlags::kDefault}; + std::vector args_; + std::vector exec_args_; + node::EmbedderPreloadCallback preload_cb_{}; + node::StartExecutionCallback start_execution_cb_{}; + NodeHandleExecutionResultCallback handle_result_{}; + + struct { + bool flags : 1; + bool args : 1; + bool exec_args : 1; + } optional_bits_{}; + + std::unordered_map modules_; + + std::unique_ptr env_setup_; + std::optional v8_scope_data_; + + std::unique_ptr user_data_{ + nullptr, ReleaseCallbackDeleter(nullptr)}; + + NodePostTaskCallback post_task_{}; + uv_async_t polling_async_handle_{}; + uv_sem_t polling_sem_{}; + uv_thread_t polling_thread_{}; + bool polling_thread_closed_{false}; +#if defined(__linux) + // Epoll to poll for uv's backend fd. + int epoll_{epoll_create(1)}; +#endif + + SmallTrivialStack node_api_scope_data_{}; + + // The node API associated with the runtime's environment. + napi_env node_api_env_{}; + + // Map from worker thread node::Environment* to napi_env. + std::mutex worker_env_mutex_; + std::unordered_map worker_env_to_node_api_; +}; + +//----------------------------------------------------------------------------- +// EmbeddedErrorHandling implementation. +//----------------------------------------------------------------------------- + +/*static*/ NodeStatus EmbeddedErrorHandling::HandleError( + NodeStatus status, std::string_view message) { + if (status == NodeStatus::kOk) { + ClearLastErrorMessage(); + } else { + SetLastErrorMessage(message.data()); + } + return status; +} + +/*static*/ NodeStatus EmbeddedErrorHandling::HandleError( + NodeStatus status, const std::vector& messages) { + if (status == NodeStatus::kOk) { + ClearLastErrorMessage(); + } else { + NodeErrorInfo::SetLastErrorMessage(messages); + } + return status; +} + +/*static*/ NodeStatus EmbeddedErrorHandling::HandleError( + NodeStatus status, + std::string_view message, + std::string_view filename, + int32_t line) { + return HandleError( + status, + FormatString( + "Error: %s at %s:%d", message.data(), filename.data(), line)); +} + +/*static*/ std::string EmbeddedErrorHandling::FormatString(const char* format, + ...) { + va_list args1; + va_start(args1, format); + va_list args2; // Required for some compilers like GCC since we go over the + // args twice. + va_copy(args2, args1); + std::string result(std::vsnprintf(nullptr, 0, format, args1), '\0'); + va_end(args1); + std::vsnprintf(&result[0], result.size() + 1, format, args2); + va_end(args2); + return result; +} + +/*static*/ const char* EmbeddedErrorHandling::GetLastErrorMessage() { + return DoErrorMessage(ErrorMessageAction::kGet, ""); +} + +/*static*/ void EmbeddedErrorHandling::SetLastErrorMessage( + std::string message) { + DoErrorMessage(ErrorMessageAction::kSet, std::move(message)); +} + +/*static*/ void EmbeddedErrorHandling::ClearLastErrorMessage() { + DoErrorMessage(ErrorMessageAction::kClear, ""); +} + +/*static*/ const char* EmbeddedErrorHandling::DoErrorMessage( + EmbeddedErrorHandling::ErrorMessageAction action, std::string message) { + static thread_local const char* thread_message_ptr = nullptr; + static std::unordered_map> + thread_to_message; + static std::mutex mutex; + + switch (action) { + case ErrorMessageAction::kGet: + break; // Just return the message. + case ErrorMessageAction::kSet: { + auto message_ptr = std::make_unique(std::move(message)); + thread_message_ptr = message_ptr->c_str(); + std::scoped_lock lock(mutex); + thread_to_message[std::this_thread::get_id()] = std::move(message_ptr); + break; + } + case ErrorMessageAction::kClear: { + if (thread_message_ptr != nullptr) { + std::scoped_lock lock(mutex); + thread_to_message.erase(std::this_thread::get_id()); + thread_message_ptr = nullptr; + } + break; + } + } + + return thread_message_ptr != nullptr ? thread_message_ptr : ""; +} + +/*static*/ NodeStatus EmbeddedErrorHandling::ExitCodeToStatus( + int32_t exit_code) { + return exit_code != 0 + ? static_cast( + static_cast(NodeStatus::kErrorExitCode) | exit_code) + : NodeStatus::kOk; +} + +//----------------------------------------------------------------------------- +// EmbeddedPlatform implementation. +//----------------------------------------------------------------------------- + +NodeStatus EmbeddedPlatform::RunMain( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data) { + node_embedding_platform platform{}; + CHECK_STATUS(EmbeddedPlatform::Create(embedding_api_version, + argc, + argv, + configure_platform, + configure_platform_data, + &platform)); + if (platform == nullptr) { + return NodeStatus::kOk; // early return + } + return EmbeddedRuntime::Run( + platform, configure_runtime, configure_runtime_data); +} + +/*static*/ NodeStatus EmbeddedPlatform::Create( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_platform* result) { + ASSERT_ARG_NOT_NULL(result); + + // Hack around with the argv pointer. Used for process.title = "blah". + argv = + const_cast(uv_setup_args(argc, const_cast(argv))); + + auto platform_ptr = std::make_unique(argc, argv); + platform_ptr->SetApiVersion(embedding_api_version); + bool early_return = false; + CHECK_STATUS(platform_ptr->Initialize( + configure_platform, configure_platform_data, &early_return)); + if (early_return) { + return platform_ptr.release()->DeleteMe(); + } + + // The initialization was successful, the caller is responsible for deleting + // the platform instance. + *result = reinterpret_cast(platform_ptr.release()); + return NodeStatus::kOk; +} + +NodeStatus EmbeddedPlatform::DeleteMe() { + if (v8_is_initialized_ && !v8_is_uninitialized_) { + v8_is_uninitialized_ = true; + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + node::TearDownOncePerProcess(); + } + + delete this; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedPlatform::SetApiVersion(int32_t embedding_api_version) { + ASSERT_ARG(embedding_api_version, + embedding_api_version > 0 && + embedding_api_version <= NODE_EMBEDDING_VERSION); + return NodeStatus::kOk; +} + +NodeStatus EmbeddedPlatform::SetFlags(NodePlatformFlags flags) { + ASSERT_EXPR(!is_initialized_); + flags_ = flags; + optional_bits_.flags = true; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedPlatform::Initialize( + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + bool* early_return) { + ASSERT_EXPR(!is_initialized_); + + node_embedding_platform_config platform_config = + reinterpret_cast(this); + if (configure_platform != nullptr) { + CHECK_STATUS(configure_platform(configure_platform_data, platform_config)); + } + + is_initialized_ = true; + + if (!optional_bits_.flags) { + flags_ = NodePlatformFlags::kNone; + } + + init_result_ = node::InitializeOncePerProcess( + args_, GetProcessInitializationFlags(flags_)); + int32_t exit_code = init_result_->exit_code(); + if (exit_code != 0) { + NodeErrorInfo::SetLastErrorMessage(init_result_->errors()); + return EmbeddedErrorHandling::ExitCodeToStatus(exit_code); + } + + if (init_result_->early_return()) { + *early_return = true; + NodeErrorInfo::SetLastErrorMessage(init_result_->errors()); + return NodeStatus::kOk; + } + + c_args_ = NodeCStringArray<>(init_result_->args()); + c_runtime_args_ = NodeCStringArray<>(init_result_->exec_args()); + + int32_t thread_pool_size = + static_cast(node::per_process::cli_options->v8_thread_pool_size); + v8_platform_ = node::MultiIsolatePlatform::Create(thread_pool_size); + v8::V8::InitializePlatform(v8_platform_.get()); + v8::V8::Initialize(); + + v8_is_initialized_ = true; + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedPlatform::GetParsedArgs(int32_t* args_count, + const char** args[], + int32_t* runtime_args_count, + const char** runtime_args[]) { + ASSERT_EXPR(is_initialized_); + + if (args_count != nullptr) { + *args_count = c_args_.size(); + } + if (args != nullptr) { + *args = c_args_.c_strs(); + } + if (runtime_args_count != nullptr) { + *runtime_args_count = c_runtime_args_.size(); + } + if (runtime_args != nullptr) { + *runtime_args = c_runtime_args_.c_strs(); + } + return NodeStatus::kOk; +} + +node::ProcessInitializationFlags::Flags +EmbeddedPlatform::GetProcessInitializationFlags(NodePlatformFlags flags) { + uint32_t result = node::ProcessInitializationFlags::kNoFlags; + if (embedding::IsFlagSet(flags, NodePlatformFlags::kEnableStdioInheritance)) { + result |= node::ProcessInitializationFlags::kEnableStdioInheritance; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kDisableNodeOptionsEnv)) { + result |= node::ProcessInitializationFlags::kDisableNodeOptionsEnv; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kDisableCliOptions)) { + result |= node::ProcessInitializationFlags::kDisableCLIOptions; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kNoICU)) { + result |= node::ProcessInitializationFlags::kNoICU; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kNoStdioInitialization)) { + result |= node::ProcessInitializationFlags::kNoStdioInitialization; + } + if (embedding::IsFlagSet(flags, + NodePlatformFlags::kNoDefaultSignalHandling)) { + result |= node::ProcessInitializationFlags::kNoDefaultSignalHandling; + } + result |= node::ProcessInitializationFlags::kNoInitializeV8; + result |= node::ProcessInitializationFlags::kNoInitializeNodeV8Platform; + if (embedding::IsFlagSet(flags, NodePlatformFlags::kNoInitOpenSSL)) { + result |= node::ProcessInitializationFlags::kNoInitOpenSSL; + } + if (embedding::IsFlagSet(flags, + NodePlatformFlags::kNoParseGlobalDebugVariables)) { + result |= node::ProcessInitializationFlags::kNoParseGlobalDebugVariables; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kNoAdjustResourceLimits)) { + result |= node::ProcessInitializationFlags::kNoAdjustResourceLimits; + } + if (embedding::IsFlagSet(flags, NodePlatformFlags::kNoUseLargePages)) { + result |= node::ProcessInitializationFlags::kNoUseLargePages; + } + if (embedding::IsFlagSet(flags, + NodePlatformFlags::kNoPrintHelpOrVersionOutput)) { + result |= node::ProcessInitializationFlags::kNoPrintHelpOrVersionOutput; + } + if (embedding::IsFlagSet(flags, + NodePlatformFlags::kGeneratePredictableSnapshot)) { + result |= node::ProcessInitializationFlags::kGeneratePredictableSnapshot; + } + return static_cast(result); +} + +//----------------------------------------------------------------------------- +// EmbeddedRuntime implementation. +//----------------------------------------------------------------------------- + +/*static*/ NodeStatus EmbeddedRuntime::Run( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data) { + node_embedding_runtime runtime{}; + CHECK_STATUS( + Create(platform, configure_runtime, configure_runtime_data, &runtime)); + CHECK_STATUS(node_embedding_runtime_event_loop_run(runtime)); + CHECK_STATUS(node_embedding_runtime_delete(runtime)); + return NodeStatus::kOk; +} + +/*static*/ NodeStatus EmbeddedRuntime::Create( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data, + node_embedding_runtime* result) { + ASSERT_ARG_NOT_NULL(platform); + ASSERT_ARG_NOT_NULL(result); + + EmbeddedPlatform* platform_ptr = + reinterpret_cast(platform); + std::unique_ptr runtime_ptr = + std::make_unique(platform_ptr); + + CHECK_STATUS( + runtime_ptr->Initialize(configure_runtime, configure_runtime_data)); + + *result = reinterpret_cast(runtime_ptr.release()); + + return NodeStatus::kOk; +} + +EmbeddedRuntime::EmbeddedRuntime(EmbeddedPlatform* platform) + : platform_(platform) {} + +NodeStatus EmbeddedRuntime::DeleteMe() { + ASSERT_EXPR(!IsNodeApiScopeOpened()); + + std::unique_ptr env_setup = + std::move(env_setup_); + if (env_setup != nullptr) { + node::Stop(env_setup->env()); + } + + delete this; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::SetNodeApiVersion(int32_t node_api_version) { + ASSERT_ARG(node_api_version, + node_api_version >= NODE_API_DEFAULT_MODULE_API_VERSION && + (node_api_version <= NAPI_VERSION || + node_api_version == NAPI_VERSION_EXPERIMENTAL)); + + node_api_version_ = node_api_version; + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::SetFlags(NodeRuntimeFlags flags) { + ASSERT_EXPR(!is_initialized_); + flags_ = flags; + optional_bits_.flags = true; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::SetArgs(int32_t argc, + const char* argv[], + int32_t exec_argc, + const char* exec_argv[]) { + ASSERT_EXPR(!is_initialized_); + if (argv != nullptr) { + args_.assign(argv, argv + argc); + optional_bits_.args = true; + } + if (exec_argv != nullptr) { + exec_args_.assign(exec_argv, exec_argv + exec_argc); + optional_bits_.exec_args = true; + } + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::OnPreload( + node_embedding_runtime_preload_callback run_preload, + void* preload_data, + node_embedding_data_release_callback release_preload_data) { + ASSERT_EXPR(!is_initialized_); + + if (run_preload != nullptr) { + preload_cb_ = node::EmbedderPreloadCallback( + [this, + run_preload_ptr = MakeSharedFunctorPtr( + run_preload, preload_data, release_preload_data)]( + node::Environment* node_env, + v8::Local process, + v8::Local require) { + napi_env env = GetOrCreateNodeApiEnv(node_env, ""); + env->CallIntoModule( + [&](napi_env env) { + napi_value process_value = + v8impl::JsValueFromV8LocalValue(process); + napi_value require_value = + v8impl::JsValueFromV8LocalValue(require); + (*run_preload_ptr)( + reinterpret_cast(this), + env, + process_value, + require_value); + }, + TriggerFatalException); + }); + } else { + preload_cb_ = {}; + } + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::OnStartExecution( + node_embedding_runtime_loading_callback start_execution, + void* start_execution_data, + node_embedding_data_release_callback release_start_execution_data) { + ASSERT_EXPR(!is_initialized_); + + if (start_execution != nullptr) { + start_execution_cb_ = node::StartExecutionCallback( + [this, + start_execution_ptr = + MakeSharedFunctorPtr(start_execution, + start_execution_data, + release_start_execution_data)]( + const node::StartExecutionCallbackInfo& info) + -> v8::MaybeLocal { + napi_value result{}; + node_api_env_->CallIntoModule( + [&](napi_env env) { + napi_value process_value = + v8impl::JsValueFromV8LocalValue(info.process_object); + napi_value require_value = + v8impl::JsValueFromV8LocalValue(info.native_require); + napi_value run_cjs_value = + v8impl::JsValueFromV8LocalValue(info.run_cjs); + result = (*start_execution_ptr)( + reinterpret_cast(this), + env, + process_value, + require_value, + run_cjs_value); + }, + TriggerFatalException); + + if (result == nullptr) + return {}; + else + return v8impl::V8LocalValueFromJsValue(result); + }); + } else { + start_execution_cb_ = {}; + } + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::OnHandleExecutionResult( + node_embedding_runtime_loaded_callback handle_result, + void* handle_result_data, + node_embedding_data_release_callback release_handle_result_data) { + ASSERT_EXPR(!is_initialized_); + + handle_result_ = NodeHandleExecutionResultCallback( + handle_result, handle_result_data, release_handle_result_data); + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::AddModule( + const char* module_name, + node_embedding_module_initialize_callback init_module, + void* init_module_data, + node_embedding_data_release_callback release_init_module_data, + int32_t module_node_api_version) { + ASSERT_ARG_NOT_NULL(module_name); + ASSERT_ARG_NOT_NULL(init_module); + ASSERT_EXPR(!is_initialized_); + + auto insert_result = modules_.try_emplace( + module_name, + ModuleInfo{reinterpret_cast(this), + module_name, + NodeInitializeModuleCallback{ + init_module, init_module_data, release_init_module_data}, + module_node_api_version}); + if (!insert_result.second) { + return EmbeddedErrorHandling::HandleError( + NodeStatus::kBadArg, + EmbeddedErrorHandling::FormatString( + "Module with name '%s' is already added.", module_name)); + } + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::Initialize( + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data) { + ASSERT_EXPR(!is_initialized_); + + if (configure_runtime != nullptr) { + CHECK_STATUS(configure_runtime( + configure_runtime_data, + reinterpret_cast(platform_), + reinterpret_cast(this))); + } + + is_initialized_ = true; + + node::EnvironmentFlags::Flags flags = GetEnvironmentFlags( + optional_bits_.flags ? flags_ : NodeRuntimeFlags::kDefault); + + const std::vector& args = + optional_bits_.args ? args_ : platform_->init_result()->args(); + + const std::vector& exec_args = + optional_bits_.exec_args ? exec_args_ + : platform_->init_result()->exec_args(); + + node::MultiIsolatePlatform* v8_platform = platform_->get_v8_platform(); + + std::vector errors; + env_setup_ = node::CommonEnvironmentSetup::Create( + v8_platform, &errors, args, exec_args, flags); + + if (env_setup_ == nullptr || !errors.empty()) { + return EmbeddedErrorHandling::HandleError(NodeStatus::kGenericError, + errors); + } + + V8ScopeLocker v8_scope_locker(*this); + + std::string filename = args_.size() > 1 ? args_[1] : ""; + node_api_env_ = + v8impl::NewEnv(env_setup_->env()->context(), filename, node_api_version_); + + node::Environment* node_env = env_setup_->env(); + + RegisterModules(); + + v8::MaybeLocal ret = + node::LoadEnvironment(node_env, start_execution_cb_, preload_cb_); + + if (ret.IsEmpty()) { + return EmbeddedErrorHandling::HandleError(NodeStatus::kGenericError, + "Failed to load environment"); + } + + if (handle_result_) { + node_api_env_->CallIntoModule( + [&](napi_env env) { + handle_result_(reinterpret_cast(this), + env, + v8impl::JsValueFromV8LocalValue(ret.ToLocalChecked())); + }, + TriggerFatalException); + } + + InitializePollingThread(); + WakeupPollingThread(); + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::SetUserData( + void* user_data, node_embedding_data_release_callback release_user_data) { + user_data_.release(); // Do not call the release callback on set + user_data_ = std::unique_ptr( + user_data, ReleaseCallbackDeleter(release_user_data)); + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::GetUserData(void** user_data) { + ASSERT_ARG_NOT_NULL(user_data); + *user_data = user_data_.get(); + return NodeStatus::kOk; +} + +uv_loop_t* EmbeddedRuntime::EventLoop() { + return env_setup_->env()->event_loop(); +} + +void EmbeddedRuntime::InitializePollingThread() { + if (!post_task_) return; + + uv_loop_t* event_loop = EventLoop(); + + { +#if defined(_WIN32) + + SYSTEM_INFO system_info = {}; + ::GetNativeSystemInfo(&system_info); + + // on single-core the IO completion port NumberOfConcurrentThreads needs to + // be 2 to avoid CPU pegging likely caused by a busy loop in PollEvents + if (system_info.dwNumberOfProcessors == 1) { + // the expectation is the event_loop has just been initialized + // which makes IOCP replacement safe + CHECK_EQ(0u, event_loop->active_handles); + CHECK_EQ(0u, event_loop->active_reqs.count); + + if (event_loop->iocp && event_loop->iocp != INVALID_HANDLE_VALUE) + ::CloseHandle(event_loop->iocp); + event_loop->iocp = + ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 2); + } + +#elif defined(__APPLE__) + + // Do nothing + +#elif defined(__linux) + + int backend_fd = uv_backend_fd(event_loop); + struct epoll_event ev = {}; + ev.events = EPOLLIN; + ev.data.fd = backend_fd; + epoll_ctl(epoll_, EPOLL_CTL_ADD, backend_fd, &ev); + +#else + ERROR_AND_ABORT("The platform is not supported yet."); +#endif + } + + // keep the loop alive and allow waking up the polling thread + uv_async_init(event_loop, &polling_async_handle_, nullptr); + + // Start worker thread that will post to the task runner when new uv events + // arrive. + polling_thread_closed_ = false; + uv_sem_init(&polling_sem_, 0); + uv_thread_create(&polling_thread_, RunPollingThread, this); +} + +void EmbeddedRuntime::DestroyPollingThread() { + if (!post_task_) return; + if (polling_thread_closed_) return; + + polling_thread_closed_ = true; + uv_sem_post(&polling_sem_); + // Wake up polling thread. + uv_async_send(&polling_async_handle_); + // Wait for polling thread to complete. + uv_thread_join(&polling_thread_); + + // Clear uv. + uv_sem_destroy(&polling_sem_); + uv_close(reinterpret_cast(&polling_async_handle_), nullptr); +} + +void EmbeddedRuntime::WakeupPollingThread() { + if (!post_task_) return; + if (polling_thread_closed_) return; + + uv_sem_post(&polling_sem_); +} + +void EmbeddedRuntime::RunPollingThread(void* data) { + EmbeddedRuntime* runtime = static_cast(data); + for (;;) { + // Wait for the task runner to deal with events. + uv_sem_wait(&runtime->polling_sem_); + if (runtime->polling_thread_closed_) break; + + // Wait for something to happen in uv loop. + runtime->PollEvents(); + if (runtime->polling_thread_closed_) break; + + // Deal with event in the task runner thread. + bool succeeded = false; + NodeStatus post_result = runtime->post_task_( + [](void* task_data) { + auto* runtime = static_cast(task_data); + return runtime->RunEventLoopNoWait(nullptr); + }, + runtime, + nullptr, + &succeeded); + + // TODO: Handle post_result + (void)post_result; + if (!succeeded) { + // The task runner is shutting down. + break; + } + } +} + +void EmbeddedRuntime::PollEvents() { + uv_loop_t* event_loop = EventLoop(); + + // If there are other kinds of events pending, uv_backend_timeout will + // instruct us not to wait. + int timeout = uv_backend_timeout(event_loop); + +#if defined(_WIN32) + + DWORD timeout_msec = static_cast(timeout); + DWORD byte_count; + ULONG_PTR completion_key; + OVERLAPPED* overlapped; + ::GetQueuedCompletionStatus(event_loop->iocp, + &byte_count, + &completion_key, + &overlapped, + timeout_msec); + + // Give the event back so libuv can deal with it. + if (overlapped != nullptr) + ::PostQueuedCompletionStatus( + event_loop->iocp, byte_count, completion_key, overlapped); + +#elif defined(__APPLE__) + + struct timeval tv; + if (timeout != -1) { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + } + + fd_set readset; + int fd = uv_backend_fd(event_loop); + FD_ZERO(&readset); + FD_SET(fd, &readset); + + // Wait for new libuv events. + int r; + do { + r = select( + fd + 1, &readset, nullptr, nullptr, timeout == -1 ? nullptr : &tv); + } while (r == -1 && errno == EINTR); + +#elif defined(__linux) + + // Wait for new libuv events. + int r; + do { + struct epoll_event ev; + r = epoll_wait(epoll_, &ev, 1, timeout); + } while (r == -1 && errno == EINTR); + +#else + ERROR_AND_ABORT("The platform is not supported yet."); +#endif +} + +NodeStatus EmbeddedRuntime::SetTaskRunner( + node_embedding_task_post_callback post_task, + void* post_task_data, + node_embedding_data_release_callback release_post_task_data) { + ASSERT_EXPR(!is_initialized_); + post_task_ = + NodePostTaskCallback{post_task, post_task_data, release_post_task_data}; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::RunEventLoop() { + ASSERT_EXPR(is_initialized_); + + V8ScopeLocker v8_scope_locker(*this); + + DestroyPollingThread(); + + int32_t exit_code = node::SpinEventLoop(env_setup_->env()).FromMaybe(1); + return EmbeddedErrorHandling::HandleError( + EmbeddedErrorHandling::ExitCodeToStatus(exit_code), + "Failed while closing the runtime"); +} + +NodeStatus EmbeddedRuntime::TerminateEventLoop() { + ASSERT_EXPR(is_initialized_); + + V8ScopeLocker v8_scope_locker(*this); + int32_t exit_code = node::Stop(env_setup_->env(), node::StopFlags::kNoFlags); + return EmbeddedErrorHandling::HandleError( + EmbeddedErrorHandling::ExitCodeToStatus(exit_code), + "Failed while stopping the runtime"); +} + +NodeStatus EmbeddedRuntime::RunEventLoopOnce(bool* has_more_work) { + ASSERT_EXPR(is_initialized_); + + V8ScopeLocker v8_scope_locker(*this); + + node::ExitCode exit_code = + node::SpinEventLoopWithoutCleanup(env_setup_->env(), UV_RUN_ONCE) + .FromMaybe(node::ExitCode::kGenericUserError); + if (exit_code != node::ExitCode::kNoFailure) { + return EmbeddedErrorHandling::HandleError( + EmbeddedErrorHandling::ExitCodeToStatus( + static_cast(exit_code)), + "Failed running the event loop"); + } + + if (has_more_work != nullptr) { + *has_more_work = uv_loop_alive(env_setup_->env()->event_loop()); + } + + WakeupPollingThread(); + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::RunEventLoopNoWait(bool* has_more_work) { + ASSERT_EXPR(is_initialized_); + + V8ScopeLocker v8_scope_locker(*this); + + node::ExitCode exit_code = + node::SpinEventLoopWithoutCleanup(env_setup_->env(), UV_RUN_ONCE) + .FromMaybe(node::ExitCode::kGenericUserError); + if (exit_code != node::ExitCode::kNoFailure) { + return EmbeddedErrorHandling::HandleError( + EmbeddedErrorHandling::ExitCodeToStatus( + static_cast(exit_code)), + "Failed running the event loop"); + } + + if (has_more_work != nullptr) { + *has_more_work = uv_loop_alive(env_setup_->env()->event_loop()); + } + + WakeupPollingThread(); + + return NodeStatus::kOk; +} + +/*static*/ void EmbeddedRuntime::TriggerFatalException( + napi_env env, v8::Local local_err) { + node_napi_env__* node_napi_env = static_cast(env); + if (node_napi_env->terminatedOrTerminating()) { + return; + } + // If there was an unhandled exception while calling Node-API, + // report it as a fatal exception. (There is no JavaScript on the + // call stack that can possibly handle it.) + node_napi_env->trigger_fatal_exception(local_err); +} + +size_t EmbeddedRuntime::OpenV8Scope() { + if (v8_scope_data_.has_value()) { + CHECK(v8_scope_data_->IsLocked()); + return v8_scope_data_->IncrementNestLevel(); + } + + v8_scope_data_.emplace(env_setup_.get()); + return 1; +} + +void EmbeddedRuntime::CloseV8Scope(size_t nest_level) { + CHECK(v8_scope_data_.has_value()); + CHECK_EQ(v8_scope_data_->nest_level(), nest_level); + if (v8_scope_data_->DecrementNestLevel()) { + v8_scope_data_.reset(); + } +} + +NodeStatus EmbeddedRuntime::RunNodeApi( + node_embedding_node_api_run_callback run_node_api, + void* run_node_api_data) { + ASSERT_ARG_NOT_NULL(run_node_api); + + node_embedding_node_api_scope node_api_scope{}; + napi_env env{}; + CHECK_STATUS(OpenNodeApiScope(&node_api_scope, &env)); + auto nodeApiScopeLeave = + node::OnScopeLeave([&]() { CloseNodeApiScope(node_api_scope); }); + + run_node_api(run_node_api_data, env); + + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::OpenNodeApiScope( + node_embedding_node_api_scope* node_api_scope, napi_env* env) { + ASSERT_ARG_NOT_NULL(node_api_scope); + ASSERT_ARG_NOT_NULL(env); + + size_t v8_scope_nest_level = OpenV8Scope(); + node_api_scope_data_.Push( + {node_api_env_->OpenCallModuleScope(), v8_scope_nest_level}); + + *node_api_scope = reinterpret_cast( + node_api_scope_data_.size()); + *env = node_api_env_; + return NodeStatus::kOk; +} + +NodeStatus EmbeddedRuntime::CloseNodeApiScope( + node_embedding_node_api_scope node_api_scope) { + CHECK_EQ(node_api_scope_data_.size(), + reinterpret_cast(node_api_scope)); + size_t v8_scope_nest_level = node_api_scope_data_.top().v8_scope_nest_level_; + + node_api_env_->CloseCallModuleScope( + node_api_scope_data_.top().module_scope_data_); + node_api_scope_data_.Pop(); + CloseV8Scope(v8_scope_nest_level); + + return NodeStatus::kOk; +} + +bool EmbeddedRuntime::IsNodeApiScopeOpened() const { + return node_api_scope_data_.size() > 0; +} + +napi_env EmbeddedRuntime::GetOrCreateNodeApiEnv( + node::Environment* node_env, const std::string& module_filename) { + // Check if this is the main environment associated with the runtime. + if (node_env == env_setup_->env()) { + return node_api_env_; + } + + { + // Return Node-API env if it already exists. + std::scoped_lock lock(worker_env_mutex_); + auto it = worker_env_to_node_api_.find(node_env); + if (it != worker_env_to_node_api_.end()) { + return it->second; + } + } + + // Create new Node-API env. We avoid creating the environment under the lock. + napi_env env = + v8impl::NewEnv(node_env->context(), module_filename, node_api_version_); + + // In case if we cannot insert the new env, we are just going to have an + // unused env which will be deleted in the end with other environments. + std::scoped_lock lock(worker_env_mutex_); + auto insert_result = worker_env_to_node_api_.try_emplace(node_env, env); + if (insert_result.second) { + // If the environment is successfully inserted, add a cleanup hook to delete + // it from the worker_env_to_node_api_ later. + struct CleanupContext { + EmbeddedRuntime* runtime_; + node::Environment* node_env_; + }; + node_env->AddCleanupHook( + [](void* arg) { + std::unique_ptr context{ + static_cast(arg)}; + std::scoped_lock lock( + context->runtime_->worker_env_mutex_); + context->runtime_->worker_env_to_node_api_.erase(context->node_env_); + }, + static_cast(new CleanupContext{this, node_env})); + } + + // Return either the inserted or the existing environment. + return insert_result.first->second; +} + +node::EnvironmentFlags::Flags EmbeddedRuntime::GetEnvironmentFlags( + NodeRuntimeFlags flags) { + uint64_t result = node::EnvironmentFlags::kNoFlags; + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kDefault)) { + result |= node::EnvironmentFlags::kDefaultFlags; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kOwnsProcessState)) { + result |= node::EnvironmentFlags::kOwnsProcessState; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kOwnsInspector)) { + result |= node::EnvironmentFlags::kOwnsInspector; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kNoRegisterEsmLoader)) { + result |= node::EnvironmentFlags::kNoRegisterESMLoader; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kTrackUnmanagedFds)) { + result |= node::EnvironmentFlags::kTrackUnmanagedFds; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kHideConsoleWindows)) { + result |= node::EnvironmentFlags::kHideConsoleWindows; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kNoNativeAddons)) { + result |= node::EnvironmentFlags::kNoNativeAddons; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kNoGlobalSearchPaths)) { + result |= node::EnvironmentFlags::kNoGlobalSearchPaths; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kNoBrowserGlobals)) { + result |= node::EnvironmentFlags::kNoBrowserGlobals; + } + if (embedding::IsFlagSet(flags, NodeRuntimeFlags::kNoCreateInspector)) { + result |= node::EnvironmentFlags::kNoCreateInspector; + } + if (embedding::IsFlagSet(flags, + NodeRuntimeFlags::kNoStartDebugSignalHandler)) { + result |= node::EnvironmentFlags::kNoStartDebugSignalHandler; + } + if (embedding::IsFlagSet(flags, + NodeRuntimeFlags::kNoWaitForInspectorFrontend)) { + result |= node::EnvironmentFlags::kNoWaitForInspectorFrontend; + } + return static_cast(result); +} + +void EmbeddedRuntime::RegisterModules() { + for (const auto& [module_name, module_info] : modules_) { + node::node_module mod = { + -1, // nm_version for Node-API + NM_F_LINKED, // nm_flags + nullptr, // nm_dso_handle + nullptr, // nm_filename + nullptr, // nm_register_func + RegisterModule, // nm_context_register_func + module_name.c_str(), // nm_modname + const_cast(&module_info), // nm_priv + nullptr // nm_link + }; + node::AddLinkedBinding(env_setup_->env(), mod); + } +} + +/*static*/ void EmbeddedRuntime::RegisterModule(v8::Local exports, + v8::Local module, + v8::Local context, + void* priv) { + ModuleInfo* module_info = static_cast(priv); + + // Create a new napi_env for this specific module. + napi_env env = v8impl::NewEnv( + context, module_info->module_name, module_info->module_node_api_version); + + napi_value node_api_exports = nullptr; + env->CallIntoModule([&](napi_env env) { + node_api_exports = + module_info->init_module(module_info->runtime, + env, + module_info->module_name.c_str(), + v8impl::JsValueFromV8LocalValue(exports)); + }); + + // If register function returned a non-null exports object different from + // the exports object we passed it, set that as the "exports" property of + // the module. + if (node_api_exports != nullptr && + node_api_exports != v8impl::JsValueFromV8LocalValue(exports)) { + napi_value node_api_module = v8impl::JsValueFromV8LocalValue(module); + napi_set_named_property(env, node_api_module, "exports", node_api_exports); + } +} + +} // namespace node::embedding + +using namespace node::embedding; + +const char* NAPI_CDECL node_embedding_last_error_message_get() { + return EmbeddedErrorHandling::GetLastErrorMessage(); +} + +void NAPI_CDECL node_embedding_last_error_message_set(const char* message) { + if (message == nullptr) { + EmbeddedErrorHandling::ClearLastErrorMessage(); + } else { + EmbeddedErrorHandling::SetLastErrorMessage(message); + } +} + +void NAPI_CDECL node_embedding_last_error_message_set_format(const char* format, + ...) { + constexpr size_t buffer_size = 1024; + char buffer[buffer_size]; + std::unique_ptr dynamic_buffer; + + va_list args1; + va_start(args1, format); + va_list args2; // Required for some compilers like GCC since we go over the + // args twice. + va_copy(args2, args1); + + size_t string_size = std::vsnprintf(nullptr, 0, format, args1); + va_end(args1); + char* message = buffer; + if (string_size >= buffer_size) { + dynamic_buffer = std::make_unique(string_size + 1); + message = dynamic_buffer.get(); + } + std::vsnprintf(&message[0], string_size + 1, format, args2); + va_end(args2); + + EmbeddedErrorHandling::SetLastErrorMessage(message); +} + +node_embedding_status NAPI_CDECL node_embedding_main_run( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data) { + return EmbeddedPlatform::RunMain(embedding_api_version, + argc, + argv, + configure_platform, + configure_platform_data, + configure_runtime, + configure_runtime_data); +} + +node_embedding_status NAPI_CDECL node_embedding_platform_create( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_platform* result) { + return EmbeddedPlatform::Create(embedding_api_version, + argc, + argv, + configure_platform, + configure_platform_data, + result); +} + +node_embedding_status NAPI_CDECL +node_embedding_platform_delete(node_embedding_platform platform) { + return EMBEDDED_PLATFORM(platform)->DeleteMe(); +} + +node_embedding_status NAPI_CDECL node_embedding_platform_config_set_flags( + node_embedding_platform_config platform_config, + node_embedding_platform_flags flags) { + return EMBEDDED_PLATFORM(platform_config)->SetFlags(flags); +} + +node_embedding_status NAPI_CDECL +node_embedding_platform_get_parsed_args(node_embedding_platform platform, + int32_t* args_count, + const char** args[], + int32_t* runtime_args_count, + const char** runtime_args[]) { + return EMBEDDED_PLATFORM(platform)->GetParsedArgs( + args_count, args, runtime_args_count, runtime_args); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_run( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data) { + return EmbeddedRuntime::Run( + platform, configure_runtime, configure_runtime_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_create( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data, + node_embedding_runtime* result) { + return EmbeddedRuntime::Create( + platform, configure_runtime, configure_runtime_data, result); +} + +node_embedding_status NAPI_CDECL +node_embedding_runtime_delete(node_embedding_runtime runtime) { + return EMBEDDED_RUNTIME(runtime)->DeleteMe(); +} + +node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_node_api_version( + node_embedding_runtime_config runtime_config, int32_t node_api_version) { + return EMBEDDED_RUNTIME(runtime_config)->SetNodeApiVersion(node_api_version); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_set_flags( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_flags flags) { + return EMBEDDED_RUNTIME(runtime_config)->SetFlags(flags); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_set_args( + node_embedding_runtime_config runtime_config, + int32_t argc, + const char* argv[], + int32_t runtime_argc, + const char* runtime_argv[]) { + return EMBEDDED_RUNTIME(runtime_config) + ->SetArgs(argc, argv, runtime_argc, runtime_argv); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_on_preload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback run_preload, + void* preload_data, + node_embedding_data_release_callback release_preload_data) { + return EMBEDDED_RUNTIME(runtime_config) + ->OnPreload(run_preload, preload_data, release_preload_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_on_loading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback start_execution, + void* start_execution_data, + node_embedding_data_release_callback release_start_execution_data) { + return EMBEDDED_RUNTIME(runtime_config) + ->OnStartExecution( + start_execution, start_execution_data, release_start_execution_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_on_loaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_result, + void* handle_result_data, + node_embedding_data_release_callback release_handle_result_data) { + return EMBEDDED_RUNTIME(runtime_config) + ->OnHandleExecutionResult( + handle_result, handle_result_data, release_handle_result_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_add_module( + node_embedding_runtime_config runtime_config, + const char* module_name, + node_embedding_module_initialize_callback init_module, + void* init_module_data, + node_embedding_data_release_callback release_init_module_data, + int32_t module_node_api_version) { + return EMBEDDED_RUNTIME(runtime_config) + ->AddModule(module_name, + init_module, + init_module_data, + release_init_module_data, + module_node_api_version); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_user_data_set( + node_embedding_runtime runtime, + void* user_data, + node_embedding_data_release_callback release_user_data) { + return EMBEDDED_RUNTIME(runtime)->SetUserData(user_data, release_user_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_user_data_get( + node_embedding_runtime runtime, void** user_data) { + return EMBEDDED_RUNTIME(runtime)->GetUserData(user_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_config_set_task_runner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + void* post_task_data, + node_embedding_data_release_callback release_post_task_data) { + return EMBEDDED_RUNTIME(runtime_config) + ->SetTaskRunner(post_task, post_task_data, release_post_task_data); +} + +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run(node_embedding_runtime runtime) { + return EMBEDDED_RUNTIME(runtime)->RunEventLoop(); +} + +node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_terminate(node_embedding_runtime runtime) { + return EMBEDDED_RUNTIME(runtime)->TerminateEventLoop(); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_event_loop_run_once( + node_embedding_runtime runtime, bool* has_more_work) { + return EMBEDDED_RUNTIME(runtime)->RunEventLoopOnce(has_more_work); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_event_loop_run_no_wait( + node_embedding_runtime runtime, bool* has_more_work) { + return EMBEDDED_RUNTIME(runtime)->RunEventLoopNoWait(has_more_work); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_node_api_run( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + void* run_node_api_data) { + return EMBEDDED_RUNTIME(runtime)->RunNodeApi(run_node_api, run_node_api_data); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_node_api_scope_open( + node_embedding_runtime runtime, + node_embedding_node_api_scope* node_api_scope, + napi_env* env) { + return EMBEDDED_RUNTIME(runtime)->OpenNodeApiScope(node_api_scope, env); +} + +node_embedding_status NAPI_CDECL node_embedding_runtime_node_api_scope_close( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope) { + return EMBEDDED_RUNTIME(runtime)->CloseNodeApiScope(node_api_scope); +} diff --git a/src/node_embedding_api.h b/src/node_embedding_api.h new file mode 100644 index 00000000000000..9e468fc91d52a2 --- /dev/null +++ b/src/node_embedding_api.h @@ -0,0 +1,484 @@ +// +// Description: C-based API for embedding Node.js. +// +// !!! WARNING !!! WARNING !!! WARNING !!! +// This is a new API and is subject to change. +// While it is C-based, it is not ABI safe yet. +// Consider all functions and data structures as experimental. +// !!! WARNING !!! WARNING !!! WARNING !!! +// +// This file contains the C-based API for embedding Node.js in a host +// application. The API is designed to be used by applications that want to +// embed Node.js as a shared library (.so or .dll) and can interop with +// C-based API. +// + +#ifndef SRC_NODE_EMBEDDING_API_H_ +#define SRC_NODE_EMBEDDING_API_H_ + +#include "node_api.h" + +#define NODE_EMBEDDING_VERSION 1 + +#if defined(__cplusplus) && !defined(NODE_EMBEDDING_DISABLE_CPP_ENUMS) + +#define NODE_ENUM(c_name, cpp_name) enum class cpp_name : int32_t + +#define NODE_ENUM_FLAGS(c_name, cpp_name) \ + enum class cpp_name : int32_t; \ + \ + inline constexpr cpp_name operator|(cpp_name lhs, cpp_name rhs) { \ + return static_cast(static_cast(lhs) | \ + static_cast(rhs)); \ + } \ + \ + inline constexpr bool IsFlagSet(cpp_name flags, cpp_name flag) { \ + return (static_cast(flags) & static_cast(flag)) != 0; \ + } \ + \ + enum class cpp_name : int32_t + +#define NODE_ENUM_ITEM(c_name, cpp_name) cpp_name + +#else + +#define NODE_ENUM(c_name, cpp_name) \ + typedef enum c_name c_name; \ + enum c_name + +#define NODE_ENUM_FLAGS(c_name, cpp_name) NODE_ENUM(c_name, cpp_name) + +#define NODE_ENUM_ITEM(c_name, cpp_name) c_name + +#endif + +//============================================================================== +// Data types +//============================================================================== + +typedef struct node_embedding_platform_s* node_embedding_platform; +typedef struct node_embedding_runtime_s* node_embedding_runtime; +typedef struct node_embedding_platform_config_s* node_embedding_platform_config; +typedef struct node_embedding_runtime_config_s* node_embedding_runtime_config; +typedef struct node_embedding_node_api_scope_s* node_embedding_node_api_scope; + +#ifdef __cplusplus +namespace node::embedding { +#endif + +// The status returned by the Node.js embedding API functions. +NODE_ENUM(node_embedding_status, NodeStatus) { + NODE_ENUM_ITEM(node_embedding_status_ok, kOk) = 0, + NODE_ENUM_ITEM(node_embedding_status_generic_error, kGenericError) = 1, + NODE_ENUM_ITEM(node_embedding_status_null_arg, kNullArg) = 2, + NODE_ENUM_ITEM(node_embedding_status_bad_arg, kBadArg) = 3, + NODE_ENUM_ITEM(node_embedding_status_out_of_memory, kOutOfMemory) = 4, + // This value is added to the exit code in cases when Node.js API returns + // an error exit code. + NODE_ENUM_ITEM(node_embedding_status_error_exit_code, kErrorExitCode) = 512, +}; + +// The flags for the Node.js platform initialization. +// They match the internal ProcessInitializationFlags::Flags enum. +NODE_ENUM_FLAGS(node_embedding_platform_flags, NodePlatformFlags) { + NODE_ENUM_ITEM(node_embedding_platform_flags_none, kNone) = 0, + // Enable stdio inheritance, which is disabled by default. + // This flag is also implied by + // node_embedding_platform_flags_no_stdio_initialization. + NODE_ENUM_ITEM(node_embedding_platform_flags_enable_stdio_inheritance, + kEnableStdioInheritance) = 1 << 0, + // Disable reading the NODE_ENUM_ITEMS environment variable. + NODE_ENUM_ITEM(node_embedding_platform_flags_disable_node_options_env, + kDisableNodeOptionsEnv) = 1 << 1, + // Do not parse CLI options. + NODE_ENUM_ITEM(node_embedding_platform_flags_disable_cli_options, + kDisableCliOptions) = 1 << 2, + // Do not initialize ICU. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_icu, kNoICU) = 1 << 3, + // Do not modify stdio file descriptor or TTY state. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_stdio_initialization, + kNoStdioInitialization) = 1 << 4, + // Do not register Node.js-specific signal handlers + // and reset other signal handlers to default state. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_default_signal_handling, + kNoDefaultSignalHandling) = 1 << 5, + // Do not initialize OpenSSL config. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_init_openssl, + kNoInitOpenSSL) = 1 << 8, + // Do not initialize Node.js debugging based on environment variables. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_parse_global_debug_variables, + kNoParseGlobalDebugVariables) = 1 << 9, + // Do not adjust OS resource limits for this process. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_adjust_resource_limits, + kNoAdjustResourceLimits) = 1 << 10, + // Do not map code segments into large pages for this process. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_use_large_pages, + kNoUseLargePages) = 1 << 11, + // Skip printing output for --help, --version, --v8-options. + NODE_ENUM_ITEM(node_embedding_platform_flags_no_print_help_or_version_output, + kNoPrintHelpOrVersionOutput) = 1 << 12, + // Initialize the process for predictable snapshot generation. + NODE_ENUM_ITEM(node_embedding_platform_flags_generate_predictable_snapshot, + kGeneratePredictableSnapshot) = 1 << 14, +}; + +// The flags for the Node.js runtime initialization. +// They match the internal EnvironmentFlags::Flags enum. +NODE_ENUM_FLAGS(node_embedding_runtime_flags, NodeRuntimeFlags) { + NODE_ENUM_ITEM(node_embedding_runtime_flags_none, kNone) = 0, + // Use the default behavior for Node.js instances. + NODE_ENUM_ITEM(node_embedding_runtime_flags_default, kDefault) = 1 << 0, + // Controls whether this Environment is allowed to affect per-process state + // (e.g. cwd, process title, uid, etc.). + // This is set when using node_embedding_runtime_flags_default. + NODE_ENUM_ITEM(node_embedding_runtime_flags_owns_process_state, + kOwnsProcessState) = 1 << 1, + // Set if this Environment instance is associated with the global inspector + // handling code (i.e. listening on SIGUSR1). + // This is set when using node_embedding_runtime_flags_default. + NODE_ENUM_ITEM(node_embedding_runtime_flags_owns_inspector, + kOwnsInspector) = 1 << 2, + // Set if Node.js should not run its own esm loader. This is needed by some + // embedders, because it's possible for the Node.js esm loader to conflict + // with another one in an embedder environment, e.g. Blink's in Chromium. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_register_esm_loader, + kNoRegisterEsmLoader) = 1 << 3, + // Set this flag to make Node.js track "raw" file descriptors, i.e. managed + // by fs.open() and fs.close(), and close them during + // node_embedding_runtime_delete(). + NODE_ENUM_ITEM(node_embedding_runtime_flags_track_unmanaged_fds, + kTrackUnmanagedFds) = 1 << 4, + // Set this flag to force hiding console windows when spawning child + // processes. This is usually used when embedding Node.js in GUI programs on + // Windows. + NODE_ENUM_ITEM(node_embedding_runtime_flags_hide_console_windows, + kHideConsoleWindows) = 1 << 5, + // Set this flag to disable loading native addons via `process.dlopen`. + // This environment flag is especially important for worker threads + // so that a worker thread can't load a native addon even if `execArgv` + // is overwritten and `--no-addons` is not specified but was specified + // for this Environment instance. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_native_addons, + kNoNativeAddons) = 1 << 6, + // Set this flag to disable searching modules from global paths like + // $HOME/.node_modules and $NODE_PATH. This is used by standalone apps that + // do not expect to have their behaviors changed because of globally + // installed modules. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_global_search_paths, + kNoGlobalSearchPaths) = 1 << 7, + // Do not export browser globals like setTimeout, console, etc. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_browser_globals, + kNoBrowserGlobals) = 1 << 8, + // Controls whether or not the Environment should call + // V8Inspector::create(). This control is needed by embedders who may not + // want to initialize the V8 inspector in situations where one has already + // been created, e.g. Blink's in Chromium. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_create_inspector, + kNoCreateInspector) = 1 << 9, + // Controls whether or not the InspectorAgent for this Environment should + // call StartDebugSignalHandler. This control is needed by embedders who may + // not want to allow other processes to start the V8 inspector. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_start_debug_signal_handler, + kNoStartDebugSignalHandler) = 1 << 10, + // Controls whether the InspectorAgent created for this Environment waits + // for Inspector frontend events during the Environment creation. It's used + // to call node::Stop(env) on a Worker thread that is waiting for the + // events. + NODE_ENUM_ITEM(node_embedding_runtime_flags_no_wait_for_inspector_frontend, + kNoWaitForInspectorFrontend) = 1 << 11, +}; + +#ifdef __cplusplus +} // namespace node::embedding +using node_embedding_status = node::embedding::NodeStatus; +using node_embedding_platform_flags = node::embedding::NodePlatformFlags; +using node_embedding_runtime_flags = node::embedding::NodeRuntimeFlags; +#endif + +//============================================================================== +// Callbacks +//============================================================================== + +typedef node_embedding_status(NAPI_CDECL* node_embedding_data_release_callback)( + void* data); + +typedef node_embedding_status( + NAPI_CDECL* node_embedding_platform_configure_callback)( + void* cb_data, node_embedding_platform_config platform_config); + +typedef node_embedding_status( + NAPI_CDECL* node_embedding_runtime_configure_callback)( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config); + +// The error is to be handled by using napi_env. +typedef void(NAPI_CDECL* node_embedding_runtime_preload_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require); + +// The error is to be handled by using napi_env. +typedef napi_value(NAPI_CDECL* node_embedding_runtime_loading_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs); + +// The error is to be handled by using napi_env. +typedef void(NAPI_CDECL* node_embedding_runtime_loaded_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value load_result); + +// The error is to be handled by using napi_env. +typedef napi_value(NAPI_CDECL* node_embedding_module_initialize_callback)( + void* cb_data, + node_embedding_runtime runtime, + napi_env env, + const char* module_name, + napi_value exports); + +typedef node_embedding_status(NAPI_CDECL* node_embedding_task_run_callback)( + void* cb_data); + +typedef node_embedding_status(NAPI_CDECL* node_embedding_task_post_callback)( + void* cb_data, + node_embedding_task_run_callback run_task, + void* task_data, + node_embedding_data_release_callback release_task_data, + bool* is_posted); + +// The error is to be handled by using napi_env. +typedef void(NAPI_CDECL* node_embedding_node_api_run_callback)(void* cb_data, + napi_env env); + +//============================================================================== +// Functions +//============================================================================== + +EXTERN_C_START + +//------------------------------------------------------------------------------ +// Error handling functions. +//------------------------------------------------------------------------------ + +// Gets the last error message for the current thread. +NAPI_EXTERN const char* NAPI_CDECL node_embedding_last_error_message_get(); + +// Sets the last error message for the current thread. +NAPI_EXTERN void NAPI_CDECL +node_embedding_last_error_message_set(const char* message); + +// Sets the last error message for the current thread using C printf string +// formatting. +NAPI_EXTERN void NAPI_CDECL +node_embedding_last_error_message_set_format(const char* format, ...); + +//------------------------------------------------------------------------------ +// Node.js global platform functions. +//------------------------------------------------------------------------------ + +// Runs Node.js main function. +// By default it is the same as running Node.js from CLI. +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_main_run( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); + +// Creates and configures a new Node.js platform instance. +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_platform_create( + int32_t embedding_api_version, + int32_t argc, + const char* argv[], + node_embedding_platform_configure_callback configure_platform, + void* configure_platform_data, + node_embedding_platform* result); + +// Deletes the Node.js platform instance. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_platform_delete(node_embedding_platform platform); + +// Sets the flags for the Node.js platform initialization. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_platform_config_set_flags( + node_embedding_platform_config platform_config, + node_embedding_platform_flags flags); + +// Gets the parsed list of non-Node.js and Node.js arguments. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_platform_get_parsed_args(node_embedding_platform platform, + int32_t* args_count, + const char** args[], + int32_t* runtime_args_count, + const char** runtime_args[]); + +//------------------------------------------------------------------------------ +// Node.js runtime functions. +//------------------------------------------------------------------------------ + +// Runs the Node.js runtime with the provided configuration. +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_runtime_run( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data); + +// Creates a new Node.js runtime instance. +NAPI_EXTERN node_embedding_status NAPI_CDECL node_embedding_runtime_create( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + void* configure_runtime_data, + node_embedding_runtime* result); + +// Deletes the Node.js runtime instance. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_delete(node_embedding_runtime runtime); + +// Sets the Node-API version used for Node.js runtime. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_node_api_version( + node_embedding_runtime_config runtime_config, int32_t node_api_version); + +// Sets the flags for the Node.js runtime initialization. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_flags( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_flags flags); + +// Sets the non-Node.js and Node.js CLI arguments for the Node.js runtime +// initialization. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_args( + node_embedding_runtime_config runtime_config, + int32_t argc, + const char* argv[], + int32_t runtime_argc, + const char* runtime_argv[]); + +// Sets the preload callback for the Node.js runtime initialization. +// It is invoked before any other code execution for the runtime Node-API +// environment and for its worker thread environments. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_preload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback preload, + void* preload_data, + node_embedding_data_release_callback release_preload_data); + +// Sets the start execution callback for the Node.js runtime initialization. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_loading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback run_load, + void* load_data, + node_embedding_data_release_callback release_load_data); + +// Handles the execution result for the Node.js runtime initialization. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_on_loaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_loaded, + void* handle_loaded_data, + node_embedding_data_release_callback release_handle_loaded_data); + +// Adds a new module to the Node.js runtime. +// It is accessed as process._linkedBinding(module_name) in the main JS and in +// the related worker threads. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_add_module( + node_embedding_runtime_config runtime_config, + const char* module_name, + node_embedding_module_initialize_callback init_module, + void* init_module_data, + node_embedding_data_release_callback release_init_module_data, + int32_t module_node_api_version); + +// Sets user data for the Node.js runtime. +// The release callback is not called for the existing user data. +// It is only called when the runtime is deleted. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_user_data_set( + node_embedding_runtime runtime, + void* user_data, + node_embedding_data_release_callback release_user_data); + +// Gets user data for the Node.js runtime. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_user_data_get(node_embedding_runtime runtime, + void** user_data); + +//------------------------------------------------------------------------------ +// Node.js runtime functions for the event loop. +//------------------------------------------------------------------------------ + +// Sets the task runner for the Node.js runtime. +// This is an alternative way to run the Node.js runtime event loop that helps +// running it inside of an existing task scheduler. +// E.g. it enables running Node.js event loop inside of the application UI event +// loop or UI dispatcher. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_config_set_task_runner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + void* post_task_data, + node_embedding_data_release_callback release_post_task_data); + +// Runs the Node.js runtime event loop in UV_RUN_DEFAULT mode. +// It finishes it with emitting the beforeExit and exit process events. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run(node_embedding_runtime runtime); + +// Stops the Node.js runtime event loop. It cannot be resumed after this call. +// It does not emit the beforeExit and exit process events if they were not +// emitted before. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_terminate(node_embedding_runtime runtime); + +// Runs the Node.js runtime event loop once. It may block the current thread. +// It matches the UV_RUN_ONCE behavior +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run_once(node_embedding_runtime runtime, + bool* has_more_work); + +// Runs the Node.js runtime event loop once. It does not block the thread. +// It matches the UV_RUN_NOWAIT behavior. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_event_loop_run_no_wait(node_embedding_runtime runtime, + bool* has_more_work); + +//------------------------------------------------------------------------------ +// Node.js runtime functions for the Node-API interop. +//------------------------------------------------------------------------------ + +// Runs Node-API code in the Node-API scope. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_run( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + void* run_node_api_data); + +// Opens a new Node-API scope. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_scope_open( + node_embedding_runtime runtime, + node_embedding_node_api_scope* node_api_scope, + napi_env* env); + +// Closes the Node-API invocation scope. +NAPI_EXTERN node_embedding_status NAPI_CDECL +node_embedding_runtime_node_api_scope_close( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope); + +EXTERN_C_END + +#endif // SRC_NODE_EMBEDDING_API_H_ diff --git a/src/node_embedding_api_cpp.h b/src/node_embedding_api_cpp.h new file mode 100644 index 00000000000000..c03796b24193de --- /dev/null +++ b/src/node_embedding_api_cpp.h @@ -0,0 +1,1125 @@ +// +// Description: C-based API for embedding Node.js. +// +// !!! WARNING !!! WARNING !!! WARNING !!! +// This is a new API and is subject to change. +// While it is C-based, it is not ABI safe yet. +// Consider all functions and data structures as experimental. +// !!! WARNING !!! WARNING !!! WARNING !!! +// +// This file contains the C-based API for embedding Node.js in a host +// application. The API is designed to be used by applications that want to +// embed Node.js as a shared library (.so or .dll) and can interop with +// C-based API. +// + +#ifndef SRC_NODE_EMBEDDING_API_CPP_H_ +#define SRC_NODE_EMBEDDING_API_CPP_H_ + +//============================================================================== +// The C++ wrappers for the Node.js embedding API. +//============================================================================== + +#include "node_embedding_api.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace node::embedding { + +//============================================================================== +// C++ convenience functions for the C API. +// These functions are not ABI safe and can be changed in future versions. +//============================================================================== + +// Move-only pointer wrapper. +// The class does not own the pointer and does not delete it. +// It simplifies implementation of the C++ API classes that wrap pointers. +template +class NodePointer { + public: + NodePointer() = default; + + explicit NodePointer(TPointer ptr) : ptr_(ptr) {} + + NodePointer(const NodePointer&) = delete; + NodePointer& operator=(const NodePointer&) = delete; + + NodePointer(NodePointer&& other) noexcept + : ptr_(std::exchange(other.ptr_, nullptr)) {} + + NodePointer& operator=(NodePointer&& other) noexcept { + if (this != &other) { + ptr_ = std::exchange(other.ptr_, nullptr); + } + return *this; + } + + NodePointer& operator=(std::nullptr_t) { + ptr_ = nullptr; + return *this; + } + + TPointer ptr() const { return ptr_; } + + explicit operator bool() const { return ptr_ != nullptr; } + + private: + TPointer ptr_{}; +}; + +template +class [[nodiscard]] NodeExpected { + public: + explicit NodeExpected(T value) : value_(std::move(value)) {} + + explicit NodeExpected(NodeStatus status) : status_(status) {} + + NodeExpected(const NodeExpected&) = delete; + NodeExpected& operator=(const NodeExpected&) = delete; + + NodeExpected(NodeExpected&& other) noexcept : status_(other.status_) { + if (other.has_value()) { + new (std::addressof(value_)) T(std::move(other.value_)); + } + } + + NodeExpected& operator=(NodeExpected&& other) noexcept { + if (this != &other) { + if (has_value()) { + value_.~T(); + } + status_ = other.status_; + if (other.has_value()) { + new (std::addressof(value_)) T(std::move(other.value_)); + } + } + return *this; + } + + ~NodeExpected() { + if (has_value()) { + value_.~T(); + } + } + + bool has_value() const { return status_ == NodeStatus::kOk; } + bool has_error() const { return status_ != NodeStatus::kOk; } + + T& value() & { return value_; } + const T& value() const& { return value_; } + T&& value() && { return std::move(value_); } + const T&& value() const&& { return std::move(value_); } + + NodeStatus status() const { return status_; } + + int32_t exit_code() const { + if (status_ == NodeStatus::kOk) { + return 0; + } else if ((static_cast(status_) & + static_cast(NodeStatus::kErrorExitCode)) != 0) { + return static_cast(status_) & + ~static_cast(NodeStatus::kErrorExitCode); + } + return 1; + } + + private: + NodeStatus status_{NodeStatus::kOk}; + union { + T value_; // The value is uninitialized if status_ is not kOk. + char padding_[sizeof(T)]; + }; +}; + +template <> +class [[nodiscard]] NodeExpected { + public: + NodeExpected() = default; + + explicit NodeExpected(NodeStatus status) : status_(status) {} + + NodeExpected(const NodeExpected&) = delete; + NodeExpected& operator=(const NodeExpected&) = delete; + + NodeExpected(NodeExpected&& other) = default; + NodeExpected& operator=(NodeExpected&& other) = default; + + bool has_value() const { return status_ == NodeStatus::kOk; } + bool has_error() const { return status_ != NodeStatus::kOk; } + + NodeStatus status() const { return status_; } + + int32_t exit_code() const { + if (status_ == NodeStatus::kOk) { + return 0; + } else if ((static_cast(status_) & + static_cast(NodeStatus::kErrorExitCode)) != 0) { + return static_cast(status_) & + ~static_cast(NodeStatus::kErrorExitCode); + } + return 1; + } + + private: + NodeStatus status_{NodeStatus::kOk}; +}; + +// 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::string::c_str() method. +template +class NodeCStringArray { + public: + NodeCStringArray() = default; + + explicit NodeCStringArray(const std::vector& strings) noexcept + : size_(strings.size()) { + if (size_ <= inplace_buffer_.size()) { + c_strs_ = inplace_buffer_.data(); + } else { + allocated_buffer_ = std::make_unique(size_); + c_strs_ = allocated_buffer_.get(); + } + for (size_t i = 0; i < size_; ++i) { + c_strs_[i] = strings[i].c_str(); + } + } + + NodeCStringArray(const NodeCStringArray&) = delete; + NodeCStringArray& operator=(const NodeCStringArray&) = delete; + + NodeCStringArray(NodeCStringArray&& other) noexcept + : size_(std::exchange(other.size_, 0)), + c_strs_(std::exchange(other.c_strs_, 0)), + allocated_buffer_(std::exchange(other.allocated_buffer_, nullptr)) { + if (size_ <= inplace_buffer_.size()) { + c_strs_ = inplace_buffer_.data(); + std::memcpy(inplace_buffer_.data(), + other.inplace_buffer_.data(), + size_ * sizeof(const char*)); + } + } + + NodeCStringArray& operator=(NodeCStringArray&& other) noexcept { + if (this != &other) { + size_ = std::exchange(other.size_, 0); + c_strs_ = std::exchange(other.c_strs_, nullptr); + allocated_buffer_ = std::exchange(other.allocated_buffer_, nullptr); + if (size_ <= inplace_buffer_.size()) { + c_strs_ = inplace_buffer_.data(); + std::memcpy(inplace_buffer_.data(), + other.inplace_buffer_.data(), + size_ * sizeof(const char*)); + } + } + return *this; + } + + int32_t size() const { return static_cast(size_); } + const char** c_strs() const { return c_strs_; } + + private: + size_t size_{}; + const char** c_strs_{}; + std::array inplace_buffer_; + std::unique_ptr allocated_buffer_; +}; + +// Wraps command line arguments. +class NodeArgs { + public: + NodeArgs(int32_t argc, const char* argv[]) : argc_(argc), argv_(argv) {} + + NodeArgs(int32_t argc, char* argv[]) + : argc_(argc), argv_(const_cast(argv)) {} + + NodeArgs(const NodeCStringArray<>& string_array_view) + : argc_(string_array_view.size()), argv_(string_array_view.c_strs()) {} + + int32_t argc() const { return argc_; } + const char** argv() const { return argv_; } + + private: + int32_t argc_{}; + const char** argv_{}; +}; + +template +class NodeFunctorInvoker; + +template +class NodeFunctorRef; + +template +class NodeFunctorRef { + using TCallback = TResult (*)(void*, TArgs...); + + public: + NodeFunctorRef(std::nullptr_t) {} + + NodeFunctorRef(TCallback callback, void* data) + : callback_(callback), data_(data) {} + + template + NodeFunctorRef(TFunctor&& functor) + : callback_(&NodeFunctorInvoker::Invoke), + data_(&functor) {} + + NodeFunctorRef(NodeFunctorRef&& other) = default; + NodeFunctorRef& operator=(NodeFunctorRef&& other) = default; + + TCallback callback() const { return callback_.ptr(); } + + void* data() const { return data_.ptr(); } + + explicit operator bool() const { return static_cast(callback_); } + + private: + NodePointer callback_; + NodePointer data_; +}; + +template +class NodeFunctor; + +template +class NodeFunctor { + using TCallback = TResult (*)(void*, TArgs...); + + public: + NodeFunctor() = default; + NodeFunctor(std::nullptr_t) {} + + NodeFunctor(TCallback callback, + void* data, + node_embedding_data_release_callback data_release) + : callback_(callback), data_(data), data_release_(data_release) {} + + // TODO: add overload for stateless lambdas. + template + NodeFunctor(TFunctor&& functor) + : callback_(&NodeFunctorInvoker::Invoke), + data_(new TFunctor(std::forward(functor))), + data_release_(&ReleaseFunctor) {} + + NodeFunctor(NodeFunctor&& other) = default; + NodeFunctor& operator=(NodeFunctor&& other) = default; + + TCallback callback() const { return callback_.ptr(); } + + void* data() const { return data_.ptr(); } + + node_embedding_data_release_callback data_release() const { + return data_release_.ptr(); + } + + explicit operator bool() const { return static_cast(callback_); } + + TResult operator()(TArgs... args) const { + return (*callback_.ptr())(data_.ptr(), args...); + } + + private: + template + static NodeStatus ReleaseFunctor(void* data) { + // TODO: Handle exceptions. + delete reinterpret_cast(data); + return NodeStatus::kOk; + } + + private: + NodePointer callback_; + NodePointer data_; + NodePointer data_release_; +}; + +// NodeConfigurePlatformCallback supported signatures: +// - NodeExpected(const NodePlatformConfig& platform_config); +using NodeConfigurePlatformCallback = + NodeFunctorRef; + +// NodeConfigureRuntimeCallback supported signatures: +// - NodeExpected(const NodePlatform& platform, +// const NodeRuntimeConfig& runtime_config); +using NodeConfigureRuntimeCallback = + NodeFunctorRef; + +// NodePreloadCallback supported signatures: +// - void(const NodeRuntime& runtime, +// napi_env env, +// napi_value process, +// napi_value require); +using NodePreloadCallback = + NodeFunctor; + +// NodeStartExecutionCallback supported signatures: +// - napi_value(const NodeRuntime& runtime, +// napi_env env, +// napi_value process, +// napi_value require, +// napi_value run_cjs); +using NodeStartExecutionCallback = + NodeFunctor; + +// NodeHandleExecutionResultCallback supported signatures: +// - void(const NodeRuntime& runtime, +// napi_env env, +// napi_value execution_result); +using NodeHandleExecutionResultCallback = + NodeFunctor; + +// NodeInitializeModuleCallback supported signatures: +// - napi_value(const NodeRuntime& runtime, +// napi_env env, +// std::string_view module_name, +// napi_value exports); +using NodeInitializeModuleCallback = + NodeFunctor; + +// NodeRunTaskCallback supported signatures: +// - NodeExpected(); +using NodeRunTaskCallback = NodeFunctor; + +// NodePostTaskCallback supported signatures: +// - NodeExpected(NodeRunTaskCallback run_task); +using NodePostTaskCallback = NodeFunctor; + +// NodeRunNodeApiCallback supported signatures: +// - void(const NodeRuntime& runtime, napi_env env); +using NodeRunNodeApiCallback = + NodeFunctorRef; + +inline std::string NodeFormatStringHelper(const char* format, va_list args) { + va_list args2; // Required for some compilers like GCC since we go over the + // args twice. + va_copy(args2, args); + std::string result(std::vsnprintf(nullptr, 0, format, args), '\0'); + std::vsnprintf(&result[0], result.size() + 1, format, args2); + va_end(args2); + return result; +} + +inline std::string NodeFormatString(const char* format, ...) { + va_list args; + va_start(args, format); + std::string result = NodeFormatStringHelper(format, args); + va_end(args); + return result; +} + +class NodeErrorInfo { + public: + static const char* GetLastErrorMessage() { + return node_embedding_last_error_message_get(); + } + + static void SetLastErrorMessage(const char* message) { + return node_embedding_last_error_message_set(message); + } + + static void SetLastErrorMessage(std::string_view message, + std::string_view filename, + int32_t line) { + SetLastErrorMessage( + NodeFormatString( + "Error: %s at %s:%d", message.data(), filename.data(), line) + .c_str()); + } + + static void SetLastErrorMessage(const std::vector& message) { + std::string message_str; + bool first = true; + for (const std::string& part : message) { + if (!first) { + message_str += '\n'; + } else { + first = false; + } + message_str += part; + } + SetLastErrorMessage(message_str.c_str()); + } + + static void ClearLastErrorMessage() { + node_embedding_last_error_message_set(nullptr); + } + + static std::string GetAndClearLastErrorMessage() { + std::string result = GetLastErrorMessage(); + ClearLastErrorMessage(); + return result; + } +}; + +class NodeEmbeddingErrorHandler { + public: + NodeEmbeddingErrorHandler() = default; + + ~NodeEmbeddingErrorHandler() { current_internal() = previous_handler_; } + + void SetLastErrorMessage(const char* format, ...) { + va_list args; + va_start(args, format); + std::string message = NodeFormatString(format, args); + va_end(args); + node_embedding_last_error_message_set(message.c_str()); + embedding_status_ = NodeStatus::kGenericError; + } + + NodeExpected ReportResult() const { + return NodeExpected(embedding_status_); + } + + bool has_embedding_error() const { + return embedding_status_ != NodeStatus::kOk; + } + + NodeStatus embedding_status() const { return embedding_status_; } + + void set_embedding_status(NodeStatus status) { embedding_status_ = status; } + + void set_embedding_status(const NodeExpected& expected) { + embedding_status_ = expected.status(); + } + + static NodeEmbeddingErrorHandler* current() { return current_internal(); } + + static void CurrentSetEmbeddingStatus(NodeStatus status) { + if (NodeEmbeddingErrorHandler* current = current_internal()) { + current->set_embedding_status(status); + } + } + + NodeEmbeddingErrorHandler(const NodeEmbeddingErrorHandler&) = delete; + NodeEmbeddingErrorHandler& operator=(const NodeEmbeddingErrorHandler&) = + delete; + + private: + // Declaring operator new and delete as deleted is not spec compliant. + // Therefore declare them private instead to disable dynamic alloc + void* operator new(size_t size) { std::abort(); } + void* operator new[](size_t size) { std::abort(); } + void operator delete(void*, size_t) { std::abort(); } + void operator delete[](void*, size_t) { std::abort(); } + + static NodeEmbeddingErrorHandler*& current_internal() { + static thread_local NodeEmbeddingErrorHandler* current_handler = nullptr; + return current_handler; + } + + private: + NodeEmbeddingErrorHandler* previous_handler_{ + std::exchange(current_internal(), this)}; + NodeStatus embedding_status_{NodeStatus::kOk}; +}; + +class NodeApiErrorHandlerBase { + public: + void SetLastErrorMessage(const char* format, ...) { + va_list args; + va_start(args, format); + std::string message = NodeFormatString(format, args); + va_end(args); + ThrowLastErrorMessage(env_, message.c_str()); + node_api_status_ = napi_pending_exception; + } + + static void GetAndThrowLastErrorMessage(napi_env env) { + const napi_extended_error_info* error_info; + napi_get_last_error_info(env, &error_info); + ThrowLastErrorMessage(env, error_info->error_message); + } + + static void ThrowLastErrorMessage(napi_env env, const char* message) { + bool is_pending; + napi_is_exception_pending(env, &is_pending); + /* If an exception is already pending, don't rethrow it */ + if (!is_pending) { + const char* error_message = + message != nullptr ? message : "empty error message"; + napi_throw_error(env, nullptr, error_message); + } + } + + napi_status ReportResult() const { return node_api_status_; } + + napi_env env() const { return env_; } + + bool has_node_api_error() const { return node_api_status_ != napi_ok; } + + napi_status node_api_status() const { return node_api_status_; } + + void set_node_api_status(napi_status status) { node_api_status_ = status; } + + bool has_embedding_error() const { + return embedding_status_ != NodeStatus::kOk; + } + + NodeStatus embedding_status() const { return embedding_status_; } + + void set_embedding_status(NodeStatus status) { + embedding_status_ = status; + ThrowLastErrorMessage(env_, node_embedding_last_error_message_get()); + node_api_status_ = napi_pending_exception; + } + + void set_embedding_status(const NodeExpected& expected) { + set_embedding_status(expected.status()); + } + + NodeApiErrorHandlerBase(const NodeApiErrorHandlerBase&) = delete; + NodeApiErrorHandlerBase& operator=(const NodeApiErrorHandlerBase&) = delete; + + protected: + NodeApiErrorHandlerBase(napi_env env) : env_(env) {} + + private: + // Declaring operator new and delete as deleted is not spec compliant. + // Therefore declare them private instead to disable dynamic alloc + void* operator new(size_t size) { std::abort(); } + void* operator new[](size_t size) { std::abort(); } + void operator delete(void*, size_t) { std::abort(); } + void operator delete[](void*, size_t) { std::abort(); } + + private: + napi_env env_{nullptr}; + napi_status node_api_status_{napi_ok}; + NodeStatus embedding_status_{NodeStatus::kOk}; +}; + +template +class NodeApiErrorHandler : public NodeApiErrorHandlerBase { + public: + NodeApiErrorHandler(napi_env env) : NodeApiErrorHandlerBase(env) {} + + TValue ReportResult() const { + if constexpr (std::is_same_v) { + return NodeApiErrorHandlerBase::ReportResult(); + } else { + GetAndThrowLastErrorMessage(env()); + return TValue(); + } + } +}; + +// Wraps the Node.js platform instance. +class NodePlatform { + public: + explicit NodePlatform(node_embedding_platform platform) + : platform_(platform) {} + + NodePlatform(NodePlatform&& other) = default; + NodePlatform& operator=(NodePlatform&& other) = default; + + ~NodePlatform() { + if (platform_) { + NodeStatus status = node_embedding_platform_delete(platform_.ptr()); + if (NodeEmbeddingErrorHandler* current = + NodeEmbeddingErrorHandler::current()) { + current->set_embedding_status(status); + } + } + } + + explicit operator bool() const { return static_cast(platform_); } + + operator node_embedding_platform() const { return platform_.ptr(); } + + node_embedding_platform Detach() { + return std::exchange(platform_, nullptr).ptr(); + } + + static NodeExpected RunMain( + NodeArgs args, + NodeConfigurePlatformCallback configure_platform, + NodeConfigureRuntimeCallback configure_runtime) { + return NodeExpected( + node_embedding_main_run(NODE_EMBEDDING_VERSION, + args.argc(), + args.argv(), + configure_platform.callback(), + configure_platform.data(), + configure_runtime.callback(), + configure_runtime.data())); + } + + static NodeExpected Create( + NodeArgs args, NodeConfigurePlatformCallback configure_platform) { + node_embedding_platform platform{}; + NodeStatus status = + node_embedding_platform_create(NODE_EMBEDDING_VERSION, + args.argc(), + args.argv(), + configure_platform.callback(), + configure_platform.data(), + &platform); + if (status != NodeStatus::kOk) { + return NodeExpected(status); + } + return NodeExpected(NodePlatform(platform)); + } + + NodeExpected> GetArgs() const { + int32_t argc = 0; + const char** argv = nullptr; + NodeStatus status = node_embedding_platform_get_parsed_args( + platform_.ptr(), &argc, &argv, nullptr, nullptr); + if (status != NodeStatus::kOk) { + return NodeExpected>(status); + } + return NodeExpected>( + std::vector(argv, argv + argc)); + } + + NodeExpected> GetRuntimeArgs() const { + int32_t argc = 0; + const char** argv = nullptr; + NodeStatus status = node_embedding_platform_get_parsed_args( + platform_.ptr(), nullptr, nullptr, &argc, &argv); + if (status != NodeStatus::kOk) { + return NodeExpected>(status); + } + return NodeExpected>( + std::vector(argv, argv + argc)); + } + + private: + NodePointer platform_; +}; + +// The NodePlatform that does not delete the platform on destruction. +class NodeDetachedPlatform : public NodePlatform { + public: + explicit NodeDetachedPlatform(node_embedding_platform platform) + : NodePlatform(platform) {} + + ~NodeDetachedPlatform() { Detach(); } +}; + +class NodePlatformConfig { + public: + explicit NodePlatformConfig(node_embedding_platform_config platform_config) + : platform_config_(platform_config) {} + + NodePlatformConfig(NodePlatformConfig&& other) = default; + NodePlatformConfig& operator=(NodePlatformConfig&& other) = default; + + operator node_embedding_platform_config() const { + return platform_config_.ptr(); + } + + NodeExpected SetFlags(NodePlatformFlags flags) const { + return NodeExpected(node_embedding_platform_config_set_flags( + platform_config_.ptr(), flags)); + } + + private: + NodePointer platform_config_{}; +}; + +class NodeApiScope { + public: + static NodeExpected Open(node_embedding_runtime runtime) { + node_embedding_node_api_scope node_api_scope{}; + napi_env env{}; + NodeStatus status = node_embedding_runtime_node_api_scope_open( + runtime, &node_api_scope, &env); + if (status != NodeStatus::kOk) { + return NodeExpected(status); + } + return NodeExpected( + NodeApiScope(runtime, node_api_scope, env)); + } + + explicit NodeApiScope(node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope, + napi_env env) + : runtime_(runtime), node_api_scope_(node_api_scope), env_(env) {} + + NodeApiScope(NodeApiScope&&) = default; + NodeApiScope& operator=(NodeApiScope&&) = default; + + ~NodeApiScope() { + if (runtime_) { + NodeEmbeddingErrorHandler::CurrentSetEmbeddingStatus( + node_embedding_runtime_node_api_scope_close(runtime_.ptr(), + node_api_scope_.ptr())); + } + } + + napi_env env() const { return env_.ptr(); } + + private: + NodePointer runtime_; + NodePointer node_api_scope_; + NodePointer env_; +}; + +class NodeRuntime { + public: + static NodeExpected Run( + const NodePlatform& platform, + NodeConfigureRuntimeCallback configure_runtime) { + return NodeExpected(node_embedding_runtime_run( + static_cast(platform), + configure_runtime.callback(), + configure_runtime.data())); + } + + static NodeExpected Create( + const NodePlatform& platform, + NodeConfigureRuntimeCallback configure_runtime) { + node_embedding_runtime runtime; + NodeStatus status = + node_embedding_runtime_create(platform, + configure_runtime.callback(), + configure_runtime.data(), + &runtime); + if (status != NodeStatus::kOk) { + return NodeExpected(status); + } + return NodeExpected(NodeRuntime(runtime)); + } + + explicit NodeRuntime(node_embedding_runtime runtime) : runtime_(runtime) {} + + NodeRuntime(NodeRuntime&& other) = default; + NodeRuntime& operator=(NodeRuntime&& other) = default; + + ~NodeRuntime() { + if (runtime_) { + NodeEmbeddingErrorHandler::CurrentSetEmbeddingStatus( + node_embedding_runtime_delete(runtime_.ptr())); + ; + } + } + + operator node_embedding_runtime() const { return runtime_.ptr(); } + + node_embedding_runtime Detach() { + return std::exchange(runtime_, nullptr).ptr(); + } + + NodeExpected RunEventLoop() const { + return NodeExpected( + node_embedding_runtime_event_loop_run(runtime_.ptr())); + } + + NodeExpected TerminateEventLoop() const { + return NodeExpected( + node_embedding_runtime_event_loop_terminate(runtime_.ptr())); + } + + NodeExpected RunEventLoopOnce() const { + bool has_more_work{}; + NodeStatus status = node_embedding_runtime_event_loop_run_once( + runtime_.ptr(), &has_more_work); + if (status != NodeStatus::kOk) { + return NodeExpected(status); + } + return NodeExpected(has_more_work); + } + + NodeExpected RunEventLoopNoWait() const { + bool has_more_work{}; + NodeStatus status = node_embedding_runtime_event_loop_run_no_wait( + runtime_.ptr(), &has_more_work); + if (status != NodeStatus::kOk) { + return NodeExpected(status); + } + return NodeExpected(has_more_work); + } + + NodeExpected RunNodeApi(NodeRunNodeApiCallback run_node_api) const { + return NodeExpected(node_embedding_runtime_node_api_run( + runtime_.ptr(), run_node_api.callback(), run_node_api.data())); + } + + NodeExpected OpenNodeApiScope() const { + return NodeApiScope::Open(runtime_.ptr()); + } + + private: + NodePointer runtime_{}; +}; + +class NodeDetachedRuntime : public NodeRuntime { + public: + explicit NodeDetachedRuntime(node_embedding_runtime runtime) + : NodeRuntime(runtime) {} + ~NodeDetachedRuntime() { Detach(); } +}; + +class NodeRuntimeConfig { + public: + explicit NodeRuntimeConfig(node_embedding_runtime_config runtime_config) + : runtime_config_(runtime_config) {} + + NodeRuntimeConfig(NodeRuntimeConfig&& other) = default; + NodeRuntimeConfig& operator=(NodeRuntimeConfig&& other) = default; + + operator node_embedding_runtime_config() const { + return runtime_config_.ptr(); + } + + NodeExpected SetNodeApiVersion(int32_t node_api_version) const { + return NodeExpected( + node_embedding_runtime_config_set_node_api_version( + runtime_config_.ptr(), node_api_version)); + } + + NodeExpected SetFlags(NodeRuntimeFlags flags) const { + return NodeExpected( + node_embedding_runtime_config_set_flags(runtime_config_.ptr(), flags)); + } + + NodeExpected SetArgs(NodeArgs args, NodeArgs runtime_args) const { + return NodeExpected( + node_embedding_runtime_config_set_args(runtime_config_.ptr(), + args.argc(), + args.argv(), + runtime_args.argc(), + runtime_args.argv())); + } + + NodeExpected OnPreload(NodePreloadCallback preload) const { + return NodeExpected( + node_embedding_runtime_config_on_preload(runtime_config_.ptr(), + preload.callback(), + preload.data(), + preload.data_release())); + } + + NodeExpected OnStartExecution( + NodeStartExecutionCallback start_execution) const { + return NodeExpected(node_embedding_runtime_config_on_loading( + runtime_config_.ptr(), + start_execution.callback(), + start_execution.data(), + start_execution.data_release())); + } + + NodeExpected OnLoaded( + NodeHandleExecutionResultCallback handle_start_result) const { + return NodeExpected(node_embedding_runtime_config_on_loaded( + runtime_config_.ptr(), + handle_start_result.callback(), + handle_start_result.data(), + handle_start_result.data_release())); + } + + NodeExpected AddModule(std::string_view module_name, + NodeInitializeModuleCallback init_module, + int32_t moduleNodeApiVersion) const { + return NodeExpected( + node_embedding_runtime_config_add_module(runtime_config_.ptr(), + module_name.data(), + init_module.callback(), + init_module.data(), + init_module.data_release(), + moduleNodeApiVersion)); + } + + NodeExpected SetTaskRunner(NodePostTaskCallback post_task) const { + return NodeExpected(node_embedding_runtime_config_set_task_runner( + runtime_config_.ptr(), + post_task.callback(), + post_task.data(), + post_task.data_release())); + } + + private: + NodePointer runtime_config_{}; +}; + +template +class NodeFunctorInvoker< + node_embedding_platform_configure_callback, + TFunctor, + std::enable_if_t, + TFunctor, + const NodePlatformConfig&>>> { + public: + static NodeStatus Invoke(void* cb_data, + node_embedding_platform_config platform_config) { + TFunctor* callback = reinterpret_cast(cb_data); + NodePlatformConfig platform_config_cpp(platform_config); + NodeExpected result_cpp = (*callback)(platform_config_cpp); + return result_cpp.status(); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_runtime_configure_callback, + TFunctor, + std::enable_if_t, + TFunctor, + const NodePlatform&, + const NodeRuntimeConfig&>>> { + public: + static NodeStatus Invoke(void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeDetachedPlatform platform_cpp(platform); + NodeRuntimeConfig runtime_config_cpp(runtime_config); + NodeExpected result_cpp = + (*callback)(platform_cpp, runtime_config_cpp); + return result_cpp.status(); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_runtime_preload_callback, + TFunctor, + std::enable_if_t>> { + public: + static void Invoke(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeDetachedRuntime runtime_cpp(runtime); + (*callback)(runtime_cpp, env, process, require); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_runtime_loading_callback, + TFunctor, + std::enable_if_t>> { + public: + static napi_value Invoke(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeDetachedRuntime runtime_cpp(runtime); + return (*callback)(runtime_cpp, env, process, require, run_cjs); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_runtime_loaded_callback, + TFunctor, + std::enable_if_t>> { + public: + static void Invoke(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value execution_result) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeDetachedRuntime runtime_cpp(runtime); + (*callback)(runtime_cpp, env, execution_result); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_module_initialize_callback, + TFunctor, + std::enable_if_t>> { + public: + static napi_value Invoke(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + const char* module_name, + napi_value exports) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeDetachedRuntime runtime_cpp(runtime); + return (*callback)(runtime_cpp, env, module_name, exports); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_task_run_callback, + TFunctor, + std::enable_if_t, TFunctor>>> { + public: + static NodeStatus Invoke(void* cb_data) { + TFunctor* callback = reinterpret_cast(cb_data); + return (*callback)().status(); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_task_post_callback, + TFunctor, + std::enable_if_t, + TFunctor, + NodeRunTaskCallback>>> { + public: + static NodeStatus Invoke( + void* cb_data, + node_embedding_task_run_callback run_task, + void* task_data, + node_embedding_data_release_callback release_task_data, + bool* is_posted) { + TFunctor* callback = reinterpret_cast(cb_data); + NodeExpected result_cpp = (*callback)( + NodeRunTaskCallback(run_task, task_data, release_task_data)); + if (is_posted != nullptr) { + *is_posted = result_cpp.value(); + } + return result_cpp.status(); + } +}; + +template +class NodeFunctorInvoker< + node_embedding_node_api_run_callback, + TFunctor, + std::enable_if_t>> { + public: + static void Invoke(void* cb_data, napi_env env) { + TFunctor* callback = reinterpret_cast(cb_data); + (*callback)(env); + } +}; + +} // namespace node::embedding + +#endif // SRC_NODE_EMBEDDING_API_CPP_H_ diff --git a/test/README.md b/test/README.md index a79c97e941d542..54075e63911da5 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/.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/README.md b/test/embedding/README.md new file mode 100644 index 00000000000000..4f82228484b47d --- /dev/null +++ b/test/embedding/README.md @@ -0,0 +1,124 @@ +# C embedding API + +This file is an overview for C embedding API. +It is mostly to catch all the work in progress. + +## The API overview + +### Error handling API +- `node_embedding_on_error` + +### Global platform API +- `node_embedding_set_api_version` +- `node_embedding_run_main` +- `node_embedding_create_platform` +- `node_embedding_delete_platform` +- `node_embedding_platform_set_flags` +- `node_embedding_platform_get_parsed_args` + +### Runtime API +- `node_embedding_run_runtime` +- `node_embedding_create_runtime` +- `node_embedding_delete_runtime` +- `node_embedding_runtime_set_flags` +- `node_embedding_runtime_set_args` +- `node_embedding_runtime_on_preload` +- `node_embedding_runtime_on_start_execution` +- `node_embedding_runtime_add_module` +- [ ] add API to handle unhandled exceptions + +### Runtime API to run event loops +- `node_embedding_on_wake_up_event_loop` +- `node_embedding_run_event_loop` +- `node_embedding_complete_event_loop` +- [ ] add API for emitting `beforeExit` event +- [ ] add API for emitting `exit` event +- [ ] add API to stop the event loop + +### Runtime API to interop with Node-API +- `node_embedding_run_node_api` +- `node_embedding_open_node_api_scope` +- `node_embedding_close_node_api_scope` + +## Functional overview + +### Platform API + +- Global handling of C API errors +- [ ] Global handling of Node.js/V8 errors (is it possible?) +- [ ] Global handling of unhandled JS errors + +- API version +- Node-API version + +- Global platform initialization +- Global platform uninitialization +- Parsing the command line parameters +- Controlled by the platform flags +- Get parsed command line arguments + +- [ ] Can we initialize platform again if it returns early? +- [-] Will not support: custom thread pool + +- [ ] API v-table to avoid DLL named function binding + +### Runtime API + +- Runtime initialization +- Runtime uninitialization +- Set runtime args +- Set runtime flags +- Register a preload callback +- Register start execution callback +- [ ] Load default Node.js snapshot without the custom start execution callback +- [ ] Get the returned value from the start execution +- Register linked modules + +- [ ] Runtime handling of API errors (is it possible?) +- [ ] Runtime handling of Node.js/V8 errors (is it possible?) +- [ ] Runtime handling of unhandled JS errors + +- [ ] Events on Runtime destruction (beforeExit, exit) +- [ ] Main vs secondary Runtimes +- [ ] Worker thread runtimes (is it possible?) +- [ ] Associate Inspector with the Runtime +- [ ] Inspector for the secondary Runtimes +- [ ] Runtime destructor to clean up all related resources including the + pending tasks. +- [ ] Exit process only for unhandled main runtime errors. +- [ ] Have an internal list of all runtimes in the system. + It is a list of all secondary runtimes attached to the main runtime. + +### Event Loop API + +- Run event loop while it has events (default) +- Run event loop once and block for an event +- Run event loop once and no wait +- Run event loop till completion +- [ ] Interrupt the event loop (uv_stop stops the default loop and then + the loop resets it) +- [ ] Loop while some condition is true +- [ ] Custom foreground task runner (is it possible?) +- [ ] Notify if event loop has work to do (based on Electron PollEvents) + It must be blocked while the loop is running + It is only for the main runtime +- [ ] V8 Microtask posting and draining +- [ ] Complete the loop immediately if needed +- [ ] Protect from nested uv_run calls +- [ ] How to post setImmediate from another thread? + +### Node-API integration + +- Run Node-API code as a lambda +- Explicitly open and close the Node-API scope +- [ ] Handle JS errors + +### Test TODOs + +- [ ] Test passing the V8 thread pool size. +- [ ] Add tests based on the environment and platform `cctest`s. +- [ ] Enable the test_main_modules_node_api test. +- [ ] Test failure in Preload callback. +- [ ] Test failure in linked modules. +- [ ] Add a test that handles JS errors. +- [ ] Make sure that the the delete calls match the create calls. diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc index f481efc59a6eba..482451285625bf 100644 --- a/test/embedding/embedtest.cc +++ b/test/embedding/embedtest.cc @@ -2,8 +2,8 @@ #undef NDEBUG #endif #include -#include "executable_wrapper.h" #include "node.h" +#include "uv.h" #include @@ -27,10 +27,7 @@ static int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector& args, const std::vector& exec_args); -NODE_MAIN(int argc, node::argv_type raw_argv[]) { - char** argv = nullptr; - node::FixupMain(argc, raw_argv, &argv); - +int32_t test_main_cpp_api(int32_t argc, const char* argv[]) { std::vector args(argv, argv + argc); std::shared_ptr result = node::InitializeOncePerProcess( diff --git a/test/embedding/embedtest_c_api.c b/test/embedding/embedtest_c_api.c new file mode 100644 index 00000000000000..a865058e4f5735 --- /dev/null +++ b/test/embedding/embedtest_c_api.c @@ -0,0 +1,262 @@ +#include "embedtest_c_api_common.h" + +static napi_status CallMe(node_embedding_runtime runtime, napi_env env); +static napi_status WaitMe(node_embedding_runtime runtime, napi_env env); +static napi_status WaitMeWithCheese(node_embedding_runtime runtime, + napi_env env); + +static node_embedding_status ConfigurePlatform( + void* cb_data, node_embedding_platform_config platform_config) { + return node_embedding_platform_config_set_flags( + platform_config, node_embedding_platform_flags_disable_node_options_env); +} + +static void HandleExecutionResult(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value execution_result) { + napi_status status = napi_ok; + NODE_API_CALL(CallMe(runtime, env)); + NODE_API_CALL(WaitMe(runtime, env)); + NODE_API_CALL(WaitMeWithCheese(runtime, env)); +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static node_embedding_status ConfigureRuntime( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + NODE_EMBEDDING_CALL(node_embedding_runtime_config_on_loaded( + runtime_config, HandleExecutionResult, NULL, NULL)); +on_exit: + return embedding_status; +} + +int32_t test_main_c_api(int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_main_run(NODE_EMBEDDING_VERSION, + argc, + argv, + ConfigurePlatform, + NULL, + ConfigureRuntime, + NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +static napi_status CallMe(node_embedding_runtime runtime, napi_env env) { + napi_status status = napi_ok; + napi_value global, cb, key; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_string_utf8(env, "callMe", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(napi_get_property(env, global, key, &cb)); + + napi_valuetype cb_type; + NODE_API_CALL(napi_typeof(env, cb, &cb_type)); + + // Only evaluate callMe if it was registered as a function. + if (cb_type == napi_function) { + napi_value undef; + NODE_API_CALL(napi_get_undefined(env, &undef)); + napi_value arg; + NODE_API_CALL( + napi_create_string_utf8(env, "called", NAPI_AUTO_LENGTH, &arg)); + napi_value result; + NODE_API_CALL(napi_call_function(env, undef, cb, 1, &arg, &result)); + + char buf[32]; + size_t len; + NODE_API_CALL(napi_get_value_string_utf8(env, result, buf, 32, &len)); + if (strcmp(buf, "called you") != 0) { + NODE_API_FAIL("Invalid value received: %s\n", buf); + } + printf("%s", buf); + } else if (cb_type != napi_undefined) { + NODE_API_FAIL("Invalid callMe value\n"); + } +on_exit: + return status; +} + +// TODO: remove static variables +char callback_buf[32]; +size_t callback_buf_len; +static napi_value c_cb(napi_env env, napi_callback_info info) { + napi_status status = napi_ok; + size_t argc = 1; + napi_value arg; + NODE_API_CALL(napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + NODE_API_CALL(napi_get_value_string_utf8( + env, arg, callback_buf, 32, &callback_buf_len)); +on_exit: + GetAndThrowLastErrorMessage(env, status); + return NULL; +} + +static napi_status WaitMe(node_embedding_runtime runtime, napi_env env) { + napi_status status = napi_ok; + node_embedding_status embedding_status = node_embedding_status_ok; + napi_value global, cb, key; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_string_utf8(env, "waitMe", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(napi_get_property(env, global, key, &cb)); + + napi_valuetype cb_type; + NODE_API_CALL(napi_typeof(env, cb, &cb_type)); + + // Only evaluate waitMe if it was registered as a function. + if (cb_type == napi_function) { + napi_value undef; + NODE_API_CALL(napi_get_undefined(env, &undef)); + napi_value args[2]; + NODE_API_CALL( + napi_create_string_utf8(env, "waited", NAPI_AUTO_LENGTH, &args[0])); + NODE_API_CALL(napi_create_function( + env, "wait_cb", strlen("wait_cb"), c_cb, NULL, &args[1])); + + napi_value result; + memset(callback_buf, 0, 32); + NODE_API_CALL(napi_call_function(env, undef, cb, 2, args, &result)); + if (strcmp(callback_buf, "waited you") == 0) { + NODE_API_FAIL("Anachronism detected: %s\n", callback_buf); + } + + bool has_more_events = true; + while (has_more_events) { + NODE_EMBEDDING_CALL(node_embedding_runtime_event_loop_run_once( + runtime, &has_more_events)); + } + + if (strcmp(callback_buf, "waited you") != 0) { + NODE_API_FAIL("Invalid value received: %s\n", callback_buf); + } + printf("%s", callback_buf); + } else if (cb_type != napi_undefined) { + NODE_API_FAIL("Invalid waitMe value\n"); + } + +on_exit: + if (embedding_status != node_embedding_status_ok) { + NODE_API_FAIL("WaitMe failed: %s\n", + node_embedding_last_error_message_get()); + } + return status; +} + +typedef enum { + kPromiseStatePending, + kPromiseStateFulfilled, + kPromiseStateRejected, +} PromiseState; + +static napi_value OnFulfilled(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value result; + void* data; + napi_get_cb_info(env, info, &argc, &result, NULL, &data); + napi_get_value_string_utf8(env, result, callback_buf, 32, &callback_buf_len); + *(PromiseState*)data = kPromiseStateFulfilled; + return NULL; +} + +static napi_value OnRejected(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value result; + void* data; + napi_get_cb_info(env, info, &argc, &result, NULL, &data); + napi_get_value_string_utf8(env, result, callback_buf, 32, &callback_buf_len); + *(PromiseState*)data = kPromiseStateRejected; + return NULL; +} + +static napi_status WaitMeWithCheese(node_embedding_runtime runtime, + napi_env env) { + napi_status status = napi_ok; + node_embedding_status embedding_status = node_embedding_status_ok; + PromiseState promise_state = kPromiseStatePending; + napi_value global, wait_promise, undefined; + napi_value on_fulfilled, on_rejected; + napi_value then_args[2]; + const char* expected; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_undefined(env, &undefined)); + + NODE_API_CALL( + napi_get_named_property(env, global, "waitPromise", &wait_promise)); + + napi_valuetype wait_promise_type; + NODE_API_CALL(napi_typeof(env, wait_promise, &wait_promise_type)); + + // Only evaluate waitPromise if it was registered as a function. + if (wait_promise_type == napi_undefined) { + return napi_ok; + } else if (wait_promise_type != napi_function) { + NODE_API_FAIL("Invalid waitPromise value\n"); + } + + napi_value arg; + NODE_API_CALL(napi_create_string_utf8(env, "waited", NAPI_AUTO_LENGTH, &arg)); + + memset(callback_buf, 0, 32); + napi_value promise; + NODE_API_CALL( + napi_call_function(env, undefined, wait_promise, 1, &arg, &promise)); + + if (strcmp(callback_buf, "waited with cheese") == 0) { + NODE_API_FAIL("Anachronism detected: %s\n", callback_buf); + } + + bool is_promise; + NODE_API_CALL(napi_is_promise(env, promise, &is_promise)); + if (!is_promise) { + NODE_API_FAIL("Result is not a Promise\n"); + } + + NODE_API_CALL(napi_create_function(env, + "onFulfilled", + NAPI_AUTO_LENGTH, + OnFulfilled, + &promise_state, + &on_fulfilled)); + NODE_API_CALL(napi_create_function(env, + "rejected", + NAPI_AUTO_LENGTH, + OnRejected, + &promise_state, + &on_rejected)); + napi_value then; + NODE_API_CALL(napi_get_named_property(env, promise, "then", &then)); + then_args[0] = on_fulfilled; + then_args[1] = on_rejected; + NODE_API_CALL(napi_call_function(env, promise, then, 2, then_args, NULL)); + + bool has_more_events = true; + while (has_more_events && promise_state == kPromiseStatePending) { + NODE_EMBEDDING_CALL( + node_embedding_runtime_event_loop_run_once(runtime, &has_more_events)); + } + + expected = (promise_state == kPromiseStateFulfilled) + ? "waited with cheese" + : "waited without cheese"; + + if (strcmp(callback_buf, expected) != 0) { + NODE_API_FAIL("Invalid value received: %s\n", callback_buf); + } + printf("%s", callback_buf); + +on_exit: + if (embedding_status != node_embedding_status_ok) { + ThrowLastErrorMessage(env, + "WaitMeWithCheese failed: %s", + node_embedding_last_error_message_get()); + } + return status; +} diff --git a/test/embedding/embedtest_c_api_common.c b/test/embedding/embedtest_c_api_common.c new file mode 100644 index 00000000000000..f87d72176b86bd --- /dev/null +++ b/test/embedding/embedtest_c_api_common.c @@ -0,0 +1,141 @@ +#include "embedtest_c_api_common.h" + +// #include +#include +// #include +// #include + +const char* main_script = + "globalThis.require = require('module').createRequire(process.execPath);\n" + "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };\n" + "require('vm').runInThisContext(process.argv[1]);"; + +napi_status GetAndThrowLastErrorMessage(napi_env env, napi_status status) { + if (status == napi_ok) { + return status; + } + const napi_extended_error_info* error_info; + napi_get_last_error_info(env, &error_info); + ThrowLastErrorMessage(env, error_info->error_message); + return status; +} + +void ThrowLastErrorMessage(napi_env env, const char* format, ...) { + bool is_pending; + napi_is_exception_pending(env, &is_pending); + /* If an exception is already pending, don't rethrow it */ + if (is_pending) { + return; + } + char error_message_buf[1024]; + char* error_message = error_message_buf; + const char* error_format = format != NULL ? format : "empty error message"; + + // TODO: use dynamic_string_t + va_list args1; + va_start(args1, format); + va_list args2; // Required for some compilers like GCC since we go over the + // args twice. + va_copy(args2, args1); + int32_t error_message_size = vsnprintf(NULL, 0, error_format, args1); + if (error_message_size > 1024 - 1) { + error_message = (char*)malloc(error_message_size + 1); + } + va_end(args1); + vsnprintf(error_message, error_message_size + 1, error_format, args2); + va_end(args2); + + napi_throw_error(env, NULL, error_message); + + if (error_message_size > 1024 - 1) { + free(error_message); + } +} + +static napi_value LoadScript(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs) { + napi_status status = napi_ok; + napi_value script_value, null_value; + napi_value result = NULL; + const char* script = (const char*)cb_data; + NODE_API_CALL( + napi_create_string_utf8(env, script, NAPI_AUTO_LENGTH, &script_value)); + NODE_API_CALL(napi_get_null(env, &null_value)); + NODE_API_CALL( + napi_call_function(env, null_value, run_cjs, 1, &script_value, &result)); +on_exit: + GetAndThrowLastErrorMessage(env, status); + return result; +} + +node_embedding_status LoadUtf8Script( + node_embedding_runtime_config runtime_config, const char* script) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_runtime_config_on_loading( + runtime_config, LoadScript, (void*)script, NULL)); +on_exit: + return embedding_status; +} + +int32_t StatusToExitCode(node_embedding_status status) { + if (status == node_embedding_status_ok) { + return 0; + } else if ((status & node_embedding_status_error_exit_code) != 0) { + return status & ~node_embedding_status_error_exit_code; + } + return 1; +} + +node_embedding_status PrintErrorMessage(const char* exe_name, + node_embedding_status status) { + const char* error_message = node_embedding_last_error_message_get(); + if (status != node_embedding_status_ok) { + fprintf(stderr, "%s: %s\n", exe_name, error_message); + } else if (error_message != NULL) { + fprintf(stdout, "%s", error_message); + } + node_embedding_last_error_message_set(NULL); + return status; +} + +void dynamic_string_init(dynamic_string_t* str) { + if (str == NULL) return; + str->data = str->buffer; + str->length = 0; + str->buffer[0] = '\0'; +} + +void dynamic_string_destroy(dynamic_string_t* str) { + if (str == NULL) return; + if (str->data != str->buffer) { + free(str->data); + } + dynamic_string_init(str); +} + +void dynamic_string_set(dynamic_string_t* str, const char* value) { + if (str == NULL) return; + dynamic_string_destroy(str); + dynamic_string_append(str, value); +} + +void dynamic_string_append(dynamic_string_t* str, const char* value) { + if (str == NULL) return; + if (value == NULL) return; + size_t new_length = str->length + strlen(value); + char* new_data = (new_length + 1 > DYNAMIC_STRING_BUFFER_SIZE) + ? new_data = (char*)malloc(new_length + 1) + : str->buffer; + if (new_data == NULL) return; + strcpy(new_data, str->data); + strcpy(new_data + str->length, value); + if (str->data != str->buffer) { + free(str->data); + } + str->data = new_data; + str->length = new_length; +} diff --git a/test/embedding/embedtest_c_api_common.h b/test/embedding/embedtest_c_api_common.h new file mode 100644 index 00000000000000..da13a2c0ae2020 --- /dev/null +++ b/test/embedding/embedtest_c_api_common.h @@ -0,0 +1,86 @@ +#ifndef TEST_EMBEDDING_EMBEDTEST_C_API_COMMON_H_ +#define TEST_EMBEDDING_EMBEDTEST_C_API_COMMON_H_ + +#define NAPI_EXPERIMENTAL + +#include +#include +#include +#include + +extern const char* main_script; + +int32_t StatusToExitCode(node_embedding_status status); + +node_embedding_status PrintErrorMessage(const char* exe_name, + node_embedding_status status); + +node_embedding_status LoadUtf8Script( + node_embedding_runtime_config runtime_config, const char* script); + +napi_status GetAndThrowLastErrorMessage(napi_env env, napi_status status); + +void ThrowLastErrorMessage(napi_env env, const char* format, ...); + +#define DYNAMIC_STRING_BUFFER_SIZE 256 + +typedef struct { + char* data; + size_t length; + char buffer[DYNAMIC_STRING_BUFFER_SIZE]; +} dynamic_string_t; + +void dynamic_string_init(dynamic_string_t* str); +void dynamic_string_destroy(dynamic_string_t* str); +void dynamic_string_set(dynamic_string_t* str, const char* value); +void dynamic_string_append(dynamic_string_t* str, const char* value); + +//============================================================================== +// Error handing macros +//============================================================================== + +#define NODE_API_CALL(expr) \ + do { \ + status = (expr); \ + if (status != napi_ok) { \ + goto on_exit; \ + } \ + } while (0) + +// TODO: The GetAndThrowLastErrorMessage is not going to work in this mode. +// TODO: Use the napi_is_exception_pending to check if there is an exception +#define NODE_API_ASSERT(expr) \ + do { \ + if (!(expr)) { \ + status = napi_generic_failure; \ + napi_throw_error(env, NULL, "Failed: (" #expr ")"); \ + goto on_exit; \ + } \ + } while (0) + +#define NODE_API_FAIL(format, ...) \ + do { \ + status = napi_generic_failure; \ + ThrowLastErrorMessage(env, format, ##__VA_ARGS__); \ + goto on_exit; \ + } while (0) + +#define NODE_EMBEDDING_CALL(expr) \ + do { \ + embedding_status = (expr); \ + if (embedding_status != node_embedding_status_ok) { \ + goto on_exit; \ + } \ + } while (0) + +#define NODE_EMBEDDING_ASSERT(expr) \ + do { \ + if (!(expr)) { \ + embedding_status = node_embedding_status_generic_error; \ + node_embedding_last_error_message_set_format( \ + "Failed: %s\nFile: %s\nLine: %d\n", #expr, __FILE__, __LINE__); \ + goto on_exit; \ + } \ + } while (0) + +#endif // TEST_EMBEDDING_EMBEDTEST_C_API_COMMON_H_ diff --git a/test/embedding/embedtest_c_api_env.c b/test/embedding/embedtest_c_api_env.c new file mode 100644 index 00000000000000..21343ff2dcafbf --- /dev/null +++ b/test/embedding/embedtest_c_api_env.c @@ -0,0 +1,140 @@ +#include "embedtest_c_api_common.h" + +static node_embedding_status ConfigureRuntimeNoBrowserGlobals( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_runtime_config_set_flags( + runtime_config, node_embedding_runtime_flags_no_browser_globals)); + NODE_EMBEDDING_CALL(LoadUtf8Script( + runtime_config, + "const assert = require('assert');\n" + "const path = require('path');\n" + "const relativeRequire =" + " require('module').createRequire(path.join(process.cwd(), 'stub.js'));\n" + "const { intrinsics, nodeGlobals } =" + " relativeRequire('./test/common/globals');\n" + "const items = Object.getOwnPropertyNames(globalThis);\n" + "const leaks = [];\n" + "for (const item of items) {\n " + " if (intrinsics.has(item)) {\n" + " continue;\n" + " }\n" + " if (nodeGlobals.has(item)) {\n" + " continue;\n" + " }\n" + " if (item === '$jsDebugIsRegistered') {\n" + " continue;\n" + " }\n" + " leaks.push(item);\n" + "}\n" + "assert.deepStrictEqual(leaks, []);\n")); +on_exit: + return embedding_status; +} + +// Test the no_browser_globals option. +int32_t test_main_c_api_env_no_browser_globals(int32_t argc, + const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_main_run(NODE_EMBEDDING_VERSION, + argc, + argv, + NULL, + NULL, + ConfigureRuntimeNoBrowserGlobals, + NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +static node_embedding_status ConfigureRuntimeWithEsmLoader( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(LoadUtf8Script( + runtime_config, + "globalThis.require =" + " require('module').createRequire(process.execPath);\n" + "const { SourceTextModule } = require('node:vm');\n" + "(async () => {\n" + " const stmString = 'globalThis.importResult = import(\"\")';\n" + " const m = new SourceTextModule(stmString, {\n" + " importModuleDynamically: (async () => {\n" + " const m = new SourceTextModule('');\n" + " await m.link(() => 0);\n" + " await m.evaluate();\n" + " return m.namespace;\n" + " }),\n" + " });\n" + " await m.link(() => 0);\n" + " await m.evaluate();\n" + " delete globalThis.importResult;\n" + " process.exit(0);\n" + "})();\n")); +on_exit: + return embedding_status; +} + +// Test ESM loaded +int32_t test_main_c_api_env_with_esm_loader(int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + // We currently cannot pass argument to command line arguments to the runtime. + // They must be parsed by the platform. + const char* argv2[64]; + for (int32_t i = 0; i < argc; ++i) { + argv2[i] = argv[i]; + } + argv2[argc] = "--experimental-vm-modules"; + NODE_EMBEDDING_CALL(node_embedding_main_run(NODE_EMBEDDING_VERSION, + argc + 1, + argv2, + NULL, + NULL, + ConfigureRuntimeWithEsmLoader, + NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +static node_embedding_status ConfigureRuntimeWithNoEsmLoader( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + return LoadUtf8Script( + runtime_config, + "globalThis.require =" + " require('module').createRequire(process.execPath);\n" + "const { SourceTextModule } = require('node:vm');\n" + "(async () => {\n" + " const stmString = 'globalThis.importResult = import(\"\")';\n" + " const m = new SourceTextModule(stmString, {\n" + " importModuleDynamically: (async () => {\n" + " const m = new SourceTextModule('');\n" + " await m.link(() => 0);\n" + " await m.evaluate();\n" + " return m.namespace;\n" + " }),\n" + " });\n" + " await m.link(() => 0);\n" + " await m.evaluate();\n" + " delete globalThis.importResult;\n" + "})();\n"); +} + +// Test ESM loaded +int32_t test_main_c_api_env_with_no_esm_loader(int32_t argc, + const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_main_run(NODE_EMBEDDING_VERSION, + argc, + argv, + NULL, + NULL, + ConfigureRuntimeWithNoEsmLoader, + NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} diff --git a/test/embedding/embedtest_c_api_modules.c b/test/embedding/embedtest_c_api_modules.c new file mode 100644 index 00000000000000..7fc701b99c5f1f --- /dev/null +++ b/test/embedding/embedtest_c_api_modules.c @@ -0,0 +1,162 @@ +#include +#include "embedtest_c_api_common.h" + +typedef struct { + uv_mutex_t mutex; + int32_t greeter_module_init_call_count; + int32_t replicator_module_init_call_count; +} test_data_t; + +static napi_value GreeterFunction(napi_env env, napi_callback_info info) { + napi_status status = napi_ok; + char greeting_buf[256] = {0}; + strcpy(greeting_buf, "Hello, "); + size_t offset = strlen(greeting_buf); + napi_value result = NULL; + + napi_value arg; + size_t arg_count = 1; + NODE_API_CALL(napi_get_cb_info(env, info, &arg_count, &arg, NULL, NULL)); + NODE_API_CALL(napi_get_value_string_utf8( + env, arg, greeting_buf + offset, 256 - offset, NULL)); + NODE_API_CALL( + napi_create_string_utf8(env, greeting_buf, NAPI_AUTO_LENGTH, &result)); + +on_exit: + GetAndThrowLastErrorMessage(env, status); + return result; +} + +static napi_value InitGreeterModule(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + const char* module_name, + napi_value exports) { + napi_status status = napi_ok; + test_data_t* test_data = (test_data_t*)cb_data; + uv_mutex_lock(&test_data->mutex); + ++test_data->greeter_module_init_call_count; + uv_mutex_unlock(&test_data->mutex); + + napi_value greet_func; + NODE_API_CALL(napi_create_function( + env, "greet", NAPI_AUTO_LENGTH, GreeterFunction, NULL, &greet_func)); + NODE_API_CALL(napi_set_named_property(env, exports, "greet", greet_func)); + +on_exit: + GetAndThrowLastErrorMessage(env, status); + return exports; +} + +static napi_value ReplicatorFunction(napi_env env, napi_callback_info info) { + napi_status status = napi_ok; + char replicator_buf[256] = {0}; + + napi_value result = NULL; + napi_value arg; + size_t arg_count = 1; + NODE_API_CALL(napi_get_cb_info(env, info, &arg_count, &arg, NULL, NULL)); + size_t str_size = 0; + NODE_API_CALL( + napi_get_value_string_utf8(env, arg, replicator_buf, 256, &str_size)); + strcpy(replicator_buf + str_size, " "); + strncpy(replicator_buf + str_size + 1, replicator_buf, str_size); + NODE_API_CALL( + napi_create_string_utf8(env, replicator_buf, NAPI_AUTO_LENGTH, &result)); + +on_exit: + GetAndThrowLastErrorMessage(env, status); + return result; +} + +static napi_value InitReplicatorModule(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + const char* module_name, + napi_value exports) { + napi_status status = napi_ok; + test_data_t* test_data = (test_data_t*)cb_data; + uv_mutex_lock(&test_data->mutex); + ++test_data->replicator_module_init_call_count; + uv_mutex_unlock(&test_data->mutex); + + napi_value greet_func; + NODE_API_CALL(napi_create_function(env, + "replicate", + NAPI_AUTO_LENGTH, + ReplicatorFunction, + NULL, + &greet_func)); + NODE_API_CALL(napi_set_named_property(env, exports, "replicate", greet_func)); + +on_exit: + GetAndThrowLastErrorMessage(env, status); + return exports; +} + +static void OnPreload(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require) { + napi_status status = napi_ok; + napi_value global; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_set_named_property(env, global, "process", process)); +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static node_embedding_status ConfigureRuntime( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_runtime_config_on_preload( + runtime_config, OnPreload, NULL, NULL)); + NODE_EMBEDDING_CALL( + node_embedding_runtime_config_add_module(runtime_config, + "greeter_module", + InitGreeterModule, + cb_data, + NULL, + NAPI_VERSION)); + NODE_EMBEDDING_CALL( + node_embedding_runtime_config_add_module(runtime_config, + "replicator_module", + InitReplicatorModule, + cb_data, + NULL, + NAPI_VERSION)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); +on_exit: + return embedding_status; +} + +int32_t test_main_c_api_linked_modules(int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + int32_t expected_greeter_module_init_call_count = atoi(argv[2]); + int32_t expected_replicator_module_init_call_count = atoi(argv[2]); + + test_data_t test_data = {0}; + uv_mutex_init(&test_data.mutex); + + NODE_EMBEDDING_ASSERT(argc == 4); + + NODE_EMBEDDING_CALL(node_embedding_main_run(NODE_EMBEDDING_VERSION, + argc, + argv, + NULL, + NULL, + ConfigureRuntime, + &test_data)); + + NODE_EMBEDDING_ASSERT(test_data.greeter_module_init_call_count == + expected_greeter_module_init_call_count); + NODE_EMBEDDING_ASSERT(test_data.replicator_module_init_call_count == + expected_replicator_module_init_call_count); + +on_exit: + uv_mutex_destroy(&test_data.mutex); + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} diff --git a/test/embedding/embedtest_c_api_preload.c b/test/embedding/embedtest_c_api_preload.c new file mode 100644 index 00000000000000..b1aba720d8f7b9 --- /dev/null +++ b/test/embedding/embedtest_c_api_preload.c @@ -0,0 +1,37 @@ +#include "embedtest_c_api_common.h" + +static void OnPreload(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require) { + napi_status status = napi_ok; + napi_value global, value; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_int32(env, 42, &value)); + NODE_API_CALL(napi_set_named_property(env, global, "preloadValue", value)); +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static node_embedding_status ConfigureRuntime( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_runtime_config_on_preload( + runtime_config, OnPreload, NULL, NULL)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); +on_exit: + return embedding_status; +} + +// Tests that the same preload callback is called from the main thread and from +// the worker thread. +int32_t test_main_c_api_preload(int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_main_run( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, ConfigureRuntime, NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} diff --git a/test/embedding/embedtest_c_api_run_main.c b/test/embedding/embedtest_c_api_run_main.c new file mode 100644 index 00000000000000..e854ce39ffa1d7 --- /dev/null +++ b/test/embedding/embedtest_c_api_run_main.c @@ -0,0 +1,12 @@ +#include "embedtest_c_api_common.h" + +// The simplest Node.js embedding scenario where the Node.js main function is +// invoked from the libnode shared library as it would be run from the Node.js +// CLI. No embedder customizations are available in this case. +int32_t test_main_c_api_nodejs_main(int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_main_run( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, NULL, NULL)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} diff --git a/test/embedding/embedtest_c_api_threading.c b/test/embedding/embedtest_c_api_threading.c new file mode 100644 index 00000000000000..ad85a86dedcf7e --- /dev/null +++ b/test/embedding/embedtest_c_api_threading.c @@ -0,0 +1,600 @@ +#include "embedtest_c_api_common.h" + +#include // Tests in this file use libuv for threading. + +//============================================================================== +// Test that multiple runtimes can be run at the same time in their own threads. +//============================================================================== + +typedef struct { + node_embedding_platform platform; + uv_mutex_t mutex; + int32_t global_count; + node_embedding_status global_status; +} test_data1_t; + +static void OnLoaded1(void* cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value execution_result) { + napi_status status = napi_ok; + test_data1_t* data = (test_data1_t*)cb_data; + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "myCount", &my_count)); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + uv_mutex_lock(&data->mutex); + ++data->global_count; + uv_mutex_unlock(&data->mutex); +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static node_embedding_status ConfigureRuntime( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + // Inspector can be associated with only one + // runtime in the process. + NODE_EMBEDDING_CALL(node_embedding_runtime_config_set_flags( + runtime_config, + node_embedding_runtime_flags_default | + node_embedding_runtime_flags_no_create_inspector)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + NODE_EMBEDDING_CALL(node_embedding_runtime_config_on_loaded( + runtime_config, OnLoaded1, cb_data, NULL)); +on_exit: + return embedding_status; +} + +static void ThreadCallback1(void* arg) { + test_data1_t* data = (test_data1_t*)arg; + node_embedding_status status = + node_embedding_runtime_run(data->platform, ConfigureRuntime, arg); + if (status != node_embedding_status_ok) { + uv_mutex_lock(&data->mutex); + data->global_status = status; + uv_mutex_unlock(&data->mutex); + } +} + +#define TEST_THREAD_COUNT1 12 + +// Tests that multiple runtimes can be run at the same time in their own +// threads. The test creates 12 threads and 12 runtimes. Each runtime runs in it +// own thread. +int32_t test_main_c_api_threading_runtime_per_thread(int32_t argc, + const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + size_t thread_count = TEST_THREAD_COUNT1; + uv_thread_t threads[TEST_THREAD_COUNT1] = {0}; + test_data1_t test_data = {0}; + uv_mutex_init(&test_data.mutex); + + NODE_EMBEDDING_CALL(node_embedding_platform_create( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, &test_data.platform)); + if (test_data.platform == NULL) { + goto on_exit; // early return + } + + for (size_t i = 0; i < thread_count; i++) { + uv_thread_create(&threads[i], ThreadCallback1, &test_data); + } + + for (size_t i = 0; i < thread_count; i++) { + uv_thread_join(&threads[i]); + } + + // TODO: Add passing error message + NODE_EMBEDDING_CALL(test_data.global_status); + NODE_EMBEDDING_CALL(node_embedding_platform_delete(test_data.platform)); + + fprintf(stdout, "%d\n", test_data.global_count); + +on_exit: + uv_mutex_destroy(&test_data.mutex); + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +//============================================================================== +// Test that multiple runtimes can run in the same thread. +//============================================================================== + +static node_embedding_status ConfigureRuntime2( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + // Inspector can be associated with only one runtime in the process. + NODE_EMBEDDING_CALL(node_embedding_runtime_config_set_flags( + runtime_config, + node_embedding_runtime_flags_default | + node_embedding_runtime_flags_no_create_inspector)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); +on_exit: + return embedding_status; +} + +static void IncMyCount2(void* cb_data, napi_env env) { + napi_status status = napi_ok; + napi_value undefined, global, func; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_API_ASSERT(func_type == napi_function); + NODE_API_CALL(napi_call_function(env, undefined, func, 0, NULL, NULL)); +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static void SumMyCount2(void* cb_data, napi_env env) { + napi_status status = napi_ok; + int32_t* global_count = (int32_t*)cb_data; + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "myCount", &my_count)); + + napi_valuetype my_count_type; + NODE_API_CALL(napi_typeof(env, my_count, &my_count_type)); + NODE_API_ASSERT(my_count_type == napi_number); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + + *global_count += count; +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +// Tests that multiple runtimes can run in the same thread. +// The runtime scope must be opened and closed for each use. +// There are 12 runtimes that share the same main thread. +int32_t test_main_c_api_threading_several_runtimes_per_thread( + int32_t argc, const char* argv[]) { + node_embedding_status embedding_status = node_embedding_status_ok; + const size_t runtime_count = 12; + bool more_work = false; + int32_t global_count = 0; + node_embedding_runtime runtimes[12] = {0}; + + node_embedding_platform platform; + NODE_EMBEDDING_CALL(node_embedding_platform_create( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, &platform)); + if (platform == NULL) { + goto on_exit; // early return + } + + for (size_t i = 0; i < runtime_count; ++i) { + NODE_EMBEDDING_CALL(node_embedding_runtime_create( + platform, ConfigureRuntime2, NULL, &runtimes[i])); + + NODE_EMBEDDING_CALL( + node_embedding_runtime_node_api_run(runtimes[i], IncMyCount2, NULL)); + } + + do { + more_work = false; + for (size_t i = 0; i < runtime_count; ++i) { + bool has_more_work = false; + NODE_EMBEDDING_CALL(node_embedding_runtime_event_loop_run_no_wait( + runtimes[i], &has_more_work)); + more_work |= has_more_work; + } + } while (more_work); + + for (size_t i = 0; i < runtime_count; ++i) { + NODE_EMBEDDING_CALL(node_embedding_runtime_node_api_run( + runtimes[i], SumMyCount2, &global_count)); + NODE_EMBEDDING_CALL(node_embedding_runtime_event_loop_run(runtimes[i])); + NODE_EMBEDDING_CALL(node_embedding_runtime_delete(runtimes[i])); + } + + NODE_EMBEDDING_CALL(node_embedding_platform_delete(platform)); + + fprintf(stdout, "%d\n", global_count); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +//============================================================================== +// Test that a runtime can be invoked from different threads as long as only +// one thread uses it at a time. +//============================================================================== + +typedef struct { + node_embedding_runtime runtime; + uv_mutex_t mutex; + int32_t result_count; + node_embedding_status result_status; +} thread_data3; + +static node_embedding_status ConfigureRuntime3( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + return LoadUtf8Script(runtime_config, main_script); +} + +static void RunNodeApi3(void* cb_data, napi_env env) { + napi_status status = napi_ok; + thread_data3* data = (thread_data3*)cb_data; + napi_value undefined, global, func, my_count; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_API_ASSERT(func_type == napi_function); + NODE_API_CALL(napi_call_function(env, undefined, func, 0, NULL, NULL)); + + NODE_API_CALL(napi_get_named_property(env, global, "myCount", &my_count)); + napi_valuetype count_type; + NODE_API_CALL(napi_typeof(env, my_count, &count_type)); + NODE_API_ASSERT(count_type == napi_number); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + data->result_count = count; +on_exit: + GetAndThrowLastErrorMessage(env, status); +} + +static void ThreadCallback3(void* arg) { + thread_data3* data = (thread_data3*)arg; + uv_mutex_lock(&data->mutex); + node_embedding_status status = + node_embedding_runtime_node_api_run(data->runtime, RunNodeApi3, arg); + if (status != node_embedding_status_ok) { + data->result_status = status; + } + uv_mutex_unlock(&data->mutex); +} + +// Tests that a runtime can be invoked from different threads as long as only +// one thread uses it at a time. +int32_t test_main_c_api_threading_runtime_in_several_threads( + int32_t argc, const char* argv[]) { + // Use mutex to synchronize access to the runtime. + node_embedding_status embedding_status = node_embedding_status_ok; + thread_data3 data = {0}; + uv_mutex_init(&data.mutex); + + const size_t thread_count = 5; + uv_thread_t threads[5] = {0}; + + node_embedding_platform platform; + NODE_EMBEDDING_CALL(node_embedding_platform_create( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, &platform)); + if (platform == NULL) { + goto on_exit; // early return + } + + NODE_EMBEDDING_CALL(node_embedding_runtime_create( + platform, ConfigureRuntime3, NULL, &data.runtime)); + + for (size_t i = 0; i < thread_count; ++i) { + uv_thread_create(&threads[i], ThreadCallback3, &data); + } + + for (size_t i = 0; i < thread_count; ++i) { + uv_thread_join(&threads[i]); + } + + NODE_EMBEDDING_CALL(data.result_status); + NODE_EMBEDDING_CALL(node_embedding_runtime_event_loop_run(data.runtime)); + + fprintf(stdout, "%d\n", data.result_count); +on_exit: + uv_mutex_destroy(&data.mutex); + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} + +//============================================================================== +// Test that a runtime's event loop can be called from the UI thread event loop. +//============================================================================== + +//------------------------------------------------------------------------------ +// Simulation of the UI queue. +//------------------------------------------------------------------------------ + +struct test_deq_item_t { + struct test_deq_item_t* prev; + struct test_deq_item_t* next; +}; +typedef struct test_deq_item_t test_deq_item_t; + +static void test_deq_item_init(test_deq_item_t* item) { + item->prev = NULL; + item->next = NULL; +} + +typedef struct { + test_deq_item_t* head; + test_deq_item_t* tail; +} test_deq_t; + +static void test_deq_init(test_deq_t* deq) { + deq->head = NULL; + deq->tail = NULL; +} + +static void test_deq_push_back(test_deq_t* deq, test_deq_item_t* item) { + if (deq->tail == NULL) { + deq->head = item; + deq->tail = item; + } else { + deq->tail->next = item; + item->prev = deq->tail; + deq->tail = item; + } +} + +static test_deq_item_t* test_deq_pop_front(test_deq_t* deq) { + test_deq_item_t* item = deq->head; + if (item != NULL) { + deq->head = item->next; + if (deq->head == NULL) { + deq->tail = NULL; + } else { + deq->head->prev = NULL; + } + } + return item; +} + +static bool test_deq_empty(test_deq_t* deq) { + return deq->head == NULL; +} + +typedef struct { + test_deq_item_t deq_item; + void* task_data; + void (*run_task)(void*); + void (*release_task_data)(void*); +} test_task_t; + +static void test_task_init(test_task_t* task, + void* task_data, + void (*run_task)(void*), + void (*release_task_data)(void*)) { + test_deq_item_init(&task->deq_item); + task->task_data = task_data; + task->run_task = run_task; + task->release_task_data = release_task_data; +} + +typedef struct { + uv_mutex_t mutex; + uv_cond_t wakeup; + test_deq_t tasks; + bool is_finished; +} test_ui_queue_t; + +static void test_ui_queue_init(test_ui_queue_t* queue) { + uv_mutex_init(&queue->mutex); + uv_cond_init(&queue->wakeup); + test_deq_init(&queue->tasks); + queue->is_finished = false; +} + +static void test_ui_queue_post_task(test_ui_queue_t* queue, test_task_t* task) { + uv_mutex_lock(&queue->mutex); + if (!queue->is_finished) { + test_deq_push_back(&queue->tasks, &task->deq_item); + uv_cond_signal(&queue->wakeup); + } + uv_mutex_unlock(&queue->mutex); +} + +static void test_ui_queue_run(test_ui_queue_t* queue) { + for (;;) { + uv_mutex_lock(&queue->mutex); + while (!queue->is_finished && test_deq_empty(&queue->tasks)) { + uv_cond_wait(&queue->wakeup, &queue->mutex); + } + if (queue->is_finished) { + uv_mutex_unlock(&queue->mutex); + return; + } + test_task_t* task = (test_task_t*)test_deq_pop_front(&queue->tasks); + uv_mutex_unlock(&queue->mutex); + if (task->run_task != NULL) { + task->run_task(task->task_data); + } + if (task->release_task_data != NULL) { + task->release_task_data(task->task_data); + } + } +} + +static void test_ui_queue_stop(test_ui_queue_t* queue) { + uv_mutex_lock(&queue->mutex); + if (!queue->is_finished) { + queue->is_finished = true; + uv_cond_signal(&queue->wakeup); + } + uv_mutex_unlock(&queue->mutex); +} + +static void test_ui_queue_destroy(test_ui_queue_t* queue) { + uv_mutex_destroy(&queue->mutex); + uv_cond_destroy(&queue->wakeup); +} + +//------------------------------------------------------------------------------ +// Test data and task implementation. +//------------------------------------------------------------------------------ + +typedef struct { + test_ui_queue_t ui_queue; + node_embedding_runtime runtime; +} test_data4_t; + +typedef struct { + test_task_t parent_task; + node_embedding_task_run_callback run_task; + void* task_data; + node_embedding_data_release_callback release_task_data; + test_data4_t* test_data; +} test_ext_task_t; + +static void RunTestTask4(void* cb_data) { + node_embedding_status embedding_status = node_embedding_status_ok; + napi_status status = napi_ok; + test_ext_task_t* test_task = (test_ext_task_t*)cb_data; + + test_task->run_task(test_task->task_data); // TODO: handle result + + // Check myCount and stop the processing when it reaches 5. + int32_t count; + node_embedding_runtime runtime = test_task->test_data->runtime; + node_embedding_node_api_scope node_api_scope; + napi_env env; + NODE_EMBEDDING_CALL(node_embedding_runtime_node_api_scope_open( + runtime, &node_api_scope, &env)); + + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "myCount", &my_count)); + napi_valuetype count_type; + NODE_API_CALL(napi_typeof(env, my_count, &count_type)); + NODE_API_ASSERT(count_type == napi_number); + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + + NODE_EMBEDDING_CALL( + node_embedding_runtime_node_api_scope_close(runtime, node_api_scope)); + if (count == 5) { + NODE_EMBEDDING_CALL(node_embedding_runtime_event_loop_run(runtime)); + fprintf(stdout, "%d\n", count); + test_ui_queue_stop(&test_task->test_data->ui_queue); + } +on_exit: + if (embedding_status != node_embedding_status_ok) { + ThrowLastErrorMessage(env, + "RunTestTask failed: %s\n", + node_embedding_last_error_message_get()); + status = napi_generic_failure; + } + GetAndThrowLastErrorMessage(env, status); +} + +static void ReleaseTestTask4(void* cb_data) { + test_ext_task_t* test_task = (test_ext_task_t*)cb_data; + if (test_task->release_task_data != NULL) { + test_task->release_task_data(test_task->task_data); + } + free(test_task); +} + +// The callback will be invoked from the runtime's event loop observer thread. +// It must schedule the work to the UI thread's event loop. +static node_embedding_status PostTask4( + void* cb_data, + node_embedding_task_run_callback run_task, + void* task_data, + node_embedding_data_release_callback release_task_data, + bool* succeeded) { + test_data4_t* test_data = (test_data4_t*)cb_data; + test_ext_task_t* test_task = + (test_ext_task_t*)malloc(sizeof(test_ext_task_t)); + if (test_task == NULL) { + return node_embedding_status_out_of_memory; + } + memset(test_task, 0, sizeof(test_ext_task_t)); + test_task_init( + &test_task->parent_task, test_task, RunTestTask4, ReleaseTestTask4); + test_task->run_task = run_task; + test_task->task_data = task_data; + test_task->release_task_data = release_task_data; + test_task->test_data = test_data; + + test_ui_queue_post_task(&test_data->ui_queue, &test_task->parent_task); + if (succeeded != NULL) { + *succeeded = true; + } + return node_embedding_status_ok; +} + +static node_embedding_status ConfigureRuntime4( + void* cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) { + node_embedding_status embedding_status = node_embedding_status_ok; + NODE_EMBEDDING_CALL(node_embedding_runtime_config_set_task_runner( + runtime_config, PostTask4, cb_data, NULL)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); +on_exit: + return embedding_status; +} + +static void StartProcessing4(void* cb_data) { + napi_status status = napi_ok; + node_embedding_status embedding_status = node_embedding_status_ok; + test_data4_t* data = (test_data4_t*)cb_data; + node_embedding_node_api_scope node_api_scope; + napi_env env; + NODE_EMBEDDING_CALL(node_embedding_runtime_node_api_scope_open( + data->runtime, &node_api_scope, &env)); + + napi_value undefined, global, func; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_API_ASSERT(func_type == napi_function); + NODE_API_CALL(napi_call_function(env, undefined, func, 0, NULL, NULL)); + + NODE_EMBEDDING_CALL(node_embedding_runtime_node_api_scope_close( + data->runtime, node_api_scope)); +on_exit: + // TODO: extract into a separate function + if (embedding_status != node_embedding_status_ok) { + ThrowLastErrorMessage(env, + "RunTestTask failed: %s\n", + node_embedding_last_error_message_get()); + status = napi_pending_exception; + } + GetAndThrowLastErrorMessage(env, status); +} + +// Tests that a the runtime's event loop can be called from the UI thread +// event loop. +int32_t test_main_c_api_threading_runtime_in_ui_thread(int32_t argc, + const char* argv[]) { + // A simulation of the UI thread's event loop implemented as a dispatcher + // queue. Note that it is a very simplistic implementation not suitable + // for the real apps. + node_embedding_status embedding_status = node_embedding_status_ok; + test_data4_t test_data = {0}; + test_ui_queue_init(&test_data.ui_queue); + + node_embedding_platform platform; + NODE_EMBEDDING_CALL(node_embedding_platform_create( + NODE_EMBEDDING_VERSION, argc, argv, NULL, NULL, &platform)); + if (platform == NULL) { + goto on_exit; // early return + } + + NODE_EMBEDDING_CALL(node_embedding_runtime_create( + platform, ConfigureRuntime4, &test_data, &test_data.runtime)); + + // The initial task starts the JS code that then will do the timer + // scheduling. The timer supposed to be handled by the runtime's event loop. + test_task_t task; + test_task_init(&task, &test_data, StartProcessing4, NULL); + test_ui_queue_post_task(&test_data.ui_queue, &task); + + test_ui_queue_run(&test_data.ui_queue); + test_ui_queue_destroy(&test_data.ui_queue); + + NODE_EMBEDDING_CALL(node_embedding_runtime_delete(test_data.runtime)); + NODE_EMBEDDING_CALL(node_embedding_platform_delete(platform)); +on_exit: + return StatusToExitCode(PrintErrorMessage(argv[0], embedding_status)); +} diff --git a/test/embedding/embedtest_c_cpp_api.cc b/test/embedding/embedtest_c_cpp_api.cc new file mode 100644 index 00000000000000..4ce11e3dc2e7fd --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api.cc @@ -0,0 +1,247 @@ +#include "embedtest_c_cpp_api_common.h" + +#include +#include +#include +#include + +namespace node::embedding { + +static napi_status CallMe(const NodeRuntime& runtime, napi_env env); +static napi_status WaitMe(const NodeRuntime& runtime, napi_env env); +static napi_status WaitMeWithCheese(const NodeRuntime& runtime, napi_env env); + +int32_t test_main_c_cpp_api(int32_t argc, const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_EMBEDDING_CALL(NodePlatform::RunMain( + NodeArgs(argc, argv), + [](const NodePlatformConfig& platform_config) { + return platform_config.SetFlags( + NodePlatformFlags::kDisableNodeOptionsEnv); + }, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + NODE_EMBEDDING_CALL(runtime_config.OnLoaded( + [](const NodeRuntime& runtime, napi_env env, napi_value + /*value*/) { + NodeApiErrorHandler error_handler(env); + NODE_API_CALL(CallMe(runtime, env)); + NODE_API_CALL(WaitMe(runtime, env)); + NODE_API_CALL(WaitMeWithCheese(runtime, env)); + })); + return error_handler.ReportResult(); + })); + return error_handler.ReportResult(); +} + +static napi_status CallMe(const NodeRuntime& runtime, napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value global{}, cb{}, key{}; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_string_utf8(env, "callMe", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(napi_get_property(env, global, key, &cb)); + + napi_valuetype cb_type{}; + NODE_API_CALL(napi_typeof(env, cb, &cb_type)); + + // Only evaluate callMe if it was registered as a function. + if (cb_type == napi_function) { + napi_value undef{}; + NODE_API_CALL(napi_get_undefined(env, &undef)); + napi_value arg{}; + NODE_API_CALL( + napi_create_string_utf8(env, "called", NAPI_AUTO_LENGTH, &arg)); + napi_value result{}; + NODE_API_CALL(napi_call_function(env, undef, cb, 1, &arg, &result)); + + char buf[32]{}; + size_t len{}; + NODE_API_CALL(napi_get_value_string_utf8(env, result, buf, 32, &len)); + if (strcmp(buf, "called you") != 0) { + NODE_FAIL("Invalid value received: %s", buf); + } + printf("%s", buf); + } else if (cb_type != napi_undefined) { + NODE_FAIL("Invalid callMe value"); + } + return napi_ok; +} + +// TODO: remove static variables +char callback_buf[32]; +size_t callback_buf_len; +static napi_value c_cb(napi_env env, napi_callback_info info) { + NodeApiErrorHandler error_handler(env); + size_t argc = 1; + napi_value arg{}; + NODE_API_CALL(napi_get_cb_info(env, info, &argc, &arg, nullptr, nullptr)); + NODE_API_CALL(napi_get_value_string_utf8( + env, arg, callback_buf, 32, &callback_buf_len)); + return nullptr; +} + +static napi_status WaitMe(const NodeRuntime& runtime, napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value global{}, cb{}, key{}; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_string_utf8(env, "waitMe", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(napi_get_property(env, global, key, &cb)); + + napi_valuetype cb_type{}; + NODE_API_CALL(napi_typeof(env, cb, &cb_type)); + + // Only evaluate waitMe if it was registered as a function. + if (cb_type == napi_function) { + napi_value undef{}; + NODE_API_CALL(napi_get_undefined(env, &undef)); + napi_value args[2]{}; + NODE_API_CALL( + napi_create_string_utf8(env, "waited", NAPI_AUTO_LENGTH, &args[0])); + NODE_API_CALL(napi_create_function( + env, "wait_cb", strlen("wait_cb"), c_cb, NULL, &args[1])); + + napi_value result{}; + memset(callback_buf, 0, 32); + NODE_API_CALL(napi_call_function(env, undef, cb, 2, args, &result)); + if (strcmp(callback_buf, "waited you") == 0) { + NODE_FAIL("Anachronism detected: %s", callback_buf); + } + + for (;;) { + NodeExpected loop_result = runtime.RunEventLoopOnce(); + if (loop_result.has_error()) { + NODE_FAIL("Failed to run event loop: %s", + NodeErrorInfo::GetLastErrorMessage()); + } + if (!loop_result.value()) { + break; + } + } + + if (strcmp(callback_buf, "waited you") != 0) { + NODE_FAIL("Invalid value received: %s", callback_buf); + } + printf("%s", callback_buf); + } else if (cb_type != napi_undefined) { + NODE_FAIL("Invalid waitMe value"); + } + return napi_ok; +} + +static napi_status WaitMeWithCheese(const NodeRuntime& runtime, napi_env env) { + enum class PromiseState { + kPending, + kFulfilled, + kRejected, + }; + + NodeApiErrorHandler error_handler(env); + PromiseState promise_state = PromiseState::kPending; + napi_value global{}, wait_promise{}, undefined{}; + napi_value on_fulfilled{}, on_rejected{}; + napi_value then_args[2] = {}; + const char* expected{}; + + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_undefined(env, &undefined)); + + NODE_API_CALL( + napi_get_named_property(env, global, "waitPromise", &wait_promise)); + + napi_valuetype wait_promise_type; + NODE_API_CALL(napi_typeof(env, wait_promise, &wait_promise_type)); + + // Only evaluate waitPromise if it was registered as a function. + if (wait_promise_type == napi_undefined) { + return napi_ok; + } else if (wait_promise_type != napi_function) { + NODE_FAIL("Invalid waitPromise value"); + } + + napi_value arg; + NODE_API_CALL(napi_create_string_utf8(env, "waited", NAPI_AUTO_LENGTH, &arg)); + + memset(callback_buf, 0, 32); + napi_value promise; + NODE_API_CALL( + napi_call_function(env, undefined, wait_promise, 1, &arg, &promise)); + + if (strcmp(callback_buf, "waited with cheese") == 0) { + NODE_FAIL("Anachronism detected: %s", callback_buf); + } + + bool is_promise; + NODE_API_CALL(napi_is_promise(env, promise, &is_promise)); + if (!is_promise) { + NODE_FAIL("Result is not a Promise"); + } + + NODE_API_CALL(napi_create_function( + env, + "onFulfilled", + NAPI_AUTO_LENGTH, + [](napi_env env, napi_callback_info info) -> napi_value { + NodeApiErrorHandler error_handler(env); + size_t argc = 1; + napi_value result; + void* data; + NODE_API_CALL( + napi_get_cb_info(env, info, &argc, &result, nullptr, &data)); + NODE_API_CALL(napi_get_value_string_utf8( + env, result, callback_buf, 32, &callback_buf_len)); + *static_cast(data) = PromiseState::kFulfilled; + return nullptr; + }, + &promise_state, + &on_fulfilled)); + NODE_API_CALL(napi_create_function( + env, + "rejected", + NAPI_AUTO_LENGTH, + [](napi_env env, napi_callback_info info) -> napi_value { + NodeApiErrorHandler error_handler(env); + size_t argc = 1; + napi_value result; + void* data; + NODE_API_CALL( + napi_get_cb_info(env, info, &argc, &result, nullptr, &data)); + NODE_API_CALL(napi_get_value_string_utf8( + env, result, callback_buf, 32, &callback_buf_len)); + *static_cast(data) = PromiseState::kRejected; + return nullptr; + }, + &promise_state, + &on_rejected)); + napi_value then; + NODE_API_CALL(napi_get_named_property(env, promise, "then", &then)); + then_args[0] = on_fulfilled; + then_args[1] = on_rejected; + NODE_API_CALL(napi_call_function(env, promise, then, 2, then_args, nullptr)); + + while (promise_state == PromiseState::kPending) { + NodeExpected loop_result = runtime.RunEventLoopOnce(); + if (loop_result.has_error()) { + NODE_FAIL("Failed to run event loop: %s", + NodeErrorInfo::GetLastErrorMessage()); + } + if (!loop_result.value()) { + break; + } + } + + expected = (promise_state == PromiseState::kFulfilled) + ? "waited with cheese" + : "waited without cheese"; + + if (strcmp(callback_buf, expected) != 0) { + NODE_FAIL("Invalid value received: %s", callback_buf); + } + printf("%s", callback_buf); + return napi_ok; +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_common.cc b/test/embedding/embedtest_c_cpp_api_common.cc new file mode 100644 index 00000000000000..c45d203e3a3b43 --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_common.cc @@ -0,0 +1,61 @@ +#include "embedtest_c_cpp_api_common.h" + +#include +#include +#include +#include + +namespace node::embedding { + +const char* main_script = + "globalThis.require = require('module').createRequire(process.execPath);\n" + "globalThis.embedVars = { nön_ascıı: '🏳️‍🌈' };\n" + "require('vm').runInThisContext(process.argv[1]);"; + +napi_status AddUtf8String(std::string& str, napi_env env, napi_value value) { + size_t str_size = 0; + napi_status status = + napi_get_value_string_utf8(env, value, nullptr, 0, &str_size); + if (status != napi_ok) { + return status; + } + size_t offset = str.size(); + str.resize(offset + str_size); + status = napi_get_value_string_utf8( + env, value, &str[0] + offset, str_size + 1, &str_size); + return status; +} + +NodeExpected LoadUtf8Script(const NodeRuntimeConfig& runtime_config, + std::string_view script) { + NodeEmbeddingErrorHandler error_handler; + NODE_EMBEDDING_CALL(runtime_config.OnStartExecution( + [script = std::string(script)](const NodeRuntime& /*runtime*/, + napi_env env, + napi_value /*process*/, + napi_value /*require*/, + napi_value run_cjs) -> napi_value { + NodeApiErrorHandler error_handler(env); + napi_value script_value{}, null_value{}, result{}; + NODE_API_CALL(napi_create_string_utf8( + env, script.c_str(), script.size(), &script_value)); + NODE_API_CALL(napi_get_null(env, &null_value)); + NODE_API_CALL(napi_call_function( + env, null_value, run_cjs, 1, &script_value, &result)); + return result; + })); + return error_handler.ReportResult(); +} + +NodeExpected PrintErrorMessage(std::string_view exe_name, + NodeStatus status) { + std::string error_message = NodeErrorInfo::GetAndClearLastErrorMessage(); + if (status != NodeStatus::kOk) { + fprintf(stderr, "%s: %s\n", exe_name.data(), error_message.c_str()); + } else if (!error_message.empty()) { + fprintf(stdout, "%s\n", error_message.c_str()); + } + return NodeExpected(status); +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_common.h b/test/embedding/embedtest_c_cpp_api_common.h new file mode 100644 index 00000000000000..5841fcfb98c805 --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_common.h @@ -0,0 +1,97 @@ +#ifndef TEST_EMBEDDING_EMBEDTEST_C_CPP_API_COMMON_H_ +#define TEST_EMBEDDING_EMBEDTEST_C_CPP_API_COMMON_H_ + +#define NAPI_EXPERIMENTAL + +#include + +namespace node::embedding { + +extern const char* main_script; + +napi_status AddUtf8String(std::string& str, napi_env env, napi_value value); + +void GetAndThrowLastErrorMessage(napi_env env); + +void ThrowLastErrorMessage(napi_env env, const char* message); + +NodeExpected LoadUtf8Script(const NodeRuntimeConfig& runtime_config, + std::string_view script); + +NodeExpected PrintErrorMessage(std::string_view exe_name, + NodeStatus status); + +class TestExitCodeHandler : public NodeEmbeddingErrorHandler { + public: + TestExitCodeHandler(const char* exe_name) : exe_name_(exe_name) {} + + int32_t ReportResult() { + return PrintErrorMessage(exe_name_, + NodeEmbeddingErrorHandler::ReportResult().status()) + .exit_code(); + } + + private: + const char* exe_name_ = nullptr; +}; + +class TestExitOnErrorHandler : public NodeEmbeddingErrorHandler { + public: + TestExitOnErrorHandler(const char* exe_name) : exe_name_(exe_name) {} + + void ReportResult() { + int32_t exit_code = + PrintErrorMessage(exe_name_, + NodeEmbeddingErrorHandler::ReportResult().status()) + .exit_code(); + if (exit_code != 0) { + exit(exit_code); + } + } + + private: + const char* exe_name_ = nullptr; +}; + +} // namespace node::embedding + +//============================================================================== +// Error handing macros +//============================================================================== + +#define NODE_ASSERT(expr) \ + do { \ + if (!(expr)) { \ + error_handler.SetLastErrorMessage( \ + "Failed: %s\nFile: %s\nLine: %d\n", #expr, __FILE__, __LINE__); \ + return error_handler.ReportResult(); \ + } \ + } while (0) + +#define NODE_FAIL(format, ...) \ + do { \ + error_handler.SetLastErrorMessage("Failed: " format \ + "\nFile: %s\nLine: %d\n", \ + ##__VA_ARGS__, \ + __FILE__, \ + __LINE__); \ + return error_handler.ReportResult(); \ + } while (0) + +#define NODE_API_CALL(expr) \ + do { \ + error_handler.set_node_api_status(expr); \ + if (error_handler.has_node_api_error()) { \ + return error_handler.ReportResult(); \ + } \ + } while (0) + +#define NODE_EMBEDDING_CALL(expr) \ + do { \ + error_handler.set_embedding_status(expr); \ + if (error_handler.has_embedding_error()) { \ + return error_handler.ReportResult(); \ + } \ + } while (0) + +#endif // TEST_EMBEDDING_EMBEDTEST_C_CPP_API_COMMON_H_ diff --git a/test/embedding/embedtest_c_cpp_api_env.cc b/test/embedding/embedtest_c_cpp_api_env.cc new file mode 100644 index 00000000000000..c620b374df9c1b --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_env.cc @@ -0,0 +1,116 @@ +#include "embedtest_c_cpp_api_common.h" + +namespace node::embedding { + +// Test the no_browser_globals option. +int32_t test_main_c_cpp_api_env_no_browser_globals(int32_t argc, + const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_EMBEDDING_CALL(NodePlatform::RunMain( + NodeArgs(argc, argv), + nullptr, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + NODE_EMBEDDING_CALL( + runtime_config.SetFlags(NodeRuntimeFlags::kNoBrowserGlobals)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, + R"JS( +const assert = require('assert'); +const path = require('path'); +const relativeRequire = + require('module').createRequire(path.join(process.cwd(), 'stub.js')); +const { intrinsics, nodeGlobals } = + relativeRequire('./test/common/globals'); +const items = Object.getOwnPropertyNames(globalThis); +const leaks = []; +for (const item of items) { + if (intrinsics.has(item)) { + continue; + } + if (nodeGlobals.has(item)) { + continue; + } + if (item === '$jsDebugIsRegistered') { + continue; + } + leaks.push(item); +} +assert.deepStrictEqual(leaks, []); +)JS")); + return NodeExpected(); + })); + return error_handler.ReportResult(); +} + +// Test ESM loaded +int32_t test_main_c_cpp_api_env_with_esm_loader(int32_t argc, + const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + // We currently cannot pass argument to command line arguments to the runtime. + // They must be parsed by the platform. + std::vector args_vec(argv, argv + argc); + args_vec.push_back("--experimental-vm-modules"); + NodeCStringArray args(args_vec); + NODE_EMBEDDING_CALL( + NodePlatform::RunMain(NodeArgs(args), + nullptr, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + return LoadUtf8Script(runtime_config, + R"JS( +globalThis.require = require('module').createRequire(process.execPath); +const { SourceTextModule } = require('node:vm'); +(async () => { + const stmString = 'globalThis.importResult = import("")'; + const m = new SourceTextModule(stmString, { + importModuleDynamically: (async () => { + const m = new SourceTextModule(''); + await m.link(() => 0); + await m.evaluate(); + return m.namespace; + }), + }); + await m.link(() => 0); + await m.evaluate(); + delete globalThis.importResult; + process.exit(0); +})(); +)JS"); + })); + return error_handler.ReportResult(); +} + +// Test ESM loaded +int32_t test_main_c_cpp_api_env_with_no_esm_loader(int32_t argc, + const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_EMBEDDING_CALL( + NodePlatform::RunMain(NodeArgs(argc, argv), + nullptr, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + return LoadUtf8Script(runtime_config, + R"JS( +globalThis.require = require('module').createRequire(process.execPath); +const { SourceTextModule } = require('node:vm'); +(async () => { + const stmString = 'globalThis.importResult = import("")'; + const m = new SourceTextModule(stmString, { + importModuleDynamically: (async () => { + const m = new SourceTextModule(''); + await m.link(() => 0); + await m.evaluate(); + return m.namespace; + }), + }); + await m.link(() => 0); + await m.evaluate(); + delete globalThis.importResult; +})(); +)JS"); + })); + return error_handler.ReportResult(); +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_modules.cc b/test/embedding/embedtest_c_cpp_api_modules.cc new file mode 100644 index 00000000000000..a13b4bbb2ec0e5 --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_modules.cc @@ -0,0 +1,144 @@ +#include "embedtest_c_cpp_api_common.h" + +#include +#include +#include + +namespace node::embedding { + +namespace { + +class GreeterModule { + public: + explicit GreeterModule(std::atomic* counter_ptr) + : counter_ptr_(counter_ptr) {} + + napi_value operator()(const NodeRuntime& runtime, + napi_env env, + std::string_view module_name, + napi_value exports) { + NodeApiErrorHandler error_handler(env); + counter_ptr_->fetch_add(1); + + napi_value greet_func{}; + NODE_API_CALL(napi_create_function( + env, + "greet", + NAPI_AUTO_LENGTH, + [](napi_env env, napi_callback_info info) -> napi_value { + NodeApiErrorHandler error_handler(env); + std::string greeting = "Hello, "; + napi_value arg{}; + size_t arg_count = 1; + NODE_API_CALL( + napi_get_cb_info(env, info, &arg_count, &arg, nullptr, nullptr)); + NODE_API_CALL(AddUtf8String(greeting, env, arg)); + napi_value result; + NODE_API_CALL(napi_create_string_utf8( + env, greeting.c_str(), greeting.size(), &result)); + return result; + }, + nullptr, + &greet_func)); + NODE_API_CALL(napi_set_named_property(env, exports, "greet", greet_func)); + return exports; + } + + private: + std::atomic* counter_ptr_; +}; + +class ReplicatorModule { + public: + explicit ReplicatorModule(std::atomic* counter_ptr) + : counter_ptr_(counter_ptr) {} + + napi_value operator()(const NodeRuntime& runtime, + napi_env env, + std::string_view module_name, + napi_value exports) { + NodeApiErrorHandler error_handler(env); + counter_ptr_->fetch_add(1); + + napi_value greet_func{}; + NODE_API_CALL(napi_create_function( + env, + "replicate", + NAPI_AUTO_LENGTH, + [](napi_env env, napi_callback_info info) -> napi_value { + NodeApiErrorHandler error_handler(env); + std::string str; + napi_value arg{}; + size_t arg_count = 1; + NODE_API_CALL( + napi_get_cb_info(env, info, &arg_count, &arg, nullptr, nullptr)); + NODE_API_CALL(AddUtf8String(str, env, arg)); + str += " " + str; + napi_value result; + NODE_API_CALL( + napi_create_string_utf8(env, str.c_str(), str.size(), &result)); + return result; + }, + nullptr, + &greet_func)); + NODE_API_CALL( + napi_set_named_property(env, exports, "replicate", greet_func)); + return exports; + } + + private: + std::atomic* counter_ptr_; +}; + +} // namespace + +int32_t test_main_c_cpp_api_linked_modules(int32_t argc, const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_ASSERT(argc == 4); + int32_t expectedGreeterModuleInitCallCount = atoi(argv[2]); + int32_t expectedReplicatorModuleInitCallCount = atoi(argv[2]); + + std::atomic greeterModuleInitCallCount{0}; + std::atomic replicatorModuleInitCallCount{0}; + + NODE_EMBEDDING_CALL(NodePlatform::RunMain( + NodeArgs(argc, argv), + nullptr, + NodeConfigureRuntimeCallback( + [&](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + NODE_EMBEDDING_CALL( + runtime_config.OnPreload([](const NodeRuntime& runtime, + napi_env env, + napi_value process, + napi_value /*require*/ + ) { + napi_value global; + napi_get_global(env, &global); + napi_set_named_property(env, global, "process", process); + })); + + NODE_EMBEDDING_CALL(runtime_config.AddModule( + "greeter_module", + GreeterModule(&greeterModuleInitCallCount), + NAPI_VERSION)); + + NODE_EMBEDDING_CALL(runtime_config.AddModule( + "replicator_module", + ReplicatorModule(&replicatorModuleInitCallCount), + NAPI_VERSION)); + + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + + return error_handler.ReportResult(); + }))); + + NODE_ASSERT(greeterModuleInitCallCount == expectedGreeterModuleInitCallCount); + NODE_ASSERT(replicatorModuleInitCallCount == + expectedReplicatorModuleInitCallCount); + + return error_handler.ReportResult(); +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_preload.cc b/test/embedding/embedtest_c_cpp_api_preload.cc new file mode 100644 index 00000000000000..7c7d2c186fb68c --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_preload.cc @@ -0,0 +1,36 @@ +#include "embedtest_c_cpp_api_common.h" + +namespace node::embedding { + +// Tests that the same preload callback is called from the main thread and from +// the worker thread. +int32_t test_main_c_cpp_api_preload(int32_t argc, const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_EMBEDDING_CALL(NodePlatform::RunMain( + NodeArgs(argc, argv), + nullptr, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + NODE_EMBEDDING_CALL( + runtime_config.OnPreload([](const NodeRuntime& runtime, + napi_env env, + napi_value /*process*/, + napi_value /*require*/ + ) { + NodeApiErrorHandler error_handler(env); + napi_value global, value; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_create_int32(env, 42, &value)); + NODE_API_CALL( + napi_set_named_property(env, global, "preloadValue", value)); + })); + + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + + return error_handler.ReportResult(); + })); + return error_handler.ReportResult(); +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_run_main.cc b/test/embedding/embedtest_c_cpp_api_run_main.cc new file mode 100644 index 00000000000000..126ee51c8f0079 --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_run_main.cc @@ -0,0 +1,15 @@ +#include "embedtest_c_cpp_api_common.h" + +namespace node::embedding { + +// The simplest Node.js embedding scenario where the Node.js main function is +// invoked from the libnode shared library as it would be run from the Node.js +// CLI. No embedder customizations are available in this case. +int32_t test_main_c_cpp_api_nodejs_main(int32_t argc, const char* argv[]) { + TestExitCodeHandler error_handler(argv[0]); + NODE_EMBEDDING_CALL( + NodePlatform::RunMain(NodeArgs(argc, argv), nullptr, nullptr)); + return 0; +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_c_cpp_api_threading.cc b/test/embedding/embedtest_c_cpp_api_threading.cc new file mode 100644 index 00000000000000..68e8e5690a83e2 --- /dev/null +++ b/test/embedding/embedtest_c_cpp_api_threading.cc @@ -0,0 +1,386 @@ +#include "embedtest_c_cpp_api_common.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace node::embedding { + +// Tests that multiple runtimes can be run at the same time in their own +// threads. The test creates 12 threads and 12 runtimes. Each runtime runs in it +// own thread. +int32_t test_main_c_cpp_api_threading_runtime_per_thread(int32_t argc, + const char* argv[]) { + const size_t thread_count = 12; + std::vector threads; + threads.reserve(thread_count); + std::atomic global_count{0}; + std::atomic global_status{NodeStatus::kOk}; + + TestExitCodeHandler error_handler(argv[0]); + { + NodeExpected expected_platform = + NodePlatform::Create(NodeArgs(argc, argv), nullptr); + NODE_EMBEDDING_CALL(expected_platform.status()); + NodePlatform platform = std::move(expected_platform).value(); + if (!platform) { + return error_handler.ReportResult(); // early return + } + + for (size_t i = 0; i < thread_count; i++) { + threads.emplace_back([&platform, &global_count, &global_status] { + NodeExpected result = NodeRuntime::Run( + platform, + [&](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + // Inspector can be associated with only one + // runtime in the process. + NODE_EMBEDDING_CALL(runtime_config.SetFlags( + NodeRuntimeFlags::kDefault | + NodeRuntimeFlags::kNoCreateInspector)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + NODE_EMBEDDING_CALL( + runtime_config.OnLoaded([&](const NodeRuntime& runtime, + napi_env env, + napi_value /*value*/) { + NodeApiErrorHandler error_handler(env); + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property( + env, global, "myCount", &my_count)); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + global_count.fetch_add(count); + })); + return error_handler.ReportResult(); + }); + if (result.has_error()) { + global_status.store(result.status()); + } + }); + } + + for (size_t i = 0; i < thread_count; i++) { + threads[i].join(); + } + + NODE_EMBEDDING_CALL(NodeExpected(global_status.load())); + } + + fprintf(stdout, "%d\n", global_count.load()); + return error_handler.ReportResult(); +} + +// Tests that multiple runtimes can run in the same thread. +// The runtime scope must be opened and closed for each use. +// There are 12 runtimes that share the same main thread. +int32_t test_main_c_cpp_api_threading_several_runtimes_per_thread( + int32_t argc, const char* argv[]) { + const size_t runtime_count = 12; + bool more_work = false; + int32_t global_count = 0; + + TestExitCodeHandler error_handler(argv[0]); + { + NodeExpected expected_platform = + NodePlatform::Create(NodeArgs(argc, argv), nullptr); + NODE_EMBEDDING_CALL(expected_platform.status()); + NodePlatform platform = std::move(expected_platform).value(); + + // We declared list of NodeRuntime after NodePlatform to ensure that they + // are released before the platform. + std::vector runtimes; + runtimes.reserve(runtime_count); + + for (size_t i = 0; i < runtime_count; i++) { + NodeExpected expected_runtime = NodeRuntime::Create( + platform, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + // Inspector can be associated with only one runtime in the process. + NODE_EMBEDDING_CALL( + runtime_config.SetFlags(NodeRuntimeFlags::kDefault | + NodeRuntimeFlags::kNoCreateInspector)); + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + return error_handler.ReportResult(); + }); + + NODE_EMBEDDING_CALL(expected_runtime.status()); + NodeRuntime runtime = std::move(expected_runtime).value(); + + NODE_EMBEDDING_CALL(runtime.RunNodeApi([&](napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value undefined, global, func; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL( + napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_ASSERT(func_type == napi_function); + NODE_API_CALL( + napi_call_function(env, undefined, func, 0, nullptr, nullptr)); + })); + + runtimes.push_back(std::move(runtime)); + } + + do { + more_work = false; + for (const NodeRuntime& runtime : runtimes) { + NodeExpected has_more_work = runtime.RunEventLoopNoWait(); + NODE_EMBEDDING_CALL(has_more_work.status()); + more_work |= has_more_work.value(); + } + } while (more_work); + + for (const NodeRuntime& runtime : runtimes) { + NODE_EMBEDDING_CALL(runtime.RunNodeApi([&](napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL( + napi_get_named_property(env, global, "myCount", &my_count)); + + napi_valuetype my_count_type; + NODE_API_CALL(napi_typeof(env, my_count, &my_count_type)); + NODE_ASSERT(my_count_type == napi_number); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + + global_count += count; + })); + NODE_EMBEDDING_CALL(runtime.RunEventLoop()); + } + } + + fprintf(stdout, "%d\n", global_count); + return error_handler.ReportResult(); +} + +// Tests that a runtime can be invoked from different threads as long as only +// one thread uses it at a time. +int32_t test_main_c_cpp_api_threading_runtime_in_several_threads( + int32_t argc, const char* argv[]) { + // Use mutex to synchronize access to the runtime. + std::mutex mutex; + std::atomic result_count{0}; + std::atomic result_status{NodeStatus::kOk}; + const size_t thread_count = 5; + std::vector threads; + threads.reserve(thread_count); + + TestExitCodeHandler error_handler(argv[0]); + { + NodeExpected expected_platform = + NodePlatform::Create(NodeArgs(argc, argv), nullptr); + NODE_EMBEDDING_CALL(expected_platform.status()); + NodePlatform platform = std::move(expected_platform).value(); + if (!platform) { + return error_handler.ReportResult(); // early return + } + + NodeExpected expected_runtime = NodeRuntime::Create( + platform, + [](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + return LoadUtf8Script(runtime_config, main_script); + }); + + NODE_EMBEDDING_CALL(expected_runtime.status()); + NodeRuntime runtime = std::move(expected_runtime).value(); + + for (size_t i = 0; i < thread_count; i++) { + threads.emplace_back([&runtime, &result_count, &result_status, &mutex] { + std::scoped_lock lock(mutex); + NodeExpected run_result = runtime.RunNodeApi([&](napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value undefined, global, func, my_count; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL( + napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_ASSERT(func_type == napi_function); + NODE_API_CALL( + napi_call_function(env, undefined, func, 0, nullptr, nullptr)); + + NODE_API_CALL( + napi_get_named_property(env, global, "myCount", &my_count)); + napi_valuetype count_type; + NODE_API_CALL(napi_typeof(env, my_count, &count_type)); + NODE_ASSERT(count_type == napi_number); + int32_t count; + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + result_count.store(count); + }); + if (run_result.has_error()) { + result_status.store(run_result.status()); + } + }); + } + + for (size_t i = 0; i < thread_count; i++) { + threads[i].join(); + } + + NODE_EMBEDDING_CALL(NodeExpected(result_status.load())); + NODE_EMBEDDING_CALL(runtime.RunEventLoop()); + } + + fprintf(stdout, "%d\n", result_count.load()); + return error_handler.ReportResult(); +} + +namespace { + +// A simulation of the UI thread's event loop implemented as a dispatcher +// queue. Note that it is a very simplistic implementation not suitable +// for the real apps. +class UIQueue { + public: + void PostTask(std::function&& task) { + std::scoped_lock lock(mutex_); + if (!is_finished_) { + tasks_.push_back(std::move(task)); + wakeup_.notify_one(); + } + } + + void Run() { + for (;;) { + // Invoke task outside of the lock. + std::function task; + { + std::unique_lock lock(mutex_); + wakeup_.wait(lock, [&] { return is_finished_ || !tasks_.empty(); }); + if (is_finished_) return; + task = std::move(tasks_.front()); + tasks_.pop_front(); + } + task(); + } + } + + void Stop() { + std::scoped_lock lock(mutex_); + if (!is_finished_) { + is_finished_ = true; + wakeup_.notify_one(); + } + } + + private: + std::mutex mutex_; + std::condition_variable wakeup_; + std::deque> tasks_; + bool is_finished_{false}; +}; + +} // namespace + +// Tests that a the runtime's event loop can be called from the UI thread +// event loop. +int32_t test_main_c_cpp_api_threading_runtime_in_ui_thread(int32_t argc, + const char* argv[]) { + UIQueue ui_queue; + const char* exe_name = argv[0]; + TestExitCodeHandler error_handler(exe_name); + { + NodeExpected expected_platform = + NodePlatform::Create(NodeArgs(argc, argv), nullptr); + NODE_EMBEDDING_CALL(expected_platform.status()); + NodePlatform platform = std::move(expected_platform).value(); + if (!platform) { + return error_handler.ReportResult(); // early return + } + + NodeRuntime runtime{nullptr}; + NodeExpected expected_runtime = NodeRuntime::Create( + platform, + [&](const NodePlatform& platform, + const NodeRuntimeConfig& runtime_config) { + NodeEmbeddingErrorHandler error_handler; + // The callback will be invoked from the runtime's event loop + // observer thread. It must schedule the work to the UI thread's + // event loop. + NODE_EMBEDDING_CALL(runtime_config.SetTaskRunner( + // We capture the ui_queue by reference here because we + // guarantee it to be alive till the end of the test. In + // real applications, you should use a safer way to + // capture the dispatcher queue. + [&ui_queue, &runtime, exe_name](NodeRunTaskCallback run_task) { + // TODO: figure out the termination scenario. + // TODO: Release run_task data. + ui_queue.PostTask([run_task = + std::make_shared( + std::move(run_task)), + &runtime, + &ui_queue, + exe_name]() { + TestExitOnErrorHandler error_handler(exe_name); + NODE_EMBEDDING_CALL((*run_task)()); + // Check myCount and stop the processing when it reaches 5. + int32_t count{}; + NODE_EMBEDDING_CALL(runtime.RunNodeApi([&](napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value global, my_count; + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL(napi_get_named_property( + env, global, "myCount", &my_count)); + napi_valuetype count_type; + NODE_API_CALL(napi_typeof(env, my_count, &count_type)); + NODE_ASSERT(count_type == napi_number); + NODE_API_CALL(napi_get_value_int32(env, my_count, &count)); + })); + if (count == 5) { + NODE_EMBEDDING_CALL(runtime.RunEventLoop()); + fprintf(stdout, "%d\n", count); + ui_queue.Stop(); + } + }); + return NodeExpected(true); + })); + + NODE_EMBEDDING_CALL(LoadUtf8Script(runtime_config, main_script)); + return error_handler.ReportResult(); + }); + NODE_EMBEDDING_CALL(expected_runtime.status()); + runtime = std::move(expected_runtime).value(); + + // The initial task starts the JS code that then will do the timer + // scheduling. The timer supposed to be handled by the runtime's event loop. + ui_queue.PostTask([&runtime, exe_name]() { + TestExitOnErrorHandler error_handler(exe_name); + NODE_EMBEDDING_CALL(runtime.RunNodeApi([&](napi_env env) { + NodeApiErrorHandler error_handler(env); + napi_value undefined, global, func; + NODE_API_CALL(napi_get_undefined(env, &undefined)); + NODE_API_CALL(napi_get_global(env, &global)); + NODE_API_CALL( + napi_get_named_property(env, global, "incMyCount", &func)); + + napi_valuetype func_type; + NODE_API_CALL(napi_typeof(env, func, &func_type)); + NODE_ASSERT(func_type == napi_function); + NODE_API_CALL( + napi_call_function(env, undefined, func, 0, nullptr, nullptr)); + })); + }); + + ui_queue.Run(); + } + + return error_handler.ReportResult(); +} + +} // namespace node::embedding diff --git a/test/embedding/embedtest_main.cc b/test/embedding/embedtest_main.cc new file mode 100644 index 00000000000000..7fd0df90977ce6 --- /dev/null +++ b/test/embedding/embedtest_main.cc @@ -0,0 +1,113 @@ +#include +#include +#include +#include "executable_wrapper.h" + +int32_t test_main_cpp_api(int32_t argc, const char* argv[]); + +extern "C" int32_t test_main_c_api(int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_nodejs_main(int32_t argc, + const char* argv[]); +extern "C" int32_t test_main_c_api_threading_runtime_per_thread( + int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_threading_several_runtimes_per_thread( + int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_threading_runtime_in_several_threads( + int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_threading_runtime_in_ui_thread( + int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_preload(int32_t argc, const char* argv[]); +extern "C" int32_t test_main_c_api_linked_modules(int32_t argc, + const char* argv[]); +extern "C" int32_t test_main_c_api_env_no_browser_globals(int32_t argc, + const char* argv[]); +extern "C" int32_t test_main_c_api_env_with_esm_loader(int32_t argc, + const char* argv[]); +extern "C" int32_t test_main_c_api_env_with_no_esm_loader(int32_t argc, + const char* argv[]); +namespace node::embedding { + +int32_t test_main_c_cpp_api(int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_nodejs_main(int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_threading_runtime_per_thread(int32_t argc, + const char* argv[]); +int32_t test_main_c_cpp_api_threading_several_runtimes_per_thread( + int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_threading_runtime_in_several_threads( + int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_threading_runtime_in_ui_thread(int32_t argc, + const char* argv[]); +int32_t test_main_c_cpp_api_preload(int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_linked_modules(int32_t argc, const char* argv[]); +int32_t test_main_c_cpp_api_env_no_browser_globals(int32_t argc, + const char* argv[]); +int32_t test_main_c_cpp_api_env_with_esm_loader(int32_t argc, + const char* argv[]); +int32_t test_main_c_cpp_api_env_with_no_esm_loader(int32_t argc, + const char* argv[]); +} // namespace node::embedding + +typedef int32_t (*main_callback)(int32_t argc, const char* argv[]); + +int32_t CallWithoutArg1(main_callback main, int32_t argc, const char* argv[]) { + for (int32_t i = 2; i < argc; i++) { + argv[i - 1] = argv[i]; + } + argv[--argc] = nullptr; + return main(argc, argv); +} + +NODE_MAIN(int32_t argc, node::argv_type raw_argv[]) { + char** argv = nullptr; + node::FixupMain(argc, raw_argv, &argv); + + const std::unordered_map main_map = { + {"cpp-api", test_main_cpp_api}, + {"c-api", test_main_c_api}, + {"c-api-nodejs-main", test_main_c_api_nodejs_main}, + {"c-api-threading-runtime-per-thread", + test_main_c_api_threading_runtime_per_thread}, + {"c-api-threading-several-runtimes-per-thread", + test_main_c_api_threading_several_runtimes_per_thread}, + {"c-api-threading-runtime-in-several-threads", + test_main_c_api_threading_runtime_in_several_threads}, + {"c-api-threading-runtime-in-ui-thread", + test_main_c_api_threading_runtime_in_ui_thread}, + {"c-api-preload", test_main_c_api_preload}, + {"c-api-linked-modules", test_main_c_api_linked_modules}, + {"c-api-env-no-browser-globals", test_main_c_api_env_no_browser_globals}, + {"c-api-env-with-esm-loader", test_main_c_api_env_with_esm_loader}, + {"c-api-env-with-no-esm-loader", test_main_c_api_env_with_no_esm_loader}, + {"c-cpp-api", node::embedding::test_main_c_cpp_api}, + {"c-cpp-api-nodejs-main", + node::embedding::test_main_c_cpp_api_nodejs_main}, + {"c-cpp-api-threading-runtime-per-thread", + node::embedding::test_main_c_cpp_api_threading_runtime_per_thread}, + {"c-cpp-api-threading-several-runtimes-per-thread", + node::embedding:: + test_main_c_cpp_api_threading_several_runtimes_per_thread}, + {"c-cpp-api-threading-runtime-in-several-threads", + node::embedding:: + test_main_c_cpp_api_threading_runtime_in_several_threads}, + {"c-cpp-api-threading-runtime-in-ui-thread", + node::embedding::test_main_c_cpp_api_threading_runtime_in_ui_thread}, + {"c-cpp-api-preload", node::embedding::test_main_c_cpp_api_preload}, + {"c-cpp-api-linked-modules", + node::embedding::test_main_c_cpp_api_linked_modules}, + {"c-cpp-api-env-no-browser-globals", + node::embedding::test_main_c_cpp_api_env_no_browser_globals}, + {"c-cpp-api-env-with-esm-loader", + node::embedding::test_main_c_cpp_api_env_with_esm_loader}, + {"c-cpp-api-env-with-no-esm-loader", + node::embedding::test_main_c_cpp_api_env_with_no_esm_loader}, + }; + if (argc > 1) { + const char* arg1 = argv[1]; + for (const auto& [key, value] : main_map) { + if (key == arg1) { + return CallWithoutArg1(value, argc, (const char**)argv); + } + } + } + return test_main_cpp_api(argc, (const char**)argv); +} 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 71c4f7f324c973..8eae3ef1dd3066 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); @@ -24,149 +23,395 @@ function resolveBuiltBinary(binary) { } const binary = resolveBuiltBinary('embedtest'); +assert.ok(fs.existsSync(binary)); -spawnSyncAndAssert( - binary, - ['console.log(42)'], - { - trim: true, - stdout: '42', - }); +function runTest(testName, spawn, ...args) { + process.stdout.write(`Run test: ${testName} ... `); + spawn(binary, ...args); + console.log('ok'); +} -spawnSyncAndAssert( - binary, - ['console.log(embedVars.nön_ascıı)'], - { - trim: true, - stdout: '🏳️‍🌈', - }); +function runCommonApiTests(apiType) { + runTest( + `${apiType}: console.log`, + spawnSyncAndAssert, + [apiType, 'console.log(42)'], + { + trim: true, + stdout: '42', + } + ); -spawnSyncAndExit( - binary, - ['throw new Error()'], - { - status: 1, - signal: null, - }); + runTest( + `${apiType}: console.log non-ascii`, + spawnSyncAndAssert, + [apiType, 'console.log(embedVars.nön_ascıı)'], + { + trim: true, + stdout: '🏳️‍🌈', + } + ); -spawnSyncAndExit( - binary, - ['require("lib/internal/test/binding")'], - { - status: 1, - signal: null, - }); + runTest( + `${apiType}: throw new Error()`, + spawnSyncAndExit, + [apiType, 'throw new Error()'], + { + status: 1, + signal: null, + } + ); -spawnSyncAndExit( - binary, - ['process.exitCode = 8'], - { - status: 8, - signal: null, - }); - -const fixturePath = JSON.stringify(fixtures.path('exit.js')); -spawnSyncAndExit( - binary, - [`require(${fixturePath})`, 92], - { - status: 92, - signal: null, - }); + runTest( + `${apiType}: require("lib/internal/test/binding")`, + spawnSyncAndExit, + [apiType, 'require("lib/internal/test/binding")'], + { + status: 1, + signal: null, + } + ); -function getReadFileCodeForPath(path) { - return `(require("fs").readFileSync(${JSON.stringify(path)}, "utf8"))`; -} + runTest( + `${apiType}: process.exitCode = 8`, + spawnSyncAndExit, + [apiType, 'process.exitCode = 8'], + { + status: 8, + signal: null, + } + ); -// Basic snapshot support -for (const extraSnapshotArgs of [ - [], ['--embedder-snapshot-as-file'], ['--without-code-cache'], -]) { - // readSync + eval since snapshots don't support userland require() (yet) - const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); - const blobPath = tmpdir.resolve('embedder-snapshot.blob'); - const buildSnapshotExecArgs = [ - `eval(${getReadFileCodeForPath(snapshotFixture)})`, 'arg1', 'arg2', - ]; - const embedTestBuildArgs = [ - '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', - ...extraSnapshotArgs, - ]; - const buildSnapshotArgs = [ - ...buildSnapshotExecArgs, - ...embedTestBuildArgs, - ]; - - const runSnapshotExecArgs = [ - 'arg3', 'arg4', - ]; - const embedTestRunArgs = [ - '--embedder-snapshot-blob', blobPath, - ...extraSnapshotArgs, - ]; - const runSnapshotArgs = [ - ...runSnapshotExecArgs, - ...embedTestRunArgs, - ]; - - fs.rmSync(blobPath, { force: true }); - spawnSyncAndExitWithoutError( - binary, - [ '--', ...buildSnapshotArgs ], - { cwd: tmpdir.path }); - spawnSyncAndAssert( - binary, - [ '--', ...runSnapshotArgs ], - { cwd: tmpdir.path }, - { - stdout(output) { - assert.deepStrictEqual(JSON.parse(output), { - originalArgv: [binary, '__node_anonymous_main', ...buildSnapshotExecArgs], - currentArgv: [binary, ...runSnapshotExecArgs], - }); - return true; - }, - }); -} + { + const fixturePath = JSON.stringify(fixtures.path('exit.js')); + runTest( + `${apiType}: require(fixturePath)`, + spawnSyncAndExit, + [apiType, `require(${fixturePath})`, 92], + { + status: 92, + signal: null, + } + ); + } -// Create workers and vm contexts after deserialization -{ - const snapshotFixture = fixtures.path('snapshot', 'create-worker-and-vm.js'); - const blobPath = tmpdir.resolve('embedder-snapshot.blob'); - const buildSnapshotArgs = [ - `eval(${getReadFileCodeForPath(snapshotFixture)})`, - '--embedder-snapshot-blob', blobPath, '--embedder-snapshot-create', - ]; - const runEmbeddedArgs = [ - '--embedder-snapshot-blob', blobPath, - ]; - - fs.rmSync(blobPath, { force: true }); - - spawnSyncAndExitWithoutError( - binary, - [ '--', ...buildSnapshotArgs ], - { cwd: tmpdir.path }); - spawnSyncAndExitWithoutError( - binary, - [ '--', ...runEmbeddedArgs ], - { cwd: tmpdir.path }); -} + runTest( + `${apiType}: syntax error`, + spawnSyncAndExit, + [apiType, '0syntax_error'], + { + status: 1, + stderr: /SyntaxError: Invalid or unexpected token/, + } + ); -// Guarantee NODE_REPL_EXTERNAL_MODULE won't bypass kDisableNodeOptionsEnv -{ - spawnSyncAndExit( - binary, - ['require("os")'], + // Guarantee NODE_REPL_EXTERNAL_MODULE won't bypass kDisableNodeOptionsEnv + runTest( + `${apiType}: check kDisableNodeOptionsEnv`, + spawnSyncAndExit, + [apiType, 'require("os")'], { env: { ...process.env, - 'NODE_REPL_EXTERNAL_MODULE': 'fs', + NODE_REPL_EXTERNAL_MODULE: 'fs', }, }, { 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', + } + ); +} + +runCommonApiTests('cpp-api'); +runCommonApiTests('c-api'); +runCommonApiTests('c-cpp-api'); + +function getReadFileCodeForPath(path) { + return `(require("fs").readFileSync(${JSON.stringify(path)}, "utf8"))`; } + +function runSnapshotTests(apiType) { + // Basic snapshot support + for (const extraSnapshotArgs of [ + [], + ['--embedder-snapshot-as-file'], + ['--without-code-cache'], + ]) { + // readSync + eval since snapshots don't support userland require() (yet) + const snapshotFixture = fixtures.path('snapshot', 'echo-args.js'); + const blobPath = tmpdir.resolve('embedder-snapshot.blob'); + const buildSnapshotExecArgs = [ + `eval(${getReadFileCodeForPath(snapshotFixture)})`, + 'arg1', + 'arg2', + ]; + const embedTestBuildArgs = [ + '--embedder-snapshot-blob', + blobPath, + '--embedder-snapshot-create', + ...extraSnapshotArgs, + ]; + const buildSnapshotArgs = [...buildSnapshotExecArgs, ...embedTestBuildArgs]; + + const runSnapshotExecArgs = ['arg3', 'arg4']; + const embedTestRunArgs = [ + '--embedder-snapshot-blob', + blobPath, + ...extraSnapshotArgs, + ]; + const runSnapshotArgs = [...runSnapshotExecArgs, ...embedTestRunArgs]; + + fs.rmSync(blobPath, { force: true }); + runTest( + `${apiType}: build basic snapshot ${extraSnapshotArgs.join(' ')}`, + spawnSyncAndExitWithoutError, + [apiType, '--', ...buildSnapshotArgs], + { + cwd: tmpdir.path, + } + ); + + runTest( + `${apiType}: run basic snapshot ${extraSnapshotArgs.join(' ')}`, + spawnSyncAndAssert, + [apiType, '--', ...runSnapshotArgs], + { cwd: tmpdir.path }, + { + stdout(output) { + assert.deepStrictEqual(JSON.parse(output), { + originalArgv: [ + binary, + '__node_anonymous_main', + ...buildSnapshotExecArgs, + ], + currentArgv: [binary, ...runSnapshotExecArgs], + }); + return true; + }, + } + ); + } + + // Create workers and vm contexts after deserialization + { + const snapshotFixture = fixtures.path( + 'snapshot', + 'create-worker-and-vm.js' + ); + const blobPath = tmpdir.resolve('embedder-snapshot.blob'); + const buildSnapshotArgs = [ + `eval(${getReadFileCodeForPath(snapshotFixture)})`, + '--embedder-snapshot-blob', + blobPath, + '--embedder-snapshot-create', + ]; + const runEmbeddedArgs = ['--embedder-snapshot-blob', blobPath]; + + fs.rmSync(blobPath, { force: true }); + + runTest( + `${apiType}: build create-worker-and-vm snapshot`, + spawnSyncAndExitWithoutError, + [apiType, '--', ...buildSnapshotArgs], + { + cwd: tmpdir.path, + } + ); + + runTest( + `${apiType}: run create-worker-and-vm snapshot`, + spawnSyncAndExitWithoutError, + [apiType, '--', ...runEmbeddedArgs], + { + cwd: tmpdir.path, + } + ); + } +} + +runSnapshotTests('cpp-api'); + +// C-API specific tests +function runCApiTests(apiType) { + runTest( + `${apiType}-nodejs-main: run Node.js CLI`, + spawnSyncAndAssert, + [`${apiType}-nodejs-main`, '--eval', 'console.log("Hello World")'], + { + trim: true, + stdout: 'Hello World', + } + ); + + runTest( + `${apiType}: callMe`, + spawnSyncAndAssert, + [apiType, 'function callMe(text) { return text + " you"; }'], + { stdout: 'called you' } + ); + + runTest( + `${apiType}: waitMe`, + spawnSyncAndAssert, + [ + apiType, + 'function waitMe(text, cb) { setTimeout(() => cb(text + " you"), 1); }', + ], + { stdout: 'waited you' } + ); + + runTest( + `${apiType}: waitPromise`, + spawnSyncAndAssert, + [ + apiType, + 'function waitPromise(text) { ' + + 'return new Promise((res) => ' + + ' setTimeout(() => res(text + " with cheese"), 1)); ' + + '}', + ], + { stdout: 'waited with cheese' } + ); + + runTest( + `${apiType}: waitPromise reject`, + spawnSyncAndAssert, + [ + apiType, + 'function waitPromise(text) { ' + + 'return new Promise((res, rej) => ' + + ' setTimeout(() => rej(text + " without cheese"), 1)); ' + + '}', + ], + { stdout: 'waited without cheese' } + ); + + runTest( + `${apiType}-threading-runtime-per-thread: run 12 environments concurrently`, + spawnSyncAndAssert, + [`${apiType}-threading-runtime-per-thread`, 'myCount = 1'], + { + trim: true, + stdout: '12', + } + ); + + runTest( + `${apiType}-threading-several-runtimes-per-thread: run 12 environments in the same thread`, + spawnSyncAndAssert, + [ + `${apiType}-threading-several-runtimes-per-thread`, + 'myCount = 0; ' + + 'function incMyCount() { ' + + ' ++myCount; ' + + ' if (myCount < 5) setTimeout(incMyCount, 1); ' + + '}', + ], + { + trim: true, + stdout: '60', + } + ); + + runTest( + `${apiType}-threading-runtime-in-several-threads: run an environment from multiple threads`, + spawnSyncAndAssert, + [ + `${apiType}-threading-runtime-in-several-threads`, + 'myCount = 0; ' + + 'function incMyCount() { ' + + ' ++myCount; ' + + ' if (myCount < 5) setTimeout(incMyCount, 1); ' + + '}', + ], + { + trim: true, + stdout: '5', + } + ); + + runTest( + `${apiType}-threading-runtime-in-ui-thread: run an environment from UI thread`, + spawnSyncAndAssert, + [ + `${apiType}-threading-runtime-in-ui-thread`, + 'myCount = 0; ' + + 'function incMyCount() { ' + + ' ++myCount; ' + + ' if (myCount < 5) setTimeout(incMyCount, 1); ' + + '}', + ], + { + trim: true, + stdout: '5', + } + ); + + const preloadScriptPath = path.join(__dirname, 'preload-with-worker.js'); + + runTest( + `${apiType}-preload: run preload callback`, + spawnSyncAndAssert, + [`${apiType}-preload`, `eval(${getReadFileCodeForPath(preloadScriptPath)})`], + { + cwd: __dirname, + trim: true, + stdout: `preloadValue=42; worker preloadValue=42`, + } + ); + + const linkedModulesScriptPath = path.join(__dirname, 'use-linked-modules.js'); + + runTest( + `${apiType}-linked-modules: run with two linked modules`, + spawnSyncAndAssert, + [ + `${apiType}-linked-modules`, + `eval(${getReadFileCodeForPath(linkedModulesScriptPath)})`, + 2, // expected number of greeter module calls + 2, // expected number of replicator module calls + ], + { + cwd: __dirname, + trim: true, + stdout: 'main=Hello, World World; worker=Hello, Node Node', + } + ); +} + +runCApiTests('c-api'); +runCApiTests('c-cpp-api'); + +function runEnvTests(apiType) { + runTest( + `${apiType}: Env No Browser Globals`, + spawnSyncAndExitWithoutError, + [`${apiType}-env-no-browser-globals`], + {} + ); + + runTest( + `${apiType}: Env With ESM Loader`, + spawnSyncAndExitWithoutError, + [`${apiType}-env-with-esm-loader`], + {} + ); + + runTest( + `${apiType}: Env With No ESM Loader`, + spawnSyncAndExit, + [`${apiType}-env-with-no-esm-loader`], + { + status: 1, + signal: null, + } + ); +} + +runEnvTests('c-api'); +runEnvTests('c-cpp-api'); diff --git a/test/embedding/use-linked-modules.js b/test/embedding/use-linked-modules.js new file mode 100644 index 00000000000000..0ffc2330bbef15 --- /dev/null +++ b/test/embedding/use-linked-modules.js @@ -0,0 +1,28 @@ +// Check that the linked modules defined in the executable can be used in +// the main thread and worker thread. + +// Use the greeter and replicator modules in the main thread. +const greeterModule = process._linkedBinding('greeter_module'); +const replicatorModule = process._linkedBinding('replicator_module'); + +const names = replicatorModule.replicate('World'); +const greeting = greeterModule.greet(names); + +// Use the greeter and replicator modules in the worker thread. +const { Worker } = require('worker_threads'); +const worker = new Worker( + 'const greeterModule = process._linkedBinding("greeter_module");\n' + + 'const replicatorModule = process._linkedBinding("replicator_module");\n' + + 'const names = replicatorModule.replicate("Node");\n' + + 'const greeting = greeterModule.greet(names);\n' + + 'require("worker_threads").parentPort.postMessage({greeting});', + { eval: true } +); + +const messages = []; +worker.on('message', (message) => messages.push(message)); + +// Print the greetings from the main thread and worker thread. +process.on('beforeExit', () => { + console.log(`main=${greeting}; worker=${messages[0].greeting}`); +});