Node.js provides C++ APIs for executing JavaScript in a Node.js environment from other C++ software.
Using Node.js as an embedded library is different from writing code executed by Node.js. Breaking changes do not follow typical Node.js deprecation policy and may occur on each semver-major release without prior warning.
Overview
The embedder API allows you to:
- Create and manage Node.js instances programmatically
- Execute JavaScript code from C++
- Create multiple isolated Node.js environments
- Integrate Node.js into existing C++ applications
Documentation
The complete API documentation is available in:
src/node.h in the Node.js source tree
- V8 embedder API documentation
Example: Running JavaScript Code
The following example shows how to create an application equivalent to node -e <code> that runs JavaScript in a Node.js environment.
Setting Up Per-Process State
Node.js requires per-process state management:
#include <node.h>
int main(int argc, char** argv) {
argv = uv_setup_args(argc, argv);
std::vector<std::string> args(argv, argv + argc);
// Parse Node.js CLI options
std::unique_ptr<node::InitializationResult> result =
node::InitializeOncePerProcess(args, {
node::ProcessInitializationFlags::kNoInitializeV8,
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
});
// Check for errors
for (const std::string& error : result->errors())
fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
if (result->early_return() != 0) {
return result->exit_code();
}
// Create V8 platform
std::unique_ptr<MultiIsolatePlatform> platform =
MultiIsolatePlatform::Create(4);
V8::InitializePlatform(platform.get());
V8::Initialize();
// Run Node.js instance
int ret = RunNodeInstance(
platform.get(), result->args(), result->exec_args());
// Cleanup
V8::Dispose();
V8::DisposePlatform();
node::TearDownOncePerProcess();
return ret;
}
Setting Up Per-Instance State
Each Node.js instance requires:
- One
v8::Isolate (JS engine instance)
- One
uv_loop_t (event loop)
- One or more
v8::Contexts
- One
node::IsolateData instance
int RunNodeInstance(MultiIsolatePlatform* platform,
const std::vector<std::string>& args,
const std::vector<std::string>& exec_args) {
int exit_code = 0;
// Setup libuv event loop, v8::Isolate, and Node.js Environment
std::vector<std::string> errors;
std::unique_ptr<CommonEnvironmentSetup> setup =
CommonEnvironmentSetup::Create(platform, &errors, args, exec_args);
if (!setup) {
for (const std::string& err : errors)
fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str());
return 1;
}
Isolate* isolate = setup->isolate();
Environment* env = setup->env();
{
Locker locker(isolate);
Isolate::Scope isolate_scope(isolate);
HandleScope handle_scope(isolate);
Context::Scope context_scope(setup->context());
// Load and execute JavaScript
MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
env,
"const publicRequire ="
" require('node:module').createRequire(process.cwd() + '/');"
"globalThis.require = publicRequire;"
"require('node:vm').runInThisContext(process.argv[1]);");
if (loadenv_ret.IsEmpty()) // JS exception occurred
return 1;
// Run event loop
exit_code = node::SpinEventLoop(env).FromMaybe(1);
// Stop the environment
node::Stop(env);
}
return exit_code;
}
Key Concepts
Node.js Instance
A Node.js instance (commonly referred to as node::Environment) represents an isolated JavaScript execution environment.
Each Environment has:
- Exactly one
v8::Isolate
- Exactly one
uv_loop_t event loop
- One main
v8::Context (and potentially additional contexts)
- One
node::IsolateData shared across environments using the same isolate
V8 Isolate
An isolate is an independent instance of the V8 JavaScript engine.
Requirements:
- Needs a
v8::ArrayBuffer::Allocator
- Use
node::ArrayBufferAllocator::Create() for the default Node.js allocator
- Must be registered with
MultiIsolatePlatform when using workers
Helper function:
node::NewIsolate() // Creates and sets up a v8::Isolate with Node.js hooks
Environment Lifecycle
Node.js environments can be:
- Created at startup in the main process
- Created on separate threads as Worker threads
- Created and destroyed multiple times during application lifecycle
Important: Native addons may be called:
- Multiple times from multiple contexts
- Concurrently from multiple threads
- Need to manage per-environment state appropriately
Common Use Cases
Embedding in Application
Integrate Node.js into a C++ application:
// Initialize once per process
node::InitializeOncePerProcess(args, flags);
// Create platform
auto platform = MultiIsolatePlatform::Create(4);
V8::InitializePlatform(platform.get());
V8::Initialize();
// Create and run instances as needed
RunNodeInstance(platform.get(), args, exec_args);
// Cleanup
V8::Dispose();
V8::DisposePlatform();
node::TearDownOncePerProcess();
Multiple Environments
Run multiple Node.js environments:
// Create multiple environments sharing one isolate
for (int i = 0; i < num_environments; i++) {
auto setup = CommonEnvironmentSetup::Create(
platform, &errors, args, exec_args);
// Use each environment...
}
Worker Threads
Support for Worker threads requires:
- Creating a
MultiIsolatePlatform instance
- Registering isolates with the platform
- Proper cleanup with environment hooks
// Platform supports workers
auto platform = MultiIsolatePlatform::Create(4);
// Isolates registered automatically with node::NewIsolate()
Isolate* isolate = node::NewIsolate(
allocator.get(),
&loop,
platform.get());
Memory Management
ArrayBuffer Allocator
Provide an allocator for V8:
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator =
node::ArrayBufferAllocator::Create();
Benefits of Node.js allocator:
- Performance optimizations for Node.js Buffer API
- Required for tracking ArrayBuffer memory in
process.memoryUsage()
Cleanup
Proper cleanup is essential:
// Per-instance cleanup
node::Stop(env); // Stop event loop
// Per-process cleanup
V8::Dispose();
V8::DisposePlatform();
node::TearDownOncePerProcess();
Advanced Features
Custom Module Loading
Control how modules are loaded:
MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
env,
"const publicRequire ="
" require('node:module').createRequire(process.cwd() + '/');"
"globalThis.require = publicRequire;");
Event Loop Control
Manage the event loop:
// Run until no more work
exit_code = node::SpinEventLoop(env).FromMaybe(1);
// Stop from any thread
node::Stop(env);
Process Initialization Flags
Control initialization behavior:
node::ProcessInitializationFlags::kNoInitializeV8
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
node::ProcessInitializationFlags::kNoDefaultSignalHandling
Error Handling
Handle errors during initialization:
std::vector<std::string> errors;
auto setup = CommonEnvironmentSetup::Create(
platform, &errors, args, exec_args);
if (!setup) {
for (const std::string& err : errors)
fprintf(stderr, "Error: %s\n", err.c_str());
return 1;
}
Check for JavaScript exceptions:
MaybeLocal<Value> result = node::LoadEnvironment(env, code);
if (result.IsEmpty()) {
// JavaScript exception occurred
return 1;
}
Best Practices
- Initialize Once: Call
InitializeOncePerProcess() only once per process
- Platform Management: Create platform before V8 initialization
- Proper Cleanup: Always cleanup in reverse order of initialization
- Thread Safety: Use proper locking when accessing isolates from multiple threads
- Error Checking: Check all initialization results and handle errors
- Event Loop: Always run the event loop to completion with
SpinEventLoop()
Complete Example
Full example available in the Node.js source tree: