Skip to content

Commit e2ff2af

Browse files
committed
node-api: minimal C node_embedding_api function
Co-authored-by: vmoroz <vmorozov@microsoft.com>
1 parent 8edeff9 commit e2ff2af

File tree

8 files changed

+339
-12
lines changed

8 files changed

+339
-12
lines changed

doc/api/embedding.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,219 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
172172
[deprecation policy]: deprecations.md
173173
[embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc
174174
[src/node.h]: https://github.com/nodejs/node/blob/HEAD/src/node.h
175+
# C++ embedder API
176+
177+
<!--introduced_in=v12.19.0-->
178+
179+
Node.js provides a number of C++ APIs that can be used to execute JavaScript
180+
in a Node.js environment from other C++ software.
181+
182+
The documentation for these APIs can be found in [src/node.h][] in the Node.js
183+
source tree. In addition to the APIs exposed by Node.js, some required concepts
184+
are provided by the V8 embedder API.
185+
186+
Because using Node.js as an embedded library is different from writing code
187+
that is executed by Node.js, breaking changes do not follow typical Node.js
188+
[deprecation policy][] and may occur on each semver-major release without prior
189+
warning.
190+
191+
## Example embedding application
192+
193+
The following sections will provide an overview over how to use these APIs
194+
to create an application from scratch that will perform the equivalent of
195+
`node -e <code>`, i.e. that will take a piece of JavaScript and run it in
196+
a Node.js-specific environment.
197+
198+
The full code can be found [in the Node.js source tree][embedtest.cc].
199+
200+
### Setting up a per-process state
201+
202+
Node.js requires some per-process state management in order to run:
203+
204+
* Arguments parsing for Node.js [CLI options][],
205+
* V8 per-process requirements, such as a `v8::Platform` instance.
206+
207+
The following example shows how these can be set up. Some class names are from
208+
the `node` and `v8` C++ namespaces, respectively.
209+
210+
```cpp
211+
int main(int argc, char** argv) {
212+
argv = uv_setup_args(argc, argv);
213+
std::vector<std::string> args(argv, argv + argc);
214+
// Parse Node.js CLI options, and print any errors that have occurred while
215+
// trying to parse them.
216+
std::unique_ptr<node::InitializationResult> result =
217+
node::InitializeOncePerProcess(args, {
218+
node::ProcessInitializationFlags::kNoInitializeV8,
219+
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
220+
});
221+
222+
for (const std::string& error : result->errors())
223+
fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
224+
if (result->early_return() != 0) {
225+
return result->exit_code();
226+
}
227+
228+
// Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way
229+
// to create a v8::Platform instance that Node.js can use when creating
230+
// Worker threads. When no `MultiIsolatePlatform` instance is present,
231+
// Worker threads are disabled.
232+
std::unique_ptr<MultiIsolatePlatform> platform =
233+
MultiIsolatePlatform::Create(4);
234+
V8::InitializePlatform(platform.get());
235+
V8::Initialize();
236+
237+
// See below for the contents of this function.
238+
int ret = RunNodeInstance(
239+
platform.get(), result->args(), result->exec_args());
240+
241+
V8::Dispose();
242+
V8::DisposePlatform();
243+
244+
node::TearDownOncePerProcess();
245+
return ret;
246+
}
247+
```
248+
249+
### Setting up a per-instance state
250+
251+
<!-- YAML
252+
changes:
253+
- version: v15.0.0
254+
pr-url: https://github.com/nodejs/node/pull/35597
255+
description:
256+
The `CommonEnvironmentSetup` and `SpinEventLoop` utilities were added.
257+
-->
258+
259+
Node.js has a concept of a “Node.js instance”, that is commonly being referred
260+
to as `node::Environment`. Each `node::Environment` is associated with:
261+
262+
* Exactly one `v8::Isolate`, i.e. one JS Engine instance,
263+
* Exactly one `uv_loop_t`, i.e. one event loop,
264+
* A number of `v8::Context`s, but exactly one main `v8::Context`, and
265+
* One `node::IsolateData` instance that contains information that could be
266+
shared by multiple `node::Environment`s. The embedder should make sure
267+
that `node::IsolateData` is shared only among `node::Environment`s that
268+
use the same `v8::Isolate`, Node.js does not perform this check.
269+
270+
In order to set up a `v8::Isolate`, an `v8::ArrayBuffer::Allocator` needs
271+
to be provided. One possible choice is the default Node.js allocator, which
272+
can be created through `node::ArrayBufferAllocator::Create()`. Using the Node.js
273+
allocator allows minor performance optimizations when addons use the Node.js
274+
C++ `Buffer` API, and is required in order to track `ArrayBuffer` memory in
275+
[`process.memoryUsage()`][].
276+
277+
Additionally, each `v8::Isolate` that is used for a Node.js instance needs to
278+
be registered and unregistered with the `MultiIsolatePlatform` instance, if one
279+
is being used, in order for the platform to know which event loop to use
280+
for tasks scheduled by the `v8::Isolate`.
281+
282+
The `node::NewIsolate()` helper function creates a `v8::Isolate`,
283+
sets it up with some Node.js-specific hooks (e.g. the Node.js error handler),
284+
and registers it with the platform automatically.
285+
286+
```cpp
287+
int RunNodeInstance(MultiIsolatePlatform* platform,
288+
const std::vector<std::string>& args,
289+
const std::vector<std::string>& exec_args) {
290+
int exit_code = 0;
291+
292+
// Setup up a libuv event loop, v8::Isolate, and Node.js Environment.
293+
std::vector<std::string> errors;
294+
std::unique_ptr<CommonEnvironmentSetup> setup =
295+
CommonEnvironmentSetup::Create(platform, &errors, args, exec_args);
296+
if (!setup) {
297+
for (const std::string& err : errors)
298+
fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str());
299+
return 1;
300+
}
301+
302+
Isolate* isolate = setup->isolate();
303+
Environment* env = setup->env();
304+
305+
{
306+
Locker locker(isolate);
307+
Isolate::Scope isolate_scope(isolate);
308+
HandleScope handle_scope(isolate);
309+
// The v8::Context needs to be entered when node::CreateEnvironment() and
310+
// node::LoadEnvironment() are being called.
311+
Context::Scope context_scope(setup->context());
312+
313+
// Set up the Node.js instance for execution, and run code inside of it.
314+
// There is also a variant that takes a callback and provides it with
315+
// the `require` and `process` objects, so that it can manually compile
316+
// and run scripts as needed.
317+
// The `require` function inside this script does *not* access the file
318+
// system, and can only load built-in Node.js modules.
319+
// `module.createRequire()` is being used to create one that is able to
320+
// load files from the disk, and uses the standard CommonJS file loader
321+
// instead of the internal-only `require` function.
322+
MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
323+
env,
324+
"const publicRequire ="
325+
" require('node:module').createRequire(process.cwd() + '/');"
326+
"globalThis.require = publicRequire;"
327+
"require('node:vm').runInThisContext(process.argv[1]);");
328+
329+
if (loadenv_ret.IsEmpty()) // There has been a JS exception.
330+
return 1;
331+
332+
exit_code = node::SpinEventLoop(env).FromMaybe(1);
333+
334+
// node::Stop() can be used to explicitly stop the event loop and keep
335+
// further JavaScript from running. It can be called from any thread,
336+
// and will act like worker.terminate() if called from another thread.
337+
node::Stop(env);
338+
}
339+
340+
return exit_code;
341+
}
342+
```
343+
344+
## C runtime API
345+
346+
<!--introduced_in=REPLACEME-->
347+
348+
While Node.js provides an extensive C++ embedding API that can be used from C++
349+
applications, the C-based API is useful when Node.js is embedded as a shared
350+
libnode library into C++ or non-C++ applications.
351+
352+
### API design overview
353+
354+
One of the goals for the C based runtime API is to be ABI stable. It means that
355+
applications must be able to use newer libnode versions without recompilation.
356+
The following design principles are targeting to achieve that goal.
357+
358+
* Follow the best practices for the [node-api][] design and build on top of
359+
the [node-api][].
360+
361+
### API reference
362+
363+
#### Functions
364+
365+
##### `node_embed_start`
366+
367+
<!-- YAML
368+
added: REPLACEME
369+
-->
370+
371+
> Stability: 1 - Experimental
372+
373+
Runs Node.js runtime instance the same way as the Node.js executable.
374+
375+
```c
376+
int32_t NAPI_CDECL node_embed_start(
377+
int32_t argc,
378+
char* argv[]);
379+
```
380+
381+
* `[in] argc`: Number of items in the `argv` array.
382+
* `[in] argv`: CLI arguments as an array of zero terminated strings.
383+
Returns `int32_t` with runtime instance exit code.
384+
385+
[CLI options]: cli.md
386+
[`process.memoryUsage()`]: process.md#processmemoryusage
387+
[deprecation policy]: deprecations.md
388+
[embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc
389+
[node-api]: n-api.md
390+
[src/node.h]: https://github.com/nodejs/node/blob/HEAD/src/node.h

node.gyp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
'src/node_debug.cc',
122122
'src/node_dir.cc',
123123
'src/node_dotenv.cc',
124+
'src/node_embed_api.cc',
124125
'src/node_env_var.cc',
125126
'src/node_errors.cc',
126127
'src/node_external_reference.cc',
@@ -241,6 +242,7 @@
241242
'src/module_wrap.h',
242243
'src/node.h',
243244
'src/node_api.h',
245+
'src/node_api_internals.h',
244246
'src/node_api_types.h',
245247
'src/node_binding.h',
246248
'src/node_blob.h',
@@ -253,6 +255,7 @@
253255
'src/node_debug.h',
254256
'src/node_dir.h',
255257
'src/node_dotenv.h',
258+
'src/node_embed_api.h',
256259
'src/node_errors.h',
257260
'src/node_exit_code.h',
258261
'src/node_external_reference.h',
@@ -895,6 +898,7 @@
895898
'<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h
896899
],
897900
'dependencies': [
901+
'tools/v8_gypfiles/abseil.gyp:abseil',
898902
'node_js2c#host',
899903
],
900904

@@ -955,9 +959,6 @@
955959
'src/node_snapshot_stub.cc',
956960
]
957961
}],
958-
[ 'node_use_bundled_v8!="false"', {
959-
'dependencies': [ 'tools/v8_gypfiles/abseil.gyp:abseil' ],
960-
}],
961962
[ 'node_shared_gtest=="false"', {
962963
'dependencies': [
963964
'deps/googletest/googletest.gyp:gtest_prod',
@@ -1278,6 +1279,7 @@
12781279

12791280
'dependencies': [
12801281
'<(node_lib_target_name)',
1282+
'tools/v8_gypfiles/abseil.gyp:abseil',
12811283
],
12821284

12831285
'includes': [
@@ -1311,9 +1313,6 @@
13111313
[ 'node_shared_gtest=="true"', {
13121314
'libraries': [ '-lgtest_main' ],
13131315
}],
1314-
[ 'node_use_bundled_v8!="false"', {
1315-
'dependencies': [ 'tools/v8_gypfiles/abseil.gyp:abseil' ],
1316-
}],
13171316
[ 'node_shared_hdr_histogram=="false"', {
13181317
'dependencies': [
13191318
'deps/histogram/histogram.gyp:histogram',
@@ -1410,6 +1409,8 @@
14101409
'sources': [
14111410
'src/node_snapshot_stub.cc',
14121411
'test/embedding/embedtest.cc',
1412+
'test/embedding/embedtest_c_api_main.c',
1413+
'test/embedding/embedtest_main.cc',
14131414
],
14141415

14151416
'conditions': [
@@ -1552,7 +1553,7 @@
15521553
[ 'OS=="mac"', {
15531554
'libraries': [ '-framework CoreFoundation -framework Security' ],
15541555
}],
1555-
[ 'node_shared_simdutf=="false" and node_use_bundled_v8!="false"', {
1556+
[ 'node_shared_simdutf=="false"', {
15561557
'dependencies': [ 'tools/v8_gypfiles/v8.gyp:simdutf#host' ],
15571558
}],
15581559
[ 'node_shared_libuv=="false"', {

src/node_embed_api.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Description: C-based API for embedding Node.js.
3+
//
4+
// !!! WARNING !!! WARNING !!! WARNING !!!
5+
// This is a new API and is subject to change.
6+
// While it is C-based, it is not ABI safe yet.
7+
// Consider all functions and data structures as experimental.
8+
// !!! WARNING !!! WARNING !!! WARNING !!!
9+
//
10+
// This file contains the C-based API for embedding Node.js in a host
11+
// application. The API is designed to be used by applications that want to
12+
// embed Node.js as a shared library (.so or .dll) and can interop with
13+
// C-based API.
14+
//
15+
16+
#include "node_embed_api.h"
17+
#include "node.h"
18+
19+
EXTERN_C_START
20+
21+
int32_t NAPI_CDECL node_embed_start(int32_t argc, char* argv[]) {
22+
return node::Start(argc, argv);
23+
}
24+
25+
EXTERN_C_END

src/node_embed_api.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// Description: C-based API for embedding Node.js.
3+
//
4+
// !!! WARNING !!! WARNING !!! WARNING !!!
5+
// This is a new API and is subject to change.
6+
// While it is C-based, it is not ABI safe yet.
7+
// Consider all functions and data structures as experimental.
8+
// !!! WARNING !!! WARNING !!! WARNING !!!
9+
//
10+
// This file contains the C-based API for embedding Node.js in a host
11+
// application. The API is designed to be used by applications that want to
12+
// embed Node.js as a shared library (.so or .dll) and can interop with
13+
// C-based API.
14+
//
15+
16+
#ifndef SRC_NODE_EMBED_API_H_
17+
#define SRC_NODE_EMBED_API_H_
18+
19+
#include "node_api.h"
20+
21+
EXTERN_C_START
22+
23+
// Runs Node.js start function. It is the same as running Node.js from CLI.
24+
NAPI_EXTERN int32_t NAPI_CDECL node_embed_start(int32_t argc, char* argv[]);
25+
26+
EXTERN_C_END
27+
28+
#endif // SRC_NODE_EMBED_API_H_

test/embedding/embedtest.cc

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
#endif
44
#include <assert.h>
55
#include "cppgc/platform.h"
6-
#include "executable_wrapper.h"
76
#include "node.h"
7+
#include "uv.h"
88

99
#include <algorithm>
1010

@@ -28,10 +28,7 @@ static int RunNodeInstance(MultiIsolatePlatform* platform,
2828
const std::vector<std::string>& args,
2929
const std::vector<std::string>& exec_args);
3030

31-
NODE_MAIN(int argc, node::argv_type raw_argv[]) {
32-
char** argv = nullptr;
33-
node::FixupMain(argc, raw_argv, &argv);
34-
31+
int32_t test_main_cpp_api(int32_t argc, char* argv[]) {
3532
std::vector<std::string> args(argv, argv + argc);
3633
std::shared_ptr<node::InitializationResult> result =
3734
node::InitializeOncePerProcess(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "node_embed_api.h"
2+
3+
// The simplest Node.js embedding scenario where the Node.js start function is
4+
// invoked from the libnode shared library as it would be run from the Node.js
5+
// CLI.
6+
int32_t test_main_c_api_nodejs_main(int32_t argc, char* argv[]) {
7+
return node_embed_start(argc, argv);
8+
}

0 commit comments

Comments
 (0)