Skip to content
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

N-API: An api for embedding Node in applications #23265

Open
1 of 5 tasks
empyrical opened this issue Oct 4, 2018 · 56 comments
Open
1 of 5 tasks

N-API: An api for embedding Node in applications #23265

empyrical opened this issue Oct 4, 2018 · 56 comments
Assignees
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. embedding Issues and PRs related to embedding Node.js in another project. feature request Issues that request new features to be added to Node.js. never-stale Mark issue so that it is never considered stale node-api Issues and PRs related to the Node-API.

Comments

@empyrical
Copy link

empyrical commented Oct 4, 2018

Is your feature request related to a problem? Please describe.
Right now there isn't a documented/stable way to use Node as a shared library inside of an application. Were one to be made using N-API, this would open up using Chakra in addition to V8 in an application.

Describe the solution you'd like
I would like for there to be stable APIs in node_api.h for creating/managing a Node environment.

A function that does this could hypothetically look like:

NAPI_EXTERN napi_status napi_create_env(int* argc, const char** argv, napi_env* env);
// Start the node event loop
NAPI_EXTERN napi_status napi_run_env(napi_env env);
// Cleanup (e.g. FreeIsolateData, FreeEnvironment and whatever else needs to be ran on teardown)
NAPI_EXTERN napi_status napi_free_env(napi_env env);

The embedder could get this environment's libuv loop using napi_get_uv_event_loop. But I would also like to have open the possibility of providing my own libuv loop that I have control over to help integrate with other event loops (e.g. Qt's event loop). This could look like:

NAPI_EXTERN napi_status napi_create_env_from_loop(int* argc, const char** argv,
  napi_env* env, struct uv_loop_s* loop);

