Skip to content

Commit

Permalink
add run Node.js main and address some TODOs
Browse files Browse the repository at this point in the history
  • Loading branch information
vmoroz committed Sep 11, 2024
1 parent 593c373 commit d0604d8
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 51 deletions.
22 changes: 22 additions & 0 deletions doc/api/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down
91 changes: 48 additions & 43 deletions src/node_embedding_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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(
Expand All @@ -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};
Expand All @@ -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;
Expand All @@ -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.
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -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_ = {};
Expand Down Expand Up @@ -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_);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 25 additions & 8 deletions src/node_embedding_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Up @@ -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[]);
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions test/embedding/embedtest_node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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`,
Expand Down

0 comments on commit d0604d8

Please sign in to comment.