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