Skip to content

Conversation

kraenhansen
Copy link
Collaborator

@kraenhansen kraenhansen commented May 10, 2025

Merging this PR will:

  • Change weak-node-api to have the Node-API functions injected from the host, instead of resolving them itself.
  • Load weak-node-api into the process without polluting the global symbol registry, when the host injects the Node-API functions from libhermes and the host (which is future work).
  • Make the host consume weak-node-api as a pre-built, like the Node-API modules are already doing.
  • Strip the "lib" prefix from prebuild directory names, if a project generates a lib-prefixed binary.
  • Add a cross-platform log_debug function (which we'll likely want to mute in the future).
  • Fix outstanding linking issues, making Node-API modules built with our CMake wrapper or Ferric, load and register on iOS and Android.

Here's at generated files: https://gist.github.com/kraenhansen/049fac2b03daacab1ebc41ca425920ca

@kraenhansen kraenhansen self-assigned this May 10, 2025
Copy link

changeset-bot bot commented May 10, 2025

⚠️ No Changeset found

Latest commit: 1862e3b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@kraenhansen kraenhansen marked this pull request as ready for review May 10, 2025 13:03
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from a2539b0 to 63d865c Compare May 10, 2025 13:14
"#include <node_api.h>", // Node-API
"#include <stdio.h>", // fprintf()
"#include <stdlib.h>", // abort()
"namespace node_api::internal {",
Copy link
Collaborator Author

@kraenhansen kraenhansen May 10, 2025

Choose a reason for hiding this comment

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

I wonder if this is too intrusive? Should keep this under callstack:: instead or is this okay? I mean, ideally this would be usable outside of our setup.

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

Removed this as it served little purpose now that the inject function is top-level.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Now that I'm no longer using a namespace, nor any C++ features, I'm thinking about making this an actual .c file to make sure I'm not using any C++ features. IDYT @mani3xis?

@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from 63d865c to 9e0f637 Compare May 10, 2025 20:44
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from 9e0f637 to 9f18d8c Compare May 11, 2025 10:43
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from 9f18d8c to 86cff81 Compare May 11, 2025 20:36
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch 2 times, most recently from a438cb8 to 55e73d0 Compare May 11, 2025 22:31
Base automatically changed from kh/ferric to main May 11, 2025 22:48
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from 55e73d0 to 2a04f9e Compare May 11, 2025 22:49
@kraenhansen kraenhansen requested a review from mani3xis May 11, 2025 22:49
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch 3 times, most recently from 6b024e9 to d04ce9e Compare May 12, 2025 00:02
@kraenhansen kraenhansen linked an issue May 14, 2025 that may be closed by this pull request
@kraenhansen kraenhansen force-pushed the kh/weak-node-api-injection branch from 629a063 to 74b48e1 Compare May 15, 2025 05:53
Copy link
Contributor

@mani3xis mani3xis left a comment

Choose a reason for hiding this comment

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

I found a major issue (relying on mangled name) and a few minors and nits. Overall looks good!

"aarch64-linux-android": "arm64-v8a",
"armv7-linux-androideabi": "armeabi-v7a",
"i686-linux-android": "x86",
"x86_64-linux-android": "x86_64",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: What about making a type for the values to avoid typos? I'm sure that you listed all values already, so "arm64-v8a" | "armeabi-v7a" | "x86" | "x86_64" should do the trick, wdyt?

nit 2: Maybe we should pick a more general/unified way of describing targets / cpu archs? I'm afraid that adding new platforms will case an explosion of enums.

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

And use that type instead of string in the Record above? I don't see that I will be referencing that type elsewhere 🤔 Where would you use that type? Perhaps a suggestive edit would help me understand your intent.

const weakNodeApiPath = getWeakNodeApiAndroidLibraryPath(target);

return {
RUSTFLAGS: `-L ${weakNodeApiPath} -l weak-node-api`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Hopefully there are no spaces in the path (or at least the string gets quoted in such case)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right - great catch. This should be quoted 👍

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

Hmm - it failed when I quoted it (either with " or ') 🤔 It seems CARGO_ENCODED_RUSTFLAGS is what I'd have to use 😆

}
});
assert.equal(result.length, 1, "Expected exactly library file");
const result = readdirSync(tripletOutputPath, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: Perhaps we can use asynchronous functions instead and use await Promise.all(). This should help performance a bit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I turned all the sync calls (except existsSync) into async I/O 👍

/weak-node-api/*.android.node
/weak-node-api/weak-node-api.cpp
/weak-node-api/weak_node_api.cpp
/weak-node-api/weak_node_api.hpp
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: /weak-node-api/weak_node_api.[ch]pp if you like one-liners

namespace callstack::nodeapihost {
using node_api::internal::inject_host_t;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: The _t suffix is reserved by POSIX but this is not a big deal breaker. I would recommend to follow the convention and use CamelCase for types (as used in React and rest of our code), like NodeApiHost type below.

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

Okay, thanks 👍 How about InjectHostFunction?

// Generate function calling into the host
...functions.flatMap(({ returnType, name, argumentTypes }) => {
return [
`extern "C" ${returnType} ${name}(${argumentTypes
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing calling convention specifier and there's one function that does not return (napi_fatal_error IIRC) which also had noreturn attribute.

Moreover, I would highly advise to keep the NAPI_VERSION-specific #if...#endif macros in the generated source. We would be able to control which functions are "visible" and therefore generate errors when somebody tries to use function that we do not support.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

keep the NAPI_VERSION-specific #if...#endif macros in the generated source

We talked about this in person, right?

As I expect this code to be compiled into a single prebuilt and linked to from multiple Node-API modules (each with their own NAPI_VERSIONs to restrict their available API), I don't see a scenario where we'd want to provide a prebuilt with symbols less than NAPI_VERSION 10. To me the constant is entirely a "consumer side" concept. (And I might be wrong about that?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Missing calling convention specifier

What more than extern "C" do we need in your opinion?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll make sure to add the noreturn 👍

.join(", ")}) {`,
`if (g_host.${name} == nullptr) {`,
` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`,
" abort();",
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should introduce a "backdoor" which would allow users to configure how to behave in such a case? For example, in my first draft which I've generated earlier this week I'm using the ASSERT() macro for such checks (yet I'm looking for a better name). This can be a fallback to assert() from assert.h or whatever the user would like to have. Might be a great addition for testing/mocking.

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

would allow users to configure how to behave in such a case

Again, I expect us to ship the prebuild for this and I therefore expect that to be outside of the control of users.

That being said, I'd be happy to extend this, once I see a clear need 👍
I mean, we would want something relying on that behavior if we add it in.

fallbackReturnStatement: string;
};

export function getNodeApiFunctions(version: NodeApiVersion = "v8") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I understand where this is coming from (the default version when not specified), although wouldn't it be better to start with v1 which we are not supporting yet?

Copy link
Collaborator Author

@kraenhansen kraenhansen May 17, 2025

Choose a reason for hiding this comment

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

I am contemplating moving this to a top-level constant instead. As mentioned above, I don't understand the need for weak-node-api to be built with less than the highest number available - I might actually want to bump it to v10 🙈

Would you rather that prebuilds to fail compiling instead of us aborting at runtime? (Less and less as we increase our support).

${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: As mentioned earlier, shouldn't we start with NAPI_VERSION=1? Moreover hardcoding the version here might not be a good idea, as it would be hard to generate prebuilds for various versions (like with node-pre-gyp and friends).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same as above ☺️

`extern "C" ${returnType} ${name}(${argumentTypes
.map((type, index) => `${type} arg${index}`)
.join(", ")}) {`,
`if (g_host.${name} == nullptr) {`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment (just to leave the trace from our yesterday's call): This will work for (the deprecated) napi_register_module() only when weak-node-api is built as shared-library and only if we guarantee that host injected the functions before the add-on is loaded! In earlier modules, there was a macro which generated a function that would call this napi_register_module(), and it was decorated with __attribute__((constructor)), meaning that a call to napi_register_module() happened BEFORE dlopen() returned, leaving no time to inject functions (when statically linked).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Didn't we talk about this and realized this would only be an issue on a platform where all dynamic libraries are preloaded on app startup (iOS)? I mean, we'd expect to be in control of when the dlopen of the Node-API module is happening and we can make sure the functions are injected beforehand.

@kraenhansen kraenhansen merged commit 0a638a9 into main May 19, 2025
3 checks passed
@kraenhansen kraenhansen deleted the kh/weak-node-api-injection branch May 19, 2025 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor weak-node-api to have Node-API functions injected instead of loading the itself Implement stubs for the missing runtime Node-API symbols

2 participants