Skip to content

Conversation

@vmoroz
Copy link
Member

@vmoroz vmoroz commented Dec 1, 2025

Disclaimer

Please consider this PR as a proof of concept and a discussion starter. We discussed this design a bit in the latest Node-API meeting with @legendecas and @KevinEady, and this PR provides the specific implementation details to visualize the new ideas.

The issue

The recent investigation of the issue nodejs/abi-stable-node#471 had shown that the current way how Node-API modules depend on API exposed by Node.js has a number of limitations and issues that are not simple to overcome especially if the modules are distributed as pre-built libraries.
To be specific we have the following picture:

  • On Windows the .node modules are compiled with the delay loading dependency on the node.exe process. Each modules is compiled with the win_delay_load_hook.cc source file where we define a global __pfnDliNotifyHook2 that allows to bind to Node-API functions from current process if its name is different from node.exe, or from the libnode.dll. If a module was compiled where such hook is not defined or cannot be defined, then such module cannot bind to Node-API functions for non-node.exe runtimes.
  • On Linux & MacOS the .node uses weak function binding that allows to bind to any function with the same name present in the process. This does not work on Android where the modules are required to have a strong binding.
  • The current system does not allow to use in the same process multiple JS runtimes that may load Node-API modules. For example, if Word.exe uses libnode.dll and React Native for Windows with Hermes JS VM, then it is not possible to specify the target runtime for a module.
  • When we implement Node-API bindings in other languages such as C# or Java, it is quite expensive to resolve all the ~150 Node-API functions by name.

The proposed solution

The proposal is to reverse the dependency. Instead of .node to depend on Node-API, we should make it to be the responsibility of the runtime to load the .node module and to inject its API into the module. This way it does not matter what is the name of the process or the embedded runtime DLL name. The same pre-built module can be loaded from different JS runtimes such as hermes.dll or react-native.dll. (Though, the module can be used only by one runtime at this point.)

This PR shows how such injection can be implemented:

  • js_native_api_types.h and node_api_types.h define the node_api_js_native_vtable and node_api_module_vtable structs with entries for all Node-API functions. They are sorted so that all new function pointers are added in the end of the struct. The main requirement is that we must never change the position of the entries except for the experimental functions.
  • We have to have two different v-tables because the js_native_api.h and node_api.h are two sets of functions that can be used in different scearios independently.
  • js_native_api_v8.cc and node_api.cc initialize the struct instances with all Node-API functions pointers.
  • The node_api.h changes the NAPI_MODULE_INIT macro so that each .node module defines global const node_api_module_vtable* g_node_api_module_vtable and const node_api_js_native_vtable* g_node_api_js_native_vtable variables and exports the new node_api_module_set_vtable_v1 function that is called from the node_binding.cc to inject the Node-API v-tables.
  • The js_native_api.h and node_api.h also contain the implementation of all Node-API functions as static inline functions that use the global variables. These functions become part of the .node module after compilation. They do not exist when we compile Node.js code. The new macro NODE_API_MODULE_USE_VTABLE controls where the header files define Node-API function prototypes or the new static inline functions.

After this change all test modules compile and run without changes.
The NODE_API_MODULE_USE_VTABLE is only used for two tests: node-api\1_hello_world and js-native-api\7_factory_wrap.
When we look at the imports of these modules we do not see any imports from the node.exe.
It means that we can stop using the win_delay_load_hook.cc and be able to load the pre-built .node module from any runtime that supports Node-API.

The idea to use a v-table for the API is not new. E.g. Java JNI API is also based on a v-table.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/gyp
  • @nodejs/node-api

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Dec 1, 2025
@vmoroz vmoroz marked this pull request as draft December 1, 2025 03:58
@kraenhansen
Copy link

kraenhansen commented Dec 1, 2025

From the PR description this seems super valuable and a great built-in alternative to the weak-node-api library we've been working on to add Node-API support to React Native. As such, I'd be happy for us to adopt this approach over the stuff we have now 👍

The limitation around the module being bound to a single runtime, might not be an issue as a multi-runtime host can inject functions that deal with that internally.

I'm left wondering how (if at all) add-ons which are statically linked into the process are affected by this proposal? I guess not at all, as they can still register themselves and call into the global Node-API functions resolved at link time.

@devsnek
Copy link
Member

devsnek commented Dec 1, 2025

nice! node-api symbol visibility has been a pain for us in deno so we'd love to adopt this approach as well.

