Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node-api: use c-based api for libnode embedding #54660

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add run Node.js main and address some TODOs
  • Loading branch information
vmoroz committed Sep 11, 2024
commit d0604d85605d559f5059de711422795929d49c71
22 changes: 22 additions & 0 deletions doc/api/embedding.md
Original file line number Diff line number Diff line change
@@ -345,6 +345,28 @@ The callback parameters:

#### Functions

##### `node_embedding_run_nodejs_main`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Runs Node.js main function as if it is invoked from Node.js CLI without any
embedder customizations.

```c
int32_t NAPI_CDECL
node_embedding_run_nodejs_main(int32_t argc,
char* argv[]);
```

- `[in] argc`: Number of items in the `argv` array.
- `[in] argv`: CLI arguments as an array of zero terminating strings.

Returns 0 if there were no issues.

##### `node_embedding_on_error`

<!-- YAML
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
@@ -1293,6 +1293,7 @@
'test/embedding/embedtest_modules_node_api.cc',
'test/embedding/embedtest_node_api.cc',
'test/embedding/embedtest_node_api.h',
'test/embedding/embedtest_nodejs_main_node_api.cc',
'test/embedding/embedtest_preload_node_api.cc',
'test/embedding/embedtest_snapshot_node_api.cc',
],
91 changes: 48 additions & 43 deletions src/node_embedding_api.cc
Original file line number Diff line number Diff line change
@@ -80,9 +80,8 @@ namespace {
// 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 <size_t kInplaceBufferSize = 32>
class CStringArray {
static constexpr size_t kInplaceBufferSize = 32;

public:
explicit CStringArray(const std::vector<std::string>& strings) noexcept
: size_(strings.size()) {
@@ -200,14 +199,8 @@ class EmbeddedPlatform {

std::shared_ptr<node::InitializationResult> init_result_;
std::unique_ptr<node::MultiIsolatePlatform> v8_platform_;

static node_embedding_error_handler custom_error_handler_;
static void* custom_error_handler_data_;
};

node_embedding_error_handler EmbeddedPlatform::custom_error_handler_{};
void* EmbeddedPlatform::custom_error_handler_data_{};

struct IsolateLocker {
IsolateLocker(node::CommonEnvironmentSetup* env_setup)
: v8_locker_(env_setup->isolate()),
@@ -284,10 +277,9 @@ class EmbeddedRuntime {

bool IsScopeOpened() const;

static node_napi_env GetOrCreateNodeApiEnv(
node::Environment* node_env,
const std::string& module_filename = "<worker_thread>",
int32_t node_api_version = 0);
static napi_env GetOrCreateNodeApiEnv(node::Environment* node_env,
const std::string& module_filename,
int32_t node_api_version);

private:
static node::EnvironmentFlags::Flags GetEnvironmentFlags(
@@ -308,6 +300,16 @@ class EmbeddedRuntime {
int32_t module_node_api_version;
};

struct SharedData {
std::mutex mutex;
std::unordered_map<node::Environment*, napi_env> node_env_to_node_api_env;

static SharedData& Get() {
static SharedData shared_data;
return shared_data;
}
};

private:
EmbeddedPlatform* platform_;
bool is_initialized_{false};
@@ -319,7 +321,7 @@ class EmbeddedRuntime {
std::function<void(const node::EmbedderSnapshotData*)> create_snapshot_;
node::SnapshotConfig snapshot_config_{};
int32_t node_api_version_{0};
node_napi_env node_api_env_{};
napi_env node_api_env_{};

struct {
bool flags : 1;
@@ -331,17 +333,8 @@ class EmbeddedRuntime {

std::unique_ptr<node::CommonEnvironmentSetup> env_setup_;
std::optional<IsolateLocker> isolate_locker_;

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

// TODO: (vmoroz) remove from the static initialization on module load.
std::mutex EmbeddedRuntime::shared_mutex_{};
std::unordered_map<node::Environment*, node_napi_env>
EmbeddedRuntime::node_env_to_node_api_env_{};

//-----------------------------------------------------------------------------
// EmbeddedErrorHandling implementation.
//-----------------------------------------------------------------------------
@@ -642,17 +635,19 @@ napi_status EmbeddedRuntime::SetPreloadCallback(
node_embedding_preload_callback preload_cb, void* preload_cb_data) {
ASSERT(!is_initialized_);

// TODO: (vmoroz) use CallIntoModule to handle errors.
if (preload_cb != nullptr) {
preload_cb_ = node::EmbedderPreloadCallback(
[preload_cb, preload_cb_data](node::Environment* node_env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require) {
// TODO: (vmoroz) propagate node_api_version from the parent env.
node_napi_env env = GetOrCreateNodeApiEnv(node_env);
napi_value process_value = v8impl::JsValueFromV8LocalValue(process);
napi_value require_value = v8impl::JsValueFromV8LocalValue(require);
preload_cb(preload_cb_data, env, process_value, require_value);
[preload_cb, preload_cb_data, node_api_version = node_api_version_](
node::Environment* node_env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require) {
napi_env env = GetOrCreateNodeApiEnv(
node_env, "<worker thread>", node_api_version);
env->CallIntoModule([&](napi_env env) {
napi_value process_value = v8impl::JsValueFromV8LocalValue(process);
napi_value require_value = v8impl::JsValueFromV8LocalValue(require);
preload_cb(preload_cb_data, env, process_value, require_value);
});
});
} else {
preload_cb_ = {};
@@ -720,7 +715,6 @@ napi_status EmbeddedRuntime::AddModule(
return napi_ok;
}

// TODO: (vmoroz) avoid passing the main_script this way.
napi_status EmbeddedRuntime::Initialize(const char* main_script) {
ASSERT(!is_initialized_);

@@ -783,7 +777,6 @@ napi_status EmbeddedRuntime::RunEventLoop() {
return napi_ok;
}

// TODO: (vmoroz) add support node_embedding_event_loop_run_once.
napi_status EmbeddedRuntime::RunEventLoopWhile(
node_embedding_event_loop_predicate predicate,
void* predicate_data,
@@ -892,19 +885,27 @@ bool EmbeddedRuntime::IsScopeOpened() const {
return isolate_locker_.has_value();
}

node_napi_env EmbeddedRuntime::GetOrCreateNodeApiEnv(
napi_env EmbeddedRuntime::GetOrCreateNodeApiEnv(
node::Environment* node_env,
const std::string& module_filename,
int32_t node_api_version) {
std::scoped_lock<std::mutex> lock(shared_mutex_);
auto it = node_env_to_node_api_env_.find(node_env);
if (it != node_env_to_node_api_env_.end()) return it->second;
node_napi_env env = new node_napi_env__(
node_env->context(), module_filename, node_api_version);
node_env->AddCleanupHook(
[](void* arg) { static_cast<node_napi_env>(arg)->Unref(); }, env);
node_env_to_node_api_env_.try_emplace(node_env, env);
return env;
SharedData& shared_data = SharedData::Get();

{
std::scoped_lock<std::mutex> lock(shared_data.mutex);
auto it = shared_data.node_env_to_node_api_env.find(node_env);
if (it != shared_data.node_env_to_node_api_env.end()) return it->second;
}

// Avoid creating the environment under the lock.
napi_env env = NewEnv(node_env->context(), module_filename, node_api_version);

std::scoped_lock<std::mutex> lock(shared_data.mutex);
auto insert_result =
shared_data.node_env_to_node_api_env.try_emplace(node_env, env);

// Return either the inserted or the existing environment.
return insert_result.first->second;
}

node::EnvironmentFlags::Flags EmbeddedRuntime::GetEnvironmentFlags(
@@ -998,6 +999,10 @@ void EmbeddedRuntime::RegisterModules() {
} // end of anonymous namespace
} // end of namespace v8impl

int32_t NAPI_CDECL node_embedding_run_nodejs_main(int32_t argc, char* argv[]) {
return node::Start(argc, argv);
}

napi_status NAPI_CDECL node_embedding_on_error(
node_embedding_error_handler error_handler, void* error_handler_data) {
return v8impl::EmbeddedErrorHandling::SetErrorHandler(error_handler,
33 changes: 25 additions & 8 deletions src/node_embedding_api.h
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@
//
// 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 library and can interop with C-based API.
// embed Node.js as a shared library (.so or .dll) and can interop with
// C-based API.
//

#ifndef SRC_NODE_EMBEDDING_API_H_
@@ -164,7 +165,16 @@ typedef bool(NAPI_CDECL* node_embedding_event_loop_predicate)(
//==============================================================================

//------------------------------------------------------------------------------
// Error handling functions
// Node.js main function.
//------------------------------------------------------------------------------

// Runs Node.js main function as if it is invoked from Node.js CLI without any
// embedder customizations.
NAPI_EXTERN int32_t NAPI_CDECL node_embedding_run_nodejs_main(int32_t argc,
char* argv[]);

//------------------------------------------------------------------------------
// Error handling functions.
//------------------------------------------------------------------------------

// Sets the global error handing for the Node.js embedding API.
@@ -348,18 +358,25 @@ inline constexpr node_embedding_snapshot_flags operator|(

#endif // SRC_NODE_EMBEDDING_API_H_

// TODO: (vmoroz) Remove the main_script parameter.
// TODO: (vmoroz) Add exit code enum. Replace napi_status with the exit code.
// TODO: (vmoroz) Remove the main_script parameter from the initialize function.
// TODO: (vmoroz) Add startup callback with process and require parameters.
// TODO: (vmoroz) Add ABI-safe way to access internal module functionality.
// TODO: (vmoroz) Allow setting the global inspector for a specific environment.
// TODO: (vmoroz) Generate the main script based on the runtime settings.
// TODO: (vmoroz) Set the global inspector for a specific environment.
// TODO: (vmoroz) Start workers from C++.
// TODO: (vmoroz) Worker to inherit parent inspector.
// TODO: (vmoroz) Cancel pending tasks on delete env.
// TODO: (vmoroz) Can we init plat again if it returns early?
// TODO: (vmoroz) The runtime delete must avoid pumping tasks.
// TODO: (vmoroz) Can we initialize platform again if it returns early?
// TODO: (vmoroz) Add simpler threading model - without open/close scope.
// TODO: (vmoroz) Simplify API use for simple default cases.
// TODO: (vmoroz) Check how to pass the V8 thread pool size.

// TODO: (vmoroz) Test passing the V8 thread pool size.
// TODO: (vmoroz) Make the args story simpler or clear named.
// TODO: (vmoroz) Consider to have one function to retrieve the both arg types.
// TODO: (vmoroz) Consider to have one function to set the both arg types.
// TODO: (vmoroz) Single runtime by default vs multiple runtimes on demand.
// TODO: (vmoroz) Add a way to terminate the runtime.
// TODO: (vmoroz) Allow to provide custom thread pool from the app.
// TODO: (vmoroz) Follow the UV example that integrates UV loop with QT loop.
// TODO: (vmoroz) Consider adding a v-table for the API functions to simplify
// binding with other languages.
3 changes: 3 additions & 0 deletions test/embedding/embedtest_main.cc
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

extern "C" int32_t test_main_cpp_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_nodejs_main_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_modules_node_api(int32_t argc, char* argv[]);
extern "C" int32_t test_main_linked_modules_node_api(int32_t argc,
char* argv[]);
@@ -33,6 +34,8 @@ NODE_MAIN(int32_t argc, node::argv_type raw_argv[]) {
return CallWithoutArg1(test_main_cpp_api, argc, argv);
} else if (strcmp(arg1, "node-api") == 0) {
return CallWithoutArg1(test_main_node_api, argc, argv);
} else if (strcmp(arg1, "nodejs-main-node-api") == 0) {
return CallWithoutArg1(test_main_nodejs_main_node_api, argc, argv);
} else if (strcmp(arg1, "modules-node-api") == 0) {
return CallWithoutArg1(test_main_modules_node_api, argc, argv);
} else if (strcmp(arg1, "linked-modules-node-api") == 0) {
5 changes: 5 additions & 0 deletions test/embedding/embedtest_node_api.h
Original file line number Diff line number Diff line change
@@ -72,3 +72,8 @@ napi_status AddUtf8String(std::string& str, napi_env env, napi_value value);
} while (0)

#endif // TEST_EMBEDDING_EMBEDTEST_NODE_API_H_

// TODO: (vmoroz) Enable the test_main_modules_node_api test.
// TODO: (vmoroz) Test failure in Preload callback.
// TODO: (vmoroz) Test failure in linked modules.
// TODO: (vmoroz) Make sure that delete call matches the create call.
8 changes: 8 additions & 0 deletions test/embedding/embedtest_nodejs_main_node_api.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <node_embedding_api.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.
extern "C" int32_t test_main_nodejs_main_node_api(int32_t argc, char* argv[]) {
return node_embedding_run_nodejs_main(argc, argv);
}
14 changes: 14 additions & 0 deletions test/embedding/test-embedding.js
Original file line number Diff line number Diff line change
@@ -31,6 +31,20 @@ function runTest(testName, spawn, ...args) {
console.log('ok');
}

runTest(
'nodejs-main-node-api: run Node.js CLI',
spawnSyncAndAssert,
[
'nodejs-main-node-api', // This parameter is removed before invoking the API
'--eval',
'console.log("Hello World")',
],
{
trim: true,
stdout: 'Hello World',
}
);

function runCommonApiTests(apiType) {
runTest(
`${apiType}: console.log`,