Keeping the event loop going (using uv_run on the env's loop) would then be the embedder's responsibility.

Also, right now methods like node::CreateEnvironment seem to always jump into a REPL, unless you provide a string to evaluate or a file to run. Tweaks to help make this nicer to use for embedding will have to be made.

These APIs are just hypothetical, and will probably change when an actual attempt to implement them is made.

I am up to trying to implement this, but I would like to see what kind of discussion happens first and what other ideas people have before I start.

Implementation Progress

  • Create a clean non-NAPI way to use Node embedded
  • Create NAPI functions for creating and managing environments
  • Create a NAPI function for evaluating a string (exists in NAPI v1: napi_run_script)
  • Create a NAPI function for running a script from file
  • Investigate if this can play nicely with worker_threads.

Describe alternatives you've considered
I've tried using the unstable APIs, and they aren't fun to keep up with 😅

For discussions on how the shared library can be distributed, see this issue: #24028

@addaleax addaleax added node-api Issues and PRs related to the Node-API. embedding Issues and PRs related to embedding Node.js in another project. c++ Issues and PRs that require attention from people who are familiar with C++. labels Oct 4, 2018
@addaleax
Copy link
Member

addaleax commented Oct 4, 2018

I've tried using the unstable APIs, and they aren't fun to keep up with 😅

A big part of that is that they haven’t ever been designed as a coherent API (or designed at all, really), and we would likely need to iterate on them a bit more before they are stable – which is probably also the point where we can start to talk about enabling N-API versions of them.

If you want to work on this, good starting points might be #21653 (comment), or splitting CreateEnvironment() into a function that, well creates the Environment, and one that calls Environment::Start() under the hood?

@addaleax addaleax added the feature request Issues that request new features to be added to Node.js. label Oct 4, 2018
@empyrical
Copy link
Author

Thanks for helping point where to start! I also noticed some relevant TODOs in 'node_worker' that would get resolved by more stable apis for this.

@mhdawson
Copy link
Member

mhdawson commented Oct 4, 2018

@rubys another person we should loop into discussions/team about use cases/testing/api for using Node.js as a shared library.

@rubys
Copy link
Member

rubys commented Oct 5, 2018

First observation: we should plan to move to having --shared as the default for both CI and releases. This would make releases include a shared library that could be used by third parties. Unscientific comparison of results on Mac OS/X, the combine executable + dynamic library would be a total of 0.1% bigger than a standalone executable.

Second, I would suggest that one of the goals be to allow electron to be built using exclusively NAPI interfaces. See electron/atom/app/node_main.cc.

This means that in addition to Create and Destroy environments, there would need to be an interface to execute a script in an environment, and to evaluate an expression in that environment.

@empyrical
Copy link
Author

Like - making the node command basically just be node_main.cc that links against libnode? Would be very nice! And would be nice to include CMake, pkgconfig modules for finding libnode that would ship with it while we're at it too.

@rubys
Copy link
Member

rubys commented Oct 5, 2018

@empyrical today if you do the following on Mac or Linux:

./configure --shared
make -j4

You end up with out/Release/node and out/Release/libnode.67.dynlib or out/Release/lib.target/libnode.so.67. Adding additional NAPI apis would be straightforward; I'm merely stating that it should be goal to add enough APIs to make electron's node_main.cc not need to depend on any other APIs.

But again, we would either need to include these libraries in the existing releases or have separate releases.

@empyrical
Copy link
Author

Oh - I misunderstood. I thought you meant only building --shared version of Node, and making the node executable you use from the cli just very small executable that links against libnode

@rubys
Copy link
Member

rubys commented Oct 5, 2018

@empyrical that's actually what --shared does. Here are the sizes of the output files on Mac OS/X:

$ ls -l out/Release/node out/Release/libnode.67.dylib 
-rwxr-xr-x  1 rubys  staff  40410544 Sep 29 16:33 out/Release/libnode.67.dylib
-rwxr-xr-x  1 rubys  staff      9208 Sep 29 16:33 out/Release/node

@addaleax
Copy link
Member

addaleax commented Oct 5, 2018

Just two quick things to note:

  • I don’t know if that’s implied here, but I don’t think we can get away with a default where people have only a libnode + wrapper available as part of the release tarballs
  • Using --shared is definitely something that embedders will tend to do more often than others, but it’s orthogonal to the Embedder API by itself

@empyrical
Copy link
Author

empyrical commented Oct 5, 2018

Curious for some thoughts with regards to worker_threads: If you create multiple envs, should they all be "main threads" with a threadid of 0 and workers for envs would be created with a separate hypothetical API, or should the first one created be the "main thread", and subsequent ones be considered "workers" with incrementing threadids?

And should the "main thread" only be allowed to be made in the process' main thread? JS code that checks worker_threads.isMainThread to see if it's safe to do something, e.g. call functions in a GUI binding (which typically only work in the main thread) may have issues if a "main" js thread isn't truly in the process' main thread.

Maybe there should be a NAPI function for creating a "main" env, and then a different one for subsequent ones?

Basically:

// Any more than one invocation per process would result in an error napi_status
NAPI_EXTERN napi_status napi_create_main_env(int* argc, const char** argv, napi_env* env);

// Parent env should also show up as parentPort on worker_threads
NAPI_EXTERN napi_status napi_create_env(napi_env parent_env, napi_env* env);

@rubys
Copy link
Member

rubys commented Oct 5, 2018

I don’t think we can get away with a default where people have only a libnode + wrapper available as part of the release tarballs

Why not?

@gireeshpunathil
Copy link
Member

Why not?

IMO:

  • node executable probably enjoys the most compact binary for a language runtime of all time - no linkage dependency other than the c|c++(rt)
  • embedding use cases may be too small to warrant a change in the default in favor of those.

@rubys
Copy link
Member

rubys commented Oct 5, 2018

node executable probably enjoys the most compact binary for a language runtime of all time - no linkage dependency other than the c|c++(rt)

I'm clearly not understanding the downside. How is a 4M executable better than a 9k executable plus a 4M libnode?

Alternatives:

  • a 4M binary plus a 4M libnode.
  • Two separate release bundles (and sets of CIs), one with a standalone binary, and one with a libnode.

@gireeshpunathil
Copy link
Member

downsides are mostly on unforeseen consumability issues at the end-user: for example user needing to explicitly set LD_LIBRARY_PATH or LIBPATH or PATH . There could be other platform specific disparity on symbol resolutions (precedence between the launcher and the library) , issues stemming from other node processes sharing the library etc.

@rubys
Copy link
Member

rubys commented Oct 5, 2018

@gireeshpunathil others seem to manage without these problems; but in any case, what alternative would you suggest?

@gireeshpunathil
Copy link
Member

I don't know. In most of my interactions with embedded users in nodejs/help repo, I see they build from source - not because they don't have a libnode, but because each one of them wanted to embed node at different levels of abstractions - 2 node::Init, 3 node::Start, create re-use env, re-enter env , multi-isolate spawning etc. necessitate them to build from source.

Once we have normalized these into one or two or three discrete entry points, we could expose (only) those that leads to improved consumption of libnode; and that should help us take an easy decision. One obvious route is to release regular (exe) and libnode separately, against a specified version.

@addaleax
Copy link
Member

addaleax commented Oct 5, 2018

One obvious route is to release regular (exe) and libnode separately, against a specified version.

I agree, that is probably the best way forward. Dynamic linking can be pretty painful when copying executable files around (which even our own test suite does on a regular basis).

@rubys
Copy link
Member

rubys commented Oct 11, 2018

I've created a demo of how this could work.

@mhdawson
Copy link
Member

This discussion related to nodejs/Release#341 as well. If we had a Development kit and a Deployment kit (or equivalent) then we could add a shared library in addition to the existing exe without concern over the additional size.

@empyrical
Copy link
Author

empyrical commented Nov 1, 2018

I think that the shared library stuff is worth an issue of its own, imo! sadly some questions i had about what the n-api could look like got buried by this talk. (I can edit in a link to the top level issue if one exists)

edit: link to the issue: #24028

@gibfahn
Copy link
Member

gibfahn commented Nov 2, 2018

I'm clearly not understanding the downside. How is a 4M executable better than a 9k executable plus a 4M libnode?

I think people value the "single file" node binary approach. Being able to move the node executable around by itself has benefit (IMO), and for a lot of use-cases disk space is cheap, so that's less of an issue.

Two separate release bundles (and sets of CIs), one with a standalone binary, and one with a libnode.

Sounds like a win-win to me (hopefully not too much extra pain for CI / build).

@empyrical
Copy link
Author

Going to close this for now and remake this issue when I've got time to try and implement the N-API stuff.

I made a new discussion for the shared library stuff here for those interested: #24028

@refack
Copy link
Contributor

refack commented Nov 2, 2018

ping @nodejs/n-api, would you consider adding this to your backlog?

@viferga
Copy link

viferga commented May 29, 2019

@joyeecheung One of the first design decisions of MetaCall was to avoid technical debt as much as possible. So I could figure out how to solve the problem without patching any line of NodeJS code. I tried this in 8.x and 10.x and it works like a charm. I did not tried 12.x (if you did any change it would be interesting to check it out).
I found many limitations when using LoadEnvironment (it was my first approach), it could not provide me access to the current NodeJS instance internals (like the isolate) so it was unusable at the end. Basically, it is impossible to use it to truly embed NodeJS, only if you want to run scripts in a synchronous manner (blocking the main thread, and without having any control over NodeJS), and that is too limited for my purposes.
To have access to NodeJS internals I had to create a mechanism with a trampoline that does the following:

  1. Host application launch a thread with NodeJS Start (https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/source/node_loader_impl.cpp#L1552 and https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/source/node_loader_impl.cpp#L1501) passing by arguments the bootstrap.js file with some other parameters (they will be explained later).

  2. NodeJS starts and loads bootstrap.js (https://github.com/metacall/core/blob/develop/source/loaders/node_loader/bootstrap/lib/bootstrap.js)

  3. Loads an addon called trampoline.node implemented with N-API (https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/bootstrap/lib/bootstrap.js#L219 and https://github.com/metacall/core/blob/develop/source/loaders/node_loader/trampoline/source/trampoline.cc).

  4. At this point I can access to NodeJS internals from C/C++ land (napi_env env: https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/trampoline/source/trampoline.cc#L54).

  5. Trampoline receives from host application the function pointer to the callback that is going to be used to inject the NodeJS internals (https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/trampoline/source/trampoline.cc#L112)

  6. Trampoline calls back to Host application injecting back the internals (https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/trampoline/source/trampoline.cc#L134)

  7. After this the callback gets executed (https://github.com/metacall/core/blob/38abb39eed36a06584305653ff7378ae50b1b3e8/source/loaders/node_loader/source/node_loader_impl.cpp#L1220) and the NodeJS internals are exposed to the host application.

With this methodology it is possible to access the internals that are aren't exposed from embedding API, without touching any single code of NodeJS. This model turns out the N-API from extension into embedding API, inverting the model and allowing to call single functions in asynchronous manner (the event loop is also inverted).

Although it seems hacky, it has been tested on high performance FaaS and it works properly. It may encounter limitations over the fork model but they are also mitigated as I explained before.

@kohillyang
Copy link

Consider the case that the node.js is built as a shared library (using clang-cl or msvc), and use it in msvc/gcc, eg., in node.js + Cef. Because of the ABI incompatibility, one has to use the N-API instead of NAN/v8 to add global variables into the isolate. Since the program is not an extension, but an embedder, the program needs a napi_env to access the environment of the node.js. Apparently, the current N-API has no way to achieve this goal, but we can just add a function named napi_create_env, just as @empyrical said.

napi_status napi_create_env(napi_env* env, const char* module_filename) {
  v8::Isolate* isolate = v8::Isolate::TryGetCurrent();
  if (isolate) {
    v8::Local<v8::Context> ctx = isolate->GetCurrentContext();
      if (!ctx.IsEmpty()) {
        *env = new node_napi_env__(ctx, module_filename);
        return napi_status::napi_ok;
      }
  }
  return napi_status::napi_generic_failure;
}
napi_status napi_free_env(napi_env env) {
  CHECK_ENV(env);
  delete env;
  return napi_status::napi_ok;
}

In this way it is the embedder's responsibility to make sure there exists one isolate in this thread. This is possible in CEF since there exists one callback with signature CefRenderProcessHandler::OnContextCreated().

@mhdawson
Copy link
Member

If those contributing to the discussion wanted to put together:

  1. list of key use cases
  2. minimal set of suggested node-api methods needed for each of those

That might be a good way to move the conversation forward.

@NickCarducci
Copy link

I compel you to fully open source Node.js occur rather when every dependency requires it.

“I have to say that I'm amazed that there is code out there that loads one native addon from another native addon! Is it done by acquiring and then calling an instance of the require() function, or perhaps by using uv_dlopen() directly?”

"...more requests/cases where it[ otherwise] blocks adoption." - I say this exclusive nature of adoption and abstraction (and dependencies) has on the other hand opened node.js "internal" contributors to retribution for certain damages.
https://www.quora.com/unanswered/Will-the-market-crash-if-I-rebuild-Node-js-but-for-the-browser

Much less is the impetus to extend path only if there are alternatives preventing node.js dominance, there are no alternatives for dependency-without-a-global-default-design-of-commonjs users - the use case is quite literally to

  1. use named exports of the modules that industries across- and within-greenfield-verticals, use,
  2. without a server farm.

I wonder if the node.js tool was to create an industry of serverless salaries instead of add utility (1/hour).

@mmomtchev
Copy link
Contributor

I have #43542 which has a completely independent partial implementation of this feature.

One big thing that is missing is the ability to drain the pending async callbacks and then to keep using the created environment - your napi_run_env. This function is somewhat contrary to the current design principle of Node.js where once the event loop is emptied, the process exits. Still, I think that there might be a valid use case - a C++ software loads a JS plugin into a persistent environment, then starts calling async functions now and then.

But I consider it out of scope for the moment.

Also I see an API call for creating an environment out of a libuv event loop? Is it really needed? What is the use case?

@mmomtchev
Copy link
Contributor

Another thing that may be possible without the API is switching the thread that calls V8 - for those using it with fibers/green threads - but this can be added later if it is deemed necessary. What is important at the moment is that nothing is missing from napi_create_environment because this can't be changed later.
Also napi_create_platform can probably be called something else, napi_init_engine for example.

@mmomtchev
Copy link
Contributor

@empyrical @rubys @viferga @darabi @kohillyang
As part of the OSGeo's GSoC 2022 program, I have implemented a fully N-API/node-addon-api APIs for embedding Node.js in C and C++ applications that greatly reduces the boilerplate code, adds support for directly calling require and import from C/C++ and then interacting with JS entirely through the binary stable N-API, and even for await of JS promises from C/C++.
The library is currently available as binaries for Ubuntu 18.04, 20.04 and 22.04 from Ubuntu PPA for the Node.js 16.x and Node.js 18.x branches.
At the moment all other OS require rebuilding.
This is currently to be considered very experimental, especially the Node.js 18 branch.
Can you please take a look and see if these new APIs suit your needs. They are not geared towards Electron which has very specific needs - they are mostly for the developer who needs to quickly embed Node.js in his application to support JS plugins for his existing C/C++ application.
If we can get enough people to use it and ensure that it doesn't break anything, the PR (which is quite sizeable) will surely get merged in Node.js 19 and everyone will benefit from having a common binary stable API for embedding Node.js.

https://github.com/mmomtchev/libnode
https://launchpad.net/~mmomtchev

@CMCDragonkai
Copy link

Wow that's really cool. Would that make it easier to embed nodejs in a larger Android/iOS application?

@mmomtchev
Copy link
Contributor

@CMCDragonkai It should make it easier to use from any C/C++ application - if you are willing to try building it on a mobile device, I will be glad to hear from you - normally nothing of this PR should be platform-dependant, but at the moment only Linux (only Ubuntu to be more precise) has been thoroughly tested and used.

@mhdawson
Copy link
Member

From my experimentation mentioned in #43542 (comment) and the earlier suggestion that we should put together:

  1. list of key use cases
  2. minimal set of suggested node-api methods needed for each of those

The first use cases would be:

  1. run a script without any external dependencies
  2. run a script with npm installed dependencies

@viferga
Copy link

viferga commented Sep 21, 2022

@mmomtchev Wouldn't you be interested in joining forces? I have solved most of the problems you have without having embedding API, MetaCall supports now from NodeJS v10 to v18 (we used to support v8.x too but I have decided to drop the support in favor of safety and losing a bit of performance).

What @mhdawson mentioned is already supported by MetaCall too. And respect to what @CMCDragonkai said, MetaCall has also been tested in iOS and Android but the current build binaries are not published for those platforms yet (although it has been tested there).

Here's an old example (now there's no need for Python2.7 anymore in order to build Node, and Debian is distributing NodeJS with libnode as compiled library, so it does not require building NodeJS as shared library, but the rest should work): https://github.com/metacall/embedding-nodejs-example

Another good thing of MetaCall is that I do not touch a single line of NodeJS code, it should work as it is. It was one of the main design decisions because I do not want to maintain a port of NodeJS for embedding.

We support invoking functions and async functions, and we are implementing support for creating classes and objects from C/C++ side too: metacall/core#343

It also has extra features that improve embedding capabilities which you will hit eventually if you embed NodeJS at some point, related to the threading model etc.

@mmomtchev
Copy link
Contributor

@viferga Your objectives are very different than mine - in fact MetaCall should be a layer above NAPI embedding.

My objectives for NAPI embedding were:

  • Be able to easily call JS code, including npm-installed modules, from C and C++ with a clean interface
  • Do not require any dependencies besides the public header files - all Node.js internals are to be abstracted
  • Thanks to N-API, I also got binary compatibility and C++ runtime independence, which is very nice, but this was not a requirement
  • Ship a ready-to-use binary distribution for Ubuntu, compatible with the NodeSource packages

The problem with the libnode in Debian, on which libnode in Ubuntu is also based, is that you won't get very far without accessing Node.js internals. The package itself is of course very sleek - built by the authors of the distributions - and I did borrow parts of it - but linking npm modules and working with asynchronous code will be a major problem.

@viferga
Copy link

viferga commented Sep 23, 2022

@mmomtchev most of the objectives are the same..., the only difference is that we offer a simpler API and a library on top of it.

We have achieved to properly embed node only with Debian libnode and N-API. It has been very costly but it works, that's what I mean. You can check our implementation if you want to know how we did it. Also it is explained in this post the approach we followed to properly embed it with the current limitations of node.

@github-actions
Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions github-actions bot added the stale label Mar 23, 2023
@mmomtchev
Copy link
Contributor

The issue is not stale and the PR is up-to-date

@github-actions github-actions bot removed the stale label Mar 24, 2023
@mhdawson mhdawson added the never-stale Mark issue so that it is never considered stale label Mar 27, 2023
@vmoroz
Copy link
Member

vmoroz commented Sep 7, 2024

In the last few weeks I have made some progress on addressing this issue.
See the PR #54660 (a temporary spin off from #43542).

There are still a few TODOs listed in the PR's description, but I would like to ask participants of this discussion for the early feedback. While nitpicking is welcome, I am the most interested in your scenarios.
E.g., "I have scenario XYZ, how can it be addressed with the new API?", "Did you consider the scenario Z?", "I use libnode for X and I really wish it can do Y", or any other thoughts or questions about the new API.

So far the design is being actively discussed with @jasongin, one of the original creators of Node-API, and many TODOs are based on his feedback. One of our core scenarios is to use the new API from the node-api-dotnet. It must enable use of libnode in .Net based server or client apps.
@mhdawson has provided the valuable PR feedback for an early iteration, and we had a brief discussion of it in our latest @nodejs/node-api meeting.

The PR has a relatively long description, all new APIs are documented, and there are several unit tests that exercise the APIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. embedding Issues and PRs related to embedding Node.js in another project. feature request Issues that request new features to be added to Node.js. never-stale Mark issue so that it is never considered stale node-api Issues and PRs related to the Node-API.
Projects
Status: Awaiting Triage
Development

No branches or pull requests