NULL, \
{0}, \
}; \
NAPI_C_CTOR(_register_##modname) { napi_module_register(&_module); } \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still support addons compiled with legacy Node-API headers. Could this new v-table approach be opt-in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we should make it opt-in to start with and play with it a bit.
If it works well, then we can promote it to be default at some point.
I considered to use the NAPI_EXPERIMENTAL, but it seems it is not the right approach since we use the NAPI_EXPERIMENTAL for new Node-API functions, and the module loading is an orthogonal process. Thus, a special opt-in flag would be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a new macro NODE_API_MODULE_USE_VTABLE. It is currently used only by two tests.

@legendecas legendecas added the node-api Issues and PRs related to the Node-API. label Dec 1, 2025
#define NAPI_NO_RETURN
#endif

// Used by deprecated registration method napi_module_register.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anyone wondering, this was moved into the node_api_types.h.

@kraenhansen
Copy link

There might be a potential a potential coordination problem, where the addon tries to initialize itself before the vtable gets injected (set) by the host. I don't see that as an issue when the addon relies on "symbol based" module registration, since the host can ensure to call the initialize function after setting the vtable, but how about an addon trying to call napi_module_register when loaded?

@devsnek
Copy link
Member

devsnek commented Dec 1, 2025

but how about an addon trying to call napi_module_register when loaded?

these should probably be exclusive modes, so napi_module_register wouldn't be called, or could only be called from within the host call to the inject function.

src/node_api.cc Outdated

#endif

node_api_vtable g_vtable = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be const?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I will fix it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added const for the global variables related to the v-tables.

@vmoroz
Copy link
Member Author

vmoroz commented Dec 1, 2025

There might be a potential a potential coordination problem, where the addon tries to initialize itself before the vtable gets injected (set) by the host. I don't see that as an issue when the addon relies on "symbol based" module registration, since the host can ensure to call the initialize function after setting the vtable, but how about an addon trying to call napi_module_register when loaded?

The napi_module_register usage is deprecated. No new code is supposed to use it anymore.
We used to have a deprecated attribute, but we found that some developers use it to register modules explictly. E.g. when the modules are part of the host executable.
I do not have a good answer to that besides that the exisitng public API is still there and user can use as before.

I am going to add a conditional flag and restore the deleted test as @legendecas suggested. This test is using the napi_module_register method and it can be used as a show case how to use the Node-API directly when needed.

@vmoroz
Copy link
Member Author

vmoroz commented Dec 1, 2025

I'm left wondering how (if at all) add-ons which are statically linked into the process are affected by this proposal? I guess not at all, as they can still register themselves and call into the global Node-API functions resolved at link time.

Right, I would expect it too, but we should verify and test this scenario.

@vmoroz
Copy link
Member Author

vmoroz commented Dec 1, 2025

The issues that I am facing on Mac and Linux are due to the use of real "C" compilers where the inline keyword has a different semantic than in "C++". Changing it to static inline generates its own set of issues. Thus, I am still working on it.

@RobinWuu
Copy link

RobinWuu commented Dec 2, 2025

Nice!👍 In Lynx/PrimJS, we are currently using a similar vtable approach to address the needs of multi-runtime injection. However, our previous API was not fully aligned with the Node-API standard, which is a problem I have been working to fix recently.
We’re thrilled to see this solution will potentially be natively integrated into Node.js, and we’re also more than happy to adopt this approach.

@vmoroz vmoroz force-pushed the pr/node_api_vtable branch from cf9b056 to f3d584b Compare December 2, 2025 15:17
@vmoroz
Copy link
Member Author

vmoroz commented Dec 2, 2025

Nice!👍 In Lynx/PrimJS, we are currently using a similar vtable approach to address the needs of multi-runtime injection. However, our previous API was not fully aligned with the Node-API standard, which is a problem I have been working to fix recently. We’re thrilled to see this solution will potentially be natively integrated into Node.js, and we’re also more than happy to adopt this approach.

It is great to hear it! Any suggestions to improve are welcome.

@vmoroz vmoroz marked this pull request as ready for review December 2, 2025 15:35
@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 84.61538% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.53%. Comparing base (4ea921b) to head (f3d584b).

Files with missing lines Patch % Lines
src/node_binding.cc 81.81% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #60916      +/-   ##
==========================================
+ Coverage   88.52%   88.53%   +0.01%     
==========================================
  Files         703      703              
  Lines      208396   208406      +10     
  Branches    40185    40184       -1     
==========================================
+ Hits       184483   184513      +30     
+ Misses      15918    15893      -25     
- Partials     7995     8000       +5     
Files with missing lines Coverage Δ
src/js_native_api_v8.cc 76.63% <100.00%> (+0.02%) ⬆️
src/node_api.cc 75.21% <100.00%> (+0.06%) ⬆️
src/node_binding.cc 83.52% <81.81%> (+0.77%) ⬆️

... and 30 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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++. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. node-api Issues and PRs related to the Node-API.

Projects

Status: Need Triage

Development

Successfully merging this pull request may close these issues.

7 participants