Overview
Node.js provides C++ APIs that allow you to execute JavaScript in a Node.js environment from other C++ software. This enables embedding Node.js as a runtime within custom applications.
Embedding APIs may change on each semver-major release without prior warning, as they don’t follow the typical Node.js deprecation policy.
Example Application
The following sections demonstrate how to create an application that performs the equivalent of node -e <code> - executing JavaScript in a Node.js environment.
Setting Up Per-Process State
Node.js requires per-process initialization:
Parse CLI Arguments
Handle Node.js CLI options
Initialize V8 Platform
Create a v8::Platform instance
Initialize Node.js
Set up Node.js process state
#include <node.h>
#include <uv.h>
#include <v8.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
});
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 a v8::Platform instance
// MultiIsolatePlatform enables Worker thread support
std ::unique_ptr < MultiIsolatePlatform > platform =
MultiIsolatePlatform :: Create ( 4 );
V8 :: InitializePlatform ( platform . get ());
V8 :: Initialize ();
// Run the Node.js instance
int ret = RunNodeInstance (
platform . get (), result -> args (), result -> exec_args ());
V8 :: Dispose ();
V8 :: DisposePlatform ();
node :: TearDownOncePerProcess ();
return ret;
}
MultiIsolatePlatform::Create(4) creates a platform with 4 worker threads. Worker threads will be disabled if no MultiIsolatePlatform is provided.
Setting Up Per-Instance State
Each Node.js instance (node::Environment) is associated with:
v8::Isolate One JS engine instance
v8::Context One main context (can have multiple)
node::IsolateData Shared data across environments
Creating the Node.js 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 ()) // There has been a JS exception
return 1 ;
exit_code = node :: SpinEventLoop (env). FromMaybe ( 1 );
// Stop the event loop
node :: Stop (env);
}
return exit_code;
}
Key Concepts
ArrayBuffer Allocator
Provide an allocator for V8 memory management:
// Use the Node.js default allocator for performance
node ::ArrayBufferAllocator * allocator =
node :: ArrayBufferAllocator :: Create ();
Using the Node.js allocator enables minor performance optimizations when addons use the Buffer API and is required for tracking ArrayBuffer memory in process.memoryUsage().
Each v8::Isolate must be registered with the platform:
// Helper function that creates and registers an Isolate
v8 ::Isolate * isolate = node :: NewIsolate (
allocator,
platform . get ()
);
Loading Environment
Two approaches to execute code:
Direct Code Execution
Callback-Based
MaybeLocal < Value > result = node :: LoadEnvironment (
env,
"console.log('Hello from embedded Node.js!');"
);
MaybeLocal < Value > result = node :: LoadEnvironment (
env,
[]( const node :: StartExecutionCallbackInfo & info ) -> MaybeLocal<Value> {
Local < Value > require = info . require ;
Local < Value > process = info . process ;
// Manually compile and run scripts
return Undefined ( info . env -> isolate ());
}
);
Event Loop Management
Spinning the Event Loop
// Run the event loop until completion
exit_code = node :: SpinEventLoop (env). FromMaybe ( 1 );
Stopping Execution
// Can be called from any thread (like worker.terminate())
node :: Stop (env);
node::Stop() acts like worker.terminate() if called from another thread, immediately stopping JavaScript execution.
Complete Example
Here’s a minimal embedding application:
#include <node.h>
#include <uv.h>
using node ::CommonEnvironmentSetup;
using node ::Environment;
using node ::MultiIsolatePlatform;
using v8 ::Context;
using v8 ::HandleScope;
using v8 ::Isolate;
using v8 ::Locker;
using v8 ::MaybeLocal;
using v8 ::Value;
using v8 ::V8;
int RunNodeInstance ( MultiIsolatePlatform * platform ,
const std :: vector < std :: string > & args ,
const std :: vector < std :: string > & exec_args ) {
int exit_code = 0 ;
std ::vector < std ::string > errors;
auto setup = CommonEnvironmentSetup :: Create (
platform, & errors, args, exec_args);
if ( ! setup) 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 ());
MaybeLocal < Value > loadenv_ret = node :: LoadEnvironment (env,
"console.log('Embedded Node.js running!');" );
if ( loadenv_ret . IsEmpty ()) return 1 ;
exit_code = node :: SpinEventLoop (env). FromMaybe ( 1 );
node :: Stop (env);
}
return exit_code;
}
int main ( int argc , char** argv ) {
argv = uv_setup_args (argc, argv);
std ::vector < std ::string > args (argv, argv + argc);
auto result = node :: InitializeOncePerProcess (args);
auto platform = MultiIsolatePlatform :: Create ( 4 );
V8 :: InitializePlatform ( platform . get ());
V8 :: Initialize ();
int ret = RunNodeInstance (
platform . get (), result -> args (), result -> exec_args ());
V8 :: Dispose ();
V8 :: DisposePlatform ();
node :: TearDownOncePerProcess ();
return ret;
}
Build Configuration
{
'targets': [
{
'target_name': 'embedder',
'type': 'executable',
'sources': [ 'embedder.cc' ],
'include_dirs': [
'<!@(node -p "require(\'node\').path.join(process.execPath, \'include\')")'
],
'libraries': [
'<!@(node -p "require(\'node\').libnode_path")'
]
}
]
}
Use Cases
Custom Runtimes Build specialized JavaScript runtimes with custom capabilities
Application Scripting Add JavaScript scripting to C++ applications
Testing Frameworks Create custom testing or automation tools
Desktop Applications Embed Node.js in Electron-like frameworks
Best Practices
Memory Management
Always clean up resources properly with V8::Dispose() and TearDownOncePerProcess()
Error Handling
Check for errors after initialization and environment creation
Thread Safety
Use Locker and proper scopes when accessing V8 from multiple threads
Worker Support
Use MultiIsolatePlatform if you need Worker thread support
C++ Addons Build native addons for Node.js
Node.js Internals Understand Node.js core architecture
V8 Integration Learn about V8 engine integration
Embedder API View src/node.h header file