Skip to content

Commit 58223c0

Browse files
committed
Android: dlopen conformance addons as dynamic .node backed by a shared libnapi.so
Replaces the interim static-link-into-host approach with the dynamic .node model used by nodejs/node-api-cts (add_node_api_cts_addon), so the Android suite and a future node-api-cts migration share one addon model. - Core/Node-API: build napi as a SHARED library (libnapi.so) on Android. It exports all 106 napi_* (default visibility -- no global -fvisibility=hidden), and the host plus every addon depend on the one libnapi.so via a real DT_NEEDED, so there is a single napi instance. Static elsewhere. - The conformance addons are again standalone SHARED lib<name>.so (packaged into nativeLibraryDir), now linking napi (DT_NEEDED libnapi.so) instead of -Wl,--unresolved-symbols=ignore-all. - node_lite_android resolves entry points via dlopen(soname)+dlsym again; the addon's napi_* bind from libnapi.so at load. Reverts the static-link infra (entry_point.h JSR_NODE_API_STATIC_LINK branch, node_api.h overridable registrar macros, the host's per-addon OBJECT libraries). Verified on device: lib2_function_arguments.so has DT_NEEDED [libnapi.so], its napi_* are imports, libnapi.so exports the 106 napi_*, and all 4 v5 js-native-api tests pass in-process. macOS unchanged (12/12; desktop keeps static napi + dlopen'd MODULE .node).
1 parent b51d1a7 commit 58223c0

7 files changed

Lines changed: 90 additions & 165 deletions

File tree

Core/Node-API/CMakeLists.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,17 @@ if(NAPI_BUILD_ABI)
155155
message(STATUS "Selected ${NAPI_JAVASCRIPT_ENGINE}")
156156
endif()
157157

158-
add_library(napi ${SOURCES})
158+
# On Android the Node-API conformance addons are dlopen'd as standalone SHARED .node modules (matching
159+
# nodejs/node-api-cts's add_node_api_cts_addon model). For an addon's napi_* imports to bind at load,
160+
# napi must be a shared library that both the host and the addons depend on via a real DT_NEEDED
161+
# (libnapi.so) -- bionic will not surface a statically-linked host's napi to a dlopen'd module. There
162+
# is a single napi instance (one libnapi.so) shared by the host and every addon. Elsewhere (the
163+
# child-process desktop runner) napi stays a static library.
164+
if(ANDROID)
165+
add_library(napi SHARED ${SOURCES})
166+
else()
167+
add_library(napi ${SOURCES})
168+
endif()
159169

160170
target_include_directories(napi ${INCLUDE_DIRECTORIES})
161171
target_link_libraries(napi ${LINK_LIBRARIES})

Tests/NodeApi/CMakeLists.txt

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,6 @@ if(JSR_NODE_API_BUILD_NATIVE_TESTS)
120120
endif()
121121
122122
function(add_node_api_module MODULE_TARGET)
123-
if(ANDROID)
124-
# On Android the conformance addons are statically linked into the in-process test host
125-
# (Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt) -- there is no standalone .node to
126-
# build or dlopen, so this desktop helper builds nothing here.
127-
return()
128-
endif()
129-
130123
cmake_parse_arguments(PARSE_ARGV 0 ARG "" "" "SOURCES;DEFINES")
131124
132125
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME)
@@ -135,8 +128,14 @@ function(add_node_api_module MODULE_TARGET)
135128
set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}")
136129
endif()
137130
138-
# The addon is a MODULE (dlopen-only) .node, loaded by the node_lite child-process runner.
139-
add_library(${MODULE_TARGET} MODULE)
131+
# On Android the addon is a SHARED lib<name>.so so AGP packages it into the APK's lib/<abi>/ (the
132+
# app's nativeLibraryDir, the only place a native library may be dlopen'd from on API 29+).
133+
# Elsewhere it is a MODULE (dlopen-only) .node loaded by the node_lite child-process runner.
134+
if(ANDROID)
135+
add_library(${MODULE_TARGET} SHARED)
136+
else()
137+
add_library(${MODULE_TARGET} MODULE)
138+
endif()
140139
target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES})
141140
target_include_directories(${MODULE_TARGET}
142141
PRIVATE
@@ -161,36 +160,50 @@ function(add_node_api_module MODULE_TARGET)
161160
PRIVATE
162161
"-undefined" "dynamic_lookup"
163162
)
163+
elseif(ANDROID)
164+
# Link the shared napi (libnapi.so) so the addon's napi_* imports resolve at dlopen via a real
165+
# DT_NEEDED -- the same libnapi.so the host depends on, so there is a single napi instance.
166+
target_link_libraries(${MODULE_TARGET} PRIVATE napi)
164167
endif()
165168
166-
set(MODULE_OUTPUT_DIR
167-
${CMAKE_CURRENT_BINARY_DIR}/build/$<IF:$<CONFIG:Debug>,Debug,Release>)
168-
set_target_properties(${MODULE_TARGET}
169-
PROPERTIES
170-
PREFIX ""
171-
SUFFIX ".node"
172-
ARCHIVE_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
173-
LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
174-
RUNTIME_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
175-
)
169+
if(ANDROID)
170+
set_target_properties(${MODULE_TARGET}
171+
PROPERTIES
172+
PREFIX "lib"
173+
SUFFIX ".so"
174+
)
175+
else()
176+
set(MODULE_OUTPUT_DIR
177+
${CMAKE_CURRENT_BINARY_DIR}/build/$<IF:$<CONFIG:Debug>,Debug,Release>)
178+
set_target_properties(${MODULE_TARGET}
179+
PROPERTIES
180+
PREFIX ""
181+
SUFFIX ".node"
182+
ARCHIVE_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
183+
LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
184+
RUNTIME_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR}
185+
)
186+
endif()
176187
177188
add_dependencies(NodeApiModules ${MODULE_TARGET})
178189
179-
# Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it
180-
# relative to the copied test files.
181-
add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD
182-
COMMAND ${CMAKE_COMMAND} -E make_directory
183-
$<TARGET_FILE_DIR:node_lite>/test/js-native-api/${FOLDER_NAME}/build
184-
COMMAND ${CMAKE_COMMAND} -E copy_directory
185-
${CMAKE_CURRENT_BINARY_DIR}/build
186-
$<TARGET_FILE_DIR:node_lite>/test/js-native-api/${FOLDER_NAME}/build
187-
COMMAND ${CMAKE_COMMAND} -E make_directory
188-
$<TARGET_FILE_DIR:NodeApiTests>/test/js-native-api/${FOLDER_NAME}/build
189-
COMMAND ${CMAKE_COMMAND} -E copy_directory
190-
${CMAKE_CURRENT_BINARY_DIR}/build
191-
$<TARGET_FILE_DIR:NodeApiTests>/test/js-native-api/${FOLDER_NAME}/build
192-
COMMENT "Copying Node-API module ${MODULE_TARGET} outputs"
193-
)
190+
if(NOT ANDROID)
191+
# Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it
192+
# relative to the copied test files.
193+
add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD
194+
COMMAND ${CMAKE_COMMAND} -E make_directory
195+
$<TARGET_FILE_DIR:node_lite>/test/js-native-api/${FOLDER_NAME}/build
196+
COMMAND ${CMAKE_COMMAND} -E copy_directory
197+
${CMAKE_CURRENT_BINARY_DIR}/build
198+
$<TARGET_FILE_DIR:node_lite>/test/js-native-api/${FOLDER_NAME}/build
199+
COMMAND ${CMAKE_COMMAND} -E make_directory
200+
$<TARGET_FILE_DIR:NodeApiTests>/test/js-native-api/${FOLDER_NAME}/build
201+
COMMAND ${CMAKE_COMMAND} -E copy_directory
202+
${CMAKE_CURRENT_BINARY_DIR}/build
203+
$<TARGET_FILE_DIR:NodeApiTests>/test/js-native-api/${FOLDER_NAME}/build
204+
COMMENT "Copying Node-API module ${MODULE_TARGET} outputs"
205+
)
206+
endif()
194207
195208
if(APPLE)
196209
# The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE

