• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# C++ embedder API
2
3<!--introduced_in=v14.0.0-->
4
5Node.js provides a number of C++ APIs that can be used to execute JavaScript
6in a Node.js environment from other C++ software.
7
8The documentation for these APIs can be found in [src/node.h][] in the Node.js
9source tree. In addition to the APIs exposed by Node.js, some required concepts
10are provided by the V8 embedder API.
11
12Because using Node.js as an embedded library is different from writing code
13that is executed by Node.js, breaking changes do not follow typical Node.js
14[deprecation policy][] and may occur on each semver-major release without prior
15warning.
16
17## Example embedding application
18
19The following sections will provide an overview over how to use these APIs
20to create an application from scratch that will perform the equivalent of
21`node -e <code>`, i.e. that will take a piece of JavaScript and run it in
22a Node.js-specific environment.
23
24The full code can be found [in the Node.js source tree][embedtest.cc].
25
26### Setting up per-process state
27
28Node.js requires some per-process state management in order to run:
29
30* Arguments parsing for Node.js [CLI options][],
31* V8 per-process requirements, such as a `v8::Platform` instance.
32
33The following example shows how these can be set up. Some class names are from
34the `node` and `v8` C++ namespaces, respectively.
35
36```cpp
37int main(int argc, char** argv) {
38  argv = uv_setup_args(argc, argv);
39  std::vector<std::string> args(argv, argv + argc);
40  std::vector<std::string> exec_args;
41  std::vector<std::string> errors;
42  // Parse Node.js CLI options, and print any errors that have occurred while
43  // trying to parse them.
44  int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors);
45  for (const std::string& error : errors)
46    fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
47  if (exit_code != 0) {
48    return exit_code;
49  }
50
51  // Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way
52  // to create a v8::Platform instance that Node.js can use when creating
53  // Worker threads. When no `MultiIsolatePlatform` instance is present,
54  // Worker threads are disabled.
55  std::unique_ptr<MultiIsolatePlatform> platform =
56      MultiIsolatePlatform::Create(4);
57  V8::InitializePlatform(platform.get());
58  V8::Initialize();
59
60  // See below for the contents of this function.
61  int ret = RunNodeInstance(platform.get(), args, exec_args);
62
63  V8::Dispose();
64  V8::ShutdownPlatform();
65  return ret;
66}
67```
68
69### Per-instance state
70
71Node.js has a concept of a “Node.js instance”, that is commonly being referred
72to as `node::Environment`. Each `node::Environment` is associated with:
73
74* Exactly one `v8::Isolate`, i.e. one JS Engine instance,
75* Exactly one `uv_loop_t`, i.e. one event loop, and
76* A number of `v8::Context`s, but exactly one main `v8::Context`.
77* One `node::IsolateData` instance that contains information that could be
78  shared by multiple `node::Environment`s that use the same `v8::Isolate`.
79  Currently, no testing if performed for this scenario.
80
81In order to set up a `v8::Isolate`, an `v8::ArrayBuffer::Allocator` needs
82to be provided. One possible choice is the default Node.js allocator, which
83can be created through `node::ArrayBufferAllocator::Create()`. Using the Node.js
84allocator allows minor performance optimizations when addons use the Node.js
85C++ `Buffer` API, and is required in order to track `ArrayBuffer` memory in
86[`process.memoryUsage()`][].
87
88Additionally, each `v8::Isolate` that is used for a Node.js instance needs to
89be registered and unregistered with the `MultiIsolatePlatform` instance, if one
90is being used, in order for the platform to know which event loop to use
91for tasks scheduled by the `v8::Isolate`.
92
93The `node::NewIsolate()` helper function creates a `v8::Isolate`,
94sets it up with some Node.js-specific hooks (e.g. the Node.js error handler),
95and registers it with the platform automatically.
96
97```cpp
98int RunNodeInstance(MultiIsolatePlatform* platform,
99                    const std::vector<std::string>& args,
100                    const std::vector<std::string>& exec_args) {
101  int exit_code = 0;
102  // Set up a libuv event loop.
103  uv_loop_t loop;
104  int ret = uv_loop_init(&loop);
105  if (ret != 0) {
106    fprintf(stderr, "%s: Failed to initialize loop: %s\n",
107            args[0].c_str(),
108            uv_err_name(ret));
109    return 1;
110  }
111
112  std::shared_ptr<ArrayBufferAllocator> allocator =
113      ArrayBufferAllocator::Create();
114
115  Isolate* isolate = NewIsolate(allocator, &loop, platform);
116  if (isolate == nullptr) {
117    fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str());
118    return 1;
119  }
120
121  {
122    Locker locker(isolate);
123    Isolate::Scope isolate_scope(isolate);
124
125    // Create a node::IsolateData instance that will later be released using
126    // node::FreeIsolateData().
127    std::unique_ptr<IsolateData, decltype(&node::FreeIsolateData)> isolate_data(
128        node::CreateIsolateData(isolate, &loop, platform, allocator.get()),
129        node::FreeIsolateData);
130
131    // Set up a new v8::Context.
132    HandleScope handle_scope(isolate);
133    Local<Context> context = node::NewContext(isolate);
134    if (context.IsEmpty()) {
135      fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str());
136      return 1;
137    }
138
139    // The v8::Context needs to be entered when node::CreateEnvironment() and
140    // node::LoadEnvironment() are being called.
141    Context::Scope context_scope(context);
142
143    // Create a node::Environment instance that will later be released using
144    // node::FreeEnvironment().
145    std::unique_ptr<Environment, decltype(&node::FreeEnvironment)> env(
146        node::CreateEnvironment(isolate_data.get(), context, args, exec_args),
147        node::FreeEnvironment);
148
149    // Set up the Node.js instance for execution, and run code inside of it.
150    // There is also a variant that takes a callback and provides it with
151    // the `require` and `process` objects, so that it can manually compile
152    // and run scripts as needed.
153    // The `require` function inside this script does *not* access the file
154    // system, and can only load built-in Node.js modules.
155    // `module.createRequire()` is being used to create one that is able to
156    // load files from the disk, and uses the standard CommonJS file loader
157    // instead of the internal-only `require` function.
158    MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
159        env.get(),
160        "const publicRequire ="
161        "  require('module').createRequire(process.cwd() + '/');"
162        "globalThis.require = publicRequire;"
163        "require('vm').runInThisContext(process.argv[1]);");
164
165    if (loadenv_ret.IsEmpty())  // There has been a JS exception.
166      return 1;
167
168    {
169      // SealHandleScope protects against handle leaks from callbacks.
170      SealHandleScope seal(isolate);
171      bool more;
172      do {
173        uv_run(&loop, UV_RUN_DEFAULT);
174
175        // V8 tasks on background threads may end up scheduling new tasks in the
176        // foreground, which in turn can keep the event loop going. For example,
177        // WebAssembly.compile() may do so.
178        platform->DrainTasks(isolate);
179
180        // If there are new tasks, continue.
181        more = uv_loop_alive(&loop);
182        if (more) continue;
183
184        // node::EmitProcessBeforeExit() is used to emit the 'beforeExit' event
185        // on the `process` object.
186        if (node::EmitProcessBeforeExit(env.get()).IsNothing())
187          break;
188
189        // 'beforeExit' can also schedule new work that keeps the event loop
190        // running.
191        more = uv_loop_alive(&loop);
192      } while (more == true);
193    }
194
195    // node::EmitProcessExit() returns the current exit code.
196    exit_code = node::EmitProcessExit(env.get()).FromMaybe(1);
197
198    // node::Stop() can be used to explicitly stop the event loop and keep
199    // further JavaScript from running. It can be called from any thread,
200    // and will act like worker.terminate() if called from another thread.
201    node::Stop(env.get());
202  }
203
204  // Unregister the Isolate with the platform and add a listener that is called
205  // when the Platform is done cleaning up any state it had associated with
206  // the Isolate.
207  bool platform_finished = false;
208  platform->AddIsolateFinishedCallback(isolate, [](void* data) {
209    *static_cast<bool*>(data) = true;
210  }, &platform_finished);
211  platform->UnregisterIsolate(isolate);
212  isolate->Dispose();
213
214  // Wait until the platform has cleaned up all relevant resources.
215  while (!platform_finished)
216    uv_run(&loop, UV_RUN_ONCE);
217  int err = uv_loop_close(&loop);
218  assert(err == 0);
219
220  return exit_code;
221}
222```
223
224[CLI options]: cli.md
225[`process.memoryUsage()`]: process.md#process_process_memoryusage
226[deprecation policy]: deprecations.md
227[embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc
228[src/node.h]: https://github.com/nodejs/node/blob/HEAD/src/node.h
229