Skip to main content

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:
1

Parse CLI Arguments

Handle Node.js CLI options
2

Initialize V8 Platform

Create a v8::Platform instance
3

Initialize Node.js

Set up Node.js process state
main.cpp
#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

uv_loop_t

One event loop

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().

Platform Registration

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:
MaybeLocal<Value> result = node::LoadEnvironment(
    env,
    "console.log('Hello from embedded Node.js!');"
);

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:
embedder.cc
#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

binding.gyp
{
  '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

1

Memory Management

Always clean up resources properly with V8::Dispose() and TearDownOncePerProcess()
2

Error Handling

Check for errors after initialization and environment creation
3

Thread Safety

Use Locker and proper scopes when accessing V8 from multiple threads
4

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