-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend Node-API to libnode
#43542
base: main
Are you sure you want to change the base?
Extend Node-API to libnode
#43542
Conversation
Review requested:
|
doc/api/n-api.md
Outdated
@@ -515,6 +515,8 @@ the currently running Agent which was set by means of a previous call to | |||
`napi_set_instance_data()` will be overwritten. If a `finalize_cb` was provided | |||
by the previous call, it will not be called. | |||
|
|||
Not compatible with `libnode`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would this mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That API call is not to be used when using libnode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you don't need it - in this case it is the C++ software that will create environments, so it doesn't need the ability to carry its own context in this structure - and because the instance_data
pointer is used to carry the libnode
context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but that doesn’t mean that this call is not compatible with libnode or with embedded mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does because, before these new methods, there was no way to use Node-API with libnode at all.
doc/api/n-api.md
Outdated
@@ -6267,6 +6269,95 @@ idempotent. | |||
|
|||
This API may only be called from the main thread. | |||
|
|||
## Using Node.js as a shared library (`libnode`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has nothing to do with shared library mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API allows using libnode
through Node-API and it has no other uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s … wrong? Embedding and usage as a shared library are conceptually related but technically orthogonal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's call it Using an embedded version of Node.js
<!--introduced_in=REPLACEME--> | ||
|
||
As an alternative, an embedded Node.js can also be fully controlled through | ||
Node-API. This API supports both C and C++ through [node-addon-api][]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do this, we should probably start adding converters between some Node-API values and V8/Node.js C++ API values (napi_value
↔ Local<Value>
, napi_env
↔ Environment*
/Isolate*
).
The reason nobody has implemented a Node-API embedder API yet is that as an embedder, you often need fine-grained control over a lot of V8 options, and it’s not very practical to mirror the entire V8/Node.js embedder API to C (and that the Node.js C++ embedder API itself still has some rough edges that should probably be ironed out first).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to use raw V8 primitives then you can't use Node-API - this applies to both Node.js -> C++ (binary node addons) and to C++ -> Node.js (libnode). Still this API covers lots of use cases - mostly C++ software that needs to accept JS plugins. It does not cover Electron which has very particular needs.
Allowing for more fine-grained control over the initialisation is of course something that should be considered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to use raw V8 primitives then you can't use Node-API
Right, that’s why I’m bringing this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should you be able to mix V8 primitives and Node-API is another topic. If you take the ABI compatibility out of Node-API, there remains little use for it. However node-addon-api
is a truly higher level API and it is much more practical to use from C++ then raw V8. Maybe there is a use case for software which has 95% node-addon-api
and 5% custom V8 code. But this is something that goes beyond this PR - and it probably can be used both by embedders and addon authors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe Cloudflare (service) Workers will need converters. Is this a Private API thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would strongly encourage you to go through the current embedder API and understand how embedders use it. I would love to see a C embedding API, but since the goal of Node-API-style APIs is to provide long-term stability, it’s going to need to be an API that is designed to account for multiple usage patterns (e.g.: integration with libuv, usage with a node-addon-api-style C++ wrapper) and probably some kind of API versioning in advance (since Node.js and V8 embedding changes their API definitions much more frequently than JS changes as a language).
src/js_native_api_v8.cc
Outdated
{ | ||
int r = node::SpinEventLoop(node_env->node_env()).FromMaybe(1); | ||
if (exit_code != nullptr) *exit_code = r; | ||
node::Stop(node_env->node_env()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s going to be heavily counterintuitive that this is part of a destruction operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once you have drained the event loop, you have no other course of action then to destroy the environment - this is the reason I did it this way. There is no point in having a separate API call, if you will always have to call both of them one after the other. The real question is can we (and should we) keep an environment when its event loops is empty? This would allow to call async functions in a persistent environment but it will introduce a significant difference between the stand-alone Node.js and a embedded Node.js.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could want to take an action in response to the event loop being drained, though?
The real question is can we (and should we) keep an environment when its event loops is empty?
I think the answer to both questions here is yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but this would require splitting node::SpinEventLoop
into iteration and cleanup and it is something that is not possible with the current embedders' API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… but it is? The docs for SpinEventLoop
say which steps it takes, and all of those are public API. SpinEventLoop()
is a helper, not a building block, and exists only for convenience and stronger API stability for those for whom it is sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I added napi_run_environment
that spins up the event loop without destroying the environment when it is emptied and a unit test with an async callback.
We discussed this PR in the 24 June 2022 Node API meeting. Our main concerns are:
As of now, Node does not provide any APIs that are "Node-engine specific", because our Node-API is meant to be engine-agnostic. The existing Node-specific APIs, eg. |
|
We discussed in the 1 July 2022 Node API meeting, and we did want to bring up again the comment we had before:
There is a logical separation between "module extensions API" and "node hosting API" so it may be beneficial to separate this embedding API into a separate header. Regarding:
Can you clarify this a bit? |
Public API for retrieving the V8 reference from a |
@KevinEady @addaleax @rvagg My goal is to be able to support installation of this ( Basically, I have two choices:
Whatever I do at this point will likely stay. Do you have any opinion? |
@mmomtchev Two things:
|
Ok, this is what I currently have - If/when this PR goes through, |
@addaleax And do you have an idea for an elegant solution for having top-level functions appear in the global object the way they do in the REPL, besides |
@mmomtchev If somebody wants that, they can just do the same thing the REPL does to make that happen, right? |
285a377
to
38b7dc8
Compare
@addaleax I have a problem with restarting the event loop since Line 440 in 3738b57
can be run only once per isolate. If I don't drain the tasks, some pending Promises (the dynamic UPDATE: It is because |
c2329ae
to
db147f3
Compare
I was experimenting with using Node.js to support WASM containers. For that I needed to use libnode with crun. Unfortunately crun is current a C project and integrating the C++ API from libnode was going to be difficult (if crun would even consider moving to C++ versus C). The model is to dlsym the required methods as crun support multiple engines and only wants to load the shared libraries needed. With this PR is was relatively straight forward to get it working by using the new methods to start/get an env and then existing node-api methods to create and start a script to run. At this for the simple The model is to dlsym the required methods as crun support multiple engines and only wants to load the shared libraries needed. At this for the simple You can see what it used here: https://github.com/mhdawson/crun/blob/node-wasm-experiment/src/libcrun/handlers/wasmtime.c |
f0b96b4
to
33b170d
Compare
src/js_native_api.h
Outdated
@@ -102,6 +102,34 @@ node_api_symbol_for(napi_env env, | |||
size_t length, | |||
napi_value* result); | |||
#endif // NAPI_EXPERIMENTAL | |||
|
|||
#ifdef NAPI_EMBEDDING | |||
NAPI_EXTERN napi_status NAPI_CDECL napi_create_platform(int argc, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the Node-API is split up between two files: js_native_api.h
and node_api.h
.
The difference is that the js_native_api.h
contains only cross-engine JS-specific APIs and node_api.h
has Node.JS-specific APIs.
We use js_native_api.h
in different scenarios where we want to interact with different JS engine using ABI-safe Node-API functions. For example, see the Babylon Native, Hermes Node-API or V8-JSI. In all these scenarios we implement js_native_api.h
APIs, and we do not use Node.JS or Node-specific node_api.h
. Even the js_native_api_v8.h
and js_native_api_v8.cc
must be free from any Node.JS specific code. Any interaction with the Node-JS specific code must be done either through the functions declared in the js_native_api_v8_internals.h
or overriding virtual napi_env__
in the derived node_napi_env__
class in the node_api_internals.h
.
The node_api.h
has the Node.js specific functions and mostly contains code concerning Node-JS modules and asynchronous code.
From this perspective, the Node.JS embedding API must not be part of the js_native_api.h
header file. It could be in the node_api.h
, but since this file is to be used by modules, the best option is to put these new APIs in their own node_hosting_api.h
or node_embedding_api.h
file. Other applications, such as Deno, could implement the full support for node_api.h
and js_native_api.h
to reuse the Node-API modules, but they would never implement the same Node.JS embedding APIs.
These embedding APIs are Node.JS specific and cannot be re-used for other engines or applications. E.g. other engines do not create a thread pool on their own, or cannot handle Node.JS CLI arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vmoroz I moved everything to a new header file, however I would just like to point out that if all those other engines do use N-API, then they would eventually support embedding, and this could become an universal API.
Currently options are passed as a C array of strings, which is an universal method - even if the options will probably be different - and the only one which is Node.js-centric is the thread_pool_size which probably should get transformed to something else before the API is set in stone.
May one ask what the holdup is on this PR? Has OP lost interest? I'm part of a project that relies on this API and we're not too keen on using an unofficial fork and having to build Nodejs manually for all the platforms we support. |
@samardzicneo do you have cycles to help get it across the line? I think @vmoroz also showed interest in moving this forward. @mmomtchev are you still going to have cycles or would it be good to have either @samardzicneo, @vmoroz or both try to move it forward? |
Really, I'd love to do so, especially since I need this for a project of mine, but honestly, I don't trust my C/C++ skills enough to be sending PRs to Node 😅 Believe me, you don't want my messy code anywhere near your runtime. |
@mmomtchev , I have moved the embedded code out of js_native_api_v8.cc to the new file node_api_embedding.cc. The main reason is that we support a scenario where the js_native_api_v8.cc can be used outside of Node.js code on its own. |
src/node_api_embedding.cc
Outdated
// and the v8::locker | ||
delete emb_env; | ||
|
||
cppgc::ShutdownProcess(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call to ShutdownProcess()
prevents another environment instance from being created in the same process. Specifically, a second call to napi_create_environment()
crashes the process:
# Check failed: (g_page_allocator) != nullptr.
I found this because the node-api-dotnet project runs multiple embedding test cases in a process, and each test case uses a separate environment instance. With this line removed, the latest PR revision works well.
Was there a reason to add this call to ShutdownProcess()
?
Co-authored-by: Gabriel Schulhof <gabrielschulhof@gmail.com>
@mmomtchev , the code is rebased on the latest main code. |
Any updates? |
I was coming from the doc of microsoft's project node & .net interop, just to check the status of this. |
This PR adds support for instantiation and further interaction with an embedded version of Node.js (libnode) entirely through Node-API.
Refs: #43516