Tests/NodeApi/include/node_api.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,8 @@ typedef struct napi_module_s {
3535
void* reserved[4];
3636
} napi_module;
3737

38-
// These may be overridden by the build (e.g. -DNODE_API_MODULE_REGISTER_FUNCTION=...) so that several
39-
// addons can be *statically* linked into a single host binary -- each with a uniquely suffixed entry
40-
// point -- without symbol clashes. The defaults are the canonical names a dynamic loader (dlsym)
41-
// looks up in a standalone .node.
42-
#ifndef NODE_API_MODULE_GET_API_VERSION_FUNCTION
4338
#define NODE_API_MODULE_GET_API_VERSION_FUNCTION node_api_module_get_api_version_v1
44-
#endif
45-
#ifndef NODE_API_MODULE_REGISTER_FUNCTION
4639
#define NODE_API_MODULE_REGISTER_FUNCTION napi_register_module_v1
47-
#endif
4840

4941
#define NAPI_MODULE_INIT() \
5042
NODE_API_EXTERN_C_START \

Tests/NodeApi/node_lite.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,10 @@ fs::path NodeLiteRuntime::ResolveModulePath(
406406
return node_module_path;
407407
}
408408
#if defined(__ANDROID__)
409-
// On Android the conformance addons are statically linked into the host rather than present as
410-
// .node files on disk, so the existence check above fails. Resolve to the .node path anyway so
411-
// LoadNativeModule runs; node_lite_android resolves the entry points from the in-process static
412-
// registry, keyed by this path's stem (the module name).
409+
// On Android the addon ships as lib<name>.so in the app's nativeLibraryDir rather than as a .node
410+
// file on disk, so the existence check above fails. Resolve to the .node path anyway so
411+
// LoadNativeModule runs; node_lite_android dlopens it by soname (lib<stem>.so), resolving its
412+
// napi_* imports from the shared libnapi.so.
413413
return node_module_path;
414414
#endif
415415
// See if the module was prefixed with the parent folder to disambiguate C++
Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,27 @@
11
#include "node_lite.h"
22

3-
#include <cstdint>
4-
#include <string>
5-
#include <vector>
3+
#include <dlfcn.h>
64

75
namespace node_api_tests {
8-
namespace {
96

10-
// Registry of the Node-API conformance addons that are statically linked into this host binary. It is
11-
// populated at load time by the per-addon constructors emitted in entry_point.h under
12-
// JSR_NODE_API_STATIC_LINK, each registering its uniquely-suffixed entry points keyed by module name.
13-
// A function-local static (constructed on first registration) avoids any static-initialization-order
14-
// dependency between those constructors and this translation unit.
15-
struct StaticAddon {
16-
std::string name;
17-
int32_t (*get_api_version)(void);
18-
napi_value (*register_module)(napi_env, napi_value);
19-
};
20-
21-
std::vector<StaticAddon>& StaticAddonRegistry() {
22-
static std::vector<StaticAddon> registry;
23-
return registry;
24-
}
25-
26-
} // namespace
27-
} // namespace node_api_tests
28-
29-
// Called once per statically-linked addon from its load-time constructor (see entry_point.h). Declared
30-
// extern "C" to match the declaration the C addon translation units compile against.
31-
extern "C" void jsr_register_static_addon(
32-
const char* name,
33-
int32_t (*get_api_version)(void),
34-
napi_value (*register_module)(napi_env, napi_value)) {
35-
node_api_tests::StaticAddonRegistry().push_back(
36-
{name, get_api_version, register_module});
37-
}
38-
39-
namespace node_api_tests {
40-
41-
// On Android the conformance addons are statically linked into the host rather than dlopen'd: dynamic
42-
// .node loading is never shipped to the Play / Quest stores, and bionic won't resolve a dlopen'd
43-
// addon's napi_* against the System.loadLibrary-loaded host anyway (see NAPI_VERSION_ROADMAP.md, and
44-
// task #9 for the shared-lib alternative). So resolve the requested entry point from the in-process
45-
// static registry, keyed by the module's file-stem name, instead of dlopen+dlsym.
467
/*static*/ void* NodeLitePlatform::LoadFunction(
478
napi_env /*env*/,
489
const std::filesystem::path& lib_path,
4910
const std::string& function_name) noexcept
5011
{
51-
const std::string module_name = lib_path.stem().string();
52-
for (const StaticAddon& addon : StaticAddonRegistry()) {
53-
if (addon.name != module_name) {
54-
continue;
55-
}
56-
if (function_name == "napi_register_module_v1") {
57-
return reinterpret_cast<void*>(addon.register_module);
58-
}
59-
if (function_name == "node_api_module_get_api_version_v1") {
60-
return reinterpret_cast<void*>(addon.get_api_version);
61-
}
12+
// On Android the conformance addons are packaged as lib<name>.so in the app's nativeLibraryDir --
13+
// the only location a native library may be dlopen'd from on API 29+. The resolved lib_path points
14+
// at a (non-existent) <name>.node under the copied test tree, so load by soname and let the dynamic
15+
// linker resolve it from nativeLibraryDir. The addon's napi_* imports resolve from libnapi.so -- a
16+
// DT_NEEDED of both the addon and the host -- so RTLD_NOW binds them at load time.
17+
std::string soname = "lib" + lib_path.stem().string() + ".so";
18+
void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL);
19+
if (handle == nullptr)
20+
{
6221
return nullptr;
6322
}
64-
return nullptr;
23+
24+
return dlsym(handle, function_name.c_str());
6525
}
6626

6727
} // namespace node_api_tests
Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,12 @@
1-
#ifndef JS_NATIVE_API_ENTRY_POINT_H_
2-
#define JS_NATIVE_API_ENTRY_POINT_H_
3-
4-
#include <node_api.h>
5-
6-
#if defined(JSR_NODE_API_STATIC_LINK)
7-
// Static-link mode (the Android in-process conformance runner): every addon is linked into one host
8-
// binary, so Init must have internal linkage to avoid one-definition clashes across addons. The
9-
// registrar/version functions are uniquely suffixed per-module by the build (NODE_API_MODULE_*_FUNCTION
10-
// in node_api.h). A later non-static definition of Init in the addon inherits this internal linkage.
11-
static napi_value Init(napi_env env, napi_value exports);
12-
#else
13-
EXTERN_C_START
14-
napi_value Init(napi_env env, napi_value exports);
15-
EXTERN_C_END
16-
#endif
17-
18-
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
19-
20-
#if defined(JSR_NODE_API_STATIC_LINK)
21-
// Self-register this addon's uniquely-named entry points with the host at load time, keyed by module
22-
// name, so the in-process loader (node_lite_android LoadFunction) can find them by name -- the static
23-
// equivalent of dlopen+dlsym. jsr_register_static_addon is implemented by the host.
24-
EXTERN_C_START
25-
void jsr_register_static_addon(
26-
const char* name,
27-
int32_t (*get_api_version)(void),
28-
napi_value (*register_module)(napi_env, napi_value));
29-
EXTERN_C_END
30-
31-
static void __attribute__((constructor)) jsr_register_static_addon_ctor(void) {
32-
jsr_register_static_addon(NODE_GYP_MODULE_NAME,
33-
NODE_API_MODULE_GET_API_VERSION_FUNCTION,
34-
NODE_API_MODULE_REGISTER_FUNCTION);
35-
}
36-
#endif
37-
38-
#endif // JS_NATIVE_API_ENTRY_POINT_H_
1+
#ifndef JS_NATIVE_API_ENTRY_POINT_H_
2+
#define JS_NATIVE_API_ENTRY_POINT_H_
3+
4+
#include <node_api.h>
5+
6+
EXTERN_C_START
7+
napi_value Init(napi_env env, napi_value exports);
8+
EXTERN_C_END
9+
10+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
11+
12+
#endif // JS_NATIVE_API_ENTRY_POINT_H_

Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,33 +73,9 @@ target_link_libraries(UnitTestsJNI
7373
if(ANDROID)
7474
target_link_libraries(UnitTestsJNI PRIVATE dl)
7575
endif()
76-
set(JSR_NODE_API_NATIVE_TESTS 2_function_arguments 3_callbacks 4_object_factory 5_function_factory)
77-
string(JOIN "," JSR_NODE_API_NATIVE_TESTS_CSV ${JSR_NODE_API_NATIVE_TESTS})
7876
target_compile_definitions(UnitTestsJNI
79-
PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="${JSR_NODE_API_NATIVE_TESTS_CSV}")
77+
PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="2_function_arguments,3_callbacks,4_object_factory,5_function_factory")
8078

81-
# --- Statically linked Node-API conformance addons (Android in-process runner) ------------------
82-
# Dynamic .node loading is never shipped to the Play / Quest stores, and bionic will not resolve a
83-
# dlopen'd addon's napi_* imports against the System.loadLibrary-loaded host anyway (see
84-
# NAPI_VERSION_ROADMAP.md; task #9 tracks the shared-lib alternative). So compile each addon into the
85-
# host with a uniquely suffixed entry point -- avoiding one-definition clashes between addons -- and
86-
# let node_lite resolve them from the in-process static registry (node_lite_android) instead of dlsym.
87-
# entry_point.h emits, per addon, a load-time constructor that self-registers those entry points.
88-
foreach(_mod ${JSR_NODE_API_NATIVE_TESTS})
89-
add_library(jsr_addon_${_mod} OBJECT
90-
${TESTS_DIR}/NodeApi/test/js-native-api/${_mod}/${_mod}.c)
91-
target_include_directories(jsr_addon_${_mod} PRIVATE
92-
${TESTS_DIR}/NodeApi/include
93-
${TESTS_DIR}/NodeApi/test/js-native-api
94-
${REPO_ROOT_DIR}/Core/Node-API/Include/Shared
95-
${REPO_ROOT_DIR}/Core/Node-API/Include/Shared/napi
96-
${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}
97-
${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi)
98-
target_compile_definitions(jsr_addon_${_mod} PRIVATE
99-
JSR_NODE_API_STATIC_LINK
100-
NODE_API_EXPERIMENTAL_NO_WARNING
101-
NODE_GYP_MODULE_NAME=\"${_mod}\"
102-
NODE_API_MODULE_REGISTER_FUNCTION=jsr_napi_register_${_mod}
103-
NODE_API_MODULE_GET_API_VERSION_FUNCTION=jsr_get_api_version_${_mod})
104-
target_sources(UnitTestsJNI PRIVATE $<TARGET_OBJECTS:jsr_addon_${_mod}>)
105-
endforeach()
79+
# The conformance addons are built as standalone SHARED lib<name>.so by Tests/NodeApi/CMakeLists.txt,
80+
# packaged by AGP into nativeLibraryDir, and dlopen'd in-process by node_lite_android. Both they and
81+
# this host link the shared napi (libnapi.so), so the addons' napi_* imports resolve at load.

0 commit comments

Comments
 (0)