@crossbind/core-embind-napi 0.0.0-bootstrap.0 → 2.0.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -3
- package/cpp/CMakeLists.txt +76 -0
- package/cpp/check-undefined.cmake +38 -0
- package/cpp/src/node_api_module.cpp +157 -0
- package/js/addonLocation.js +13 -0
- package/js/addonPlatform.js +19 -0
- package/js/loader.js +97 -0
- package/js/missingAddon.js +9 -0
- package/js/stopOnExit.js +15 -0
- package/js/unpackedPath.js +5 -0
- package/package.json +29 -6
- package/third_party/node-api-jsi/LICENSE +21 -0
- package/third_party/node-api-jsi/README.md +59 -0
- package/third_party/node-api-jsi/jsi/jsi/instrumentation.h +150 -0
- package/third_party/node-api-jsi/jsi/jsi/jsi-inl.h +425 -0
- package/third_party/node-api-jsi/jsi/jsi/jsi.cpp +990 -0
- package/third_party/node-api-jsi/jsi/jsi/jsi.h +2031 -0
- package/third_party/node-api-jsi/node-api/js_native_api.h +627 -0
- package/third_party/node-api-jsi/node-api/js_native_api_types.h +211 -0
- package/third_party/node-api-jsi/node-api/js_runtime_api.h +214 -0
- package/third_party/node-api-jsi/node-api/node_api.h +270 -0
- package/third_party/node-api-jsi/node-api/node_api_types.h +52 -0
- package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.cpp +79 -0
- package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.h +51 -0
- package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.inc +50 -0
- package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.cpp +41 -0
- package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.h +127 -0
- package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.inc +125 -0
- package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_posix.cpp +16 -0
- package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_win.cpp +23 -0
- package/third_party/node-api-jsi/src/NodeApiJsiRuntime.cpp +3872 -0
- package/third_party/node-api-jsi/src/NodeApiJsiRuntime.h +39 -0
package/README.md
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
# @crossbind/core-embind-napi
|
|
2
|
+
**crossbind Node-API host**
|
|
3
|
+
Runs the crossbind embind runtime inside a native Node.js addon, so the bindings that serve
|
|
4
|
+
WebAssembly and React Native also build `.node` addons for Node.js and Electron.
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
<a href="https://www.npmjs.com/package/@crossbind/core-embind-napi">
|
|
7
|
+
<img alt="NPM version" src="https://img.shields.io/npm/v/@crossbind/core-embind-napi/beta?style=for-the-badge" />
|
|
8
|
+
</a>
|
|
9
|
+
<a href="https://github.com/crossbind/crossbind/blob/main/LICENSE">
|
|
10
|
+
<img alt="License" src="https://img.shields.io/github/license/crossbind/crossbind?style=for-the-badge" />
|
|
11
|
+
</a>
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
Add it next to `crossbind` and build the addons of this machine's platform, or of every desktop
|
|
16
|
+
platform:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D crossbind@beta @crossbind/core-embind-napi@beta
|
|
20
|
+
pnpm crossbind build -p host -e node -b release
|
|
21
|
+
pnpm crossbind build -p darwin,linux,linuxmusl,win32 -e node -b release
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`dist/` then holds one addon per platform and architecture (e.g. `<name>.darwin-arm64.node`,
|
|
25
|
+
`<name>.linux-x64.node`, `<name>.win32-x64.node`) and the loader that picks one at runtime:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const initNative = require('./dist/<name>.native.cjs');
|
|
29
|
+
|
|
30
|
+
const { Native } = await initNative();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
crossbind resolves this package from your project; you never import it yourself. See the
|
|
34
|
+
[Node.js integration guide](https://github.com/crossbind/crossbind/blob/main/docs/playbooks/integration/nodejs.md#native-addon-node-api) for
|
|
35
|
+
requirements, Electron and current limits (no `worker_threads` yet).
|
|
36
|
+
|
|
37
|
+
## Contents
|
|
38
|
+
|
|
39
|
+
- `cpp/` — the addon's CMake project and its Node-API entry point.
|
|
40
|
+
- `js/loader.js` — the loader crossbind bundles into `dist/<name>.native.cjs`.
|
|
41
|
+
- `js/addonPlatform.js` — tells glibc and musl Linux apart, so the loader picks the `linux` or `linuxmusl` addon.
|
|
42
|
+
- `js/unpackedPath.js` — points a packaged Electron app's addon at the data its asar archive keeps unpacked.
|
|
43
|
+
- `third_party/node-api-jsi/` — Microsoft's JSI implementation over Node-API (MIT), vendored at a
|
|
44
|
+
pinned commit; provenance and local patches are in its README.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.28)
|
|
2
|
+
project(crossbind_node_addon CXX)
|
|
3
|
+
|
|
4
|
+
# crossbind configures this project for one Node-API addon: the embind runtime and its Node-API
|
|
5
|
+
# host compile here, the project's own code arrives as prebuilt archives in CROSSBIND_LINK_ARGS.
|
|
6
|
+
|
|
7
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
8
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
9
|
+
|
|
10
|
+
set(NODE_API_JSI "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/node-api-jsi")
|
|
11
|
+
|
|
12
|
+
if(WIN32)
|
|
13
|
+
set(NODE_API_LOADER "${NODE_API_JSI}/src/ApiLoaders/NodeApi_win.cpp")
|
|
14
|
+
else()
|
|
15
|
+
set(NODE_API_LOADER "${NODE_API_JSI}/src/ApiLoaders/NodeApi_posix.cpp")
|
|
16
|
+
endif()
|
|
17
|
+
|
|
18
|
+
add_library(addon MODULE
|
|
19
|
+
src/node_api_module.cpp
|
|
20
|
+
"${NODE_API_JSI}/src/NodeApiJsiRuntime.cpp"
|
|
21
|
+
"${NODE_API_JSI}/src/ApiLoaders/NodeApi.cpp"
|
|
22
|
+
"${NODE_API_LOADER}"
|
|
23
|
+
"${NODE_API_JSI}/src/ApiLoaders/JSRuntimeApi.cpp"
|
|
24
|
+
"${NODE_API_JSI}/jsi/jsi/jsi.cpp"
|
|
25
|
+
"${CROSSBIND_JSI_ROOT}/cpp/src/emscripten/bind.cpp"
|
|
26
|
+
${CROSSBIND_EXTRA_SOURCES})
|
|
27
|
+
|
|
28
|
+
set_target_properties(addon PROPERTIES PREFIX "" SUFFIX "" OUTPUT_NAME "${CROSSBIND_ADDON_FILE}")
|
|
29
|
+
|
|
30
|
+
target_include_directories(addon PRIVATE
|
|
31
|
+
"${NODE_API_JSI}/src"
|
|
32
|
+
"${NODE_API_JSI}/node-api"
|
|
33
|
+
"${NODE_API_JSI}/jsi"
|
|
34
|
+
"${CROSSBIND_JSI_ROOT}/cpp/src")
|
|
35
|
+
|
|
36
|
+
target_link_libraries(addon PRIVATE ${CROSSBIND_LINK_ARGS})
|
|
37
|
+
# A force-loaded archive reaches the link as a linker flag, which CMake does not track: without
|
|
38
|
+
# this, a rebuilt bridge archive would leave the previous addon in place.
|
|
39
|
+
set_property(TARGET addon APPEND PROPERTY LINK_DEPENDS ${CROSSBIND_LINK_DEPENDS})
|
|
40
|
+
|
|
41
|
+
# glibc 2.28 keeps threads and dlopen in libraries of their own. The archives' symbols stay private
|
|
42
|
+
# to the addon: node exports its own OpenSSL and zlib, and an exported copy here would trade calls
|
|
43
|
+
# with them.
|
|
44
|
+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|
45
|
+
find_package(Threads REQUIRED)
|
|
46
|
+
target_link_libraries(addon PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
|
|
47
|
+
target_link_options(addon PRIVATE "LINKER:--exclude-libs,ALL")
|
|
48
|
+
file(GLOB MUSL_LIBC "${CMAKE_SYSROOT}/lib/ld-musl-*.so.1")
|
|
49
|
+
add_custom_command(TARGET addon POST_BUILD
|
|
50
|
+
COMMAND "${CMAKE_COMMAND}" "-DNM=${CMAKE_NM}" "-DADDON=$<TARGET_FILE:addon>"
|
|
51
|
+
"-DUNVERSIONED_LIBRARIES=${MUSL_LIBC}"
|
|
52
|
+
-P "${CMAKE_CURRENT_SOURCE_DIR}/check-undefined.cmake"
|
|
53
|
+
VERBATIM)
|
|
54
|
+
endif()
|
|
55
|
+
|
|
56
|
+
# The C++ runtime, its unwinder and winpthreads go into the addon; the CRT stays the system's UCRT.
|
|
57
|
+
# node-api-jsi's pooled values call the sized operator delete, which clang leaves off for MinGW.
|
|
58
|
+
if(MINGW)
|
|
59
|
+
target_compile_options(addon PRIVATE -fsized-deallocation)
|
|
60
|
+
target_link_options(addon PRIVATE -static)
|
|
61
|
+
endif()
|
|
62
|
+
|
|
63
|
+
# Node-API symbols resolve against the node (or Electron) process that loads the addon. Everything
|
|
64
|
+
# else must resolve at link time, so a stale or missing archive fails the build instead of the load.
|
|
65
|
+
if(APPLE)
|
|
66
|
+
file(STRINGS "${NODE_API_JSI}/src/ApiLoaders/NodeApi.inc" NODE_API_FUNCS REGEX "^NODE_API_FUNC\\(")
|
|
67
|
+
foreach(line IN LISTS NODE_API_FUNCS)
|
|
68
|
+
string(REGEX REPLACE "^NODE_API_FUNC\\(([A-Za-z0-9_]+)\\).*" "\\1" func "${line}")
|
|
69
|
+
target_link_options(addon PRIVATE "LINKER:-U,_${func}")
|
|
70
|
+
endforeach()
|
|
71
|
+
# Only Node-API's entry points leave the addon: dyld merges exported weak definitions across the
|
|
72
|
+
# images of a process, so two addons would share inline state such as embind's registration guards.
|
|
73
|
+
target_link_options(addon PRIVATE
|
|
74
|
+
"LINKER:-exported_symbol,_napi_register_module_v1"
|
|
75
|
+
"LINKER:-exported_symbol,_node_api_module_get_api_version_v1")
|
|
76
|
+
endif()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# A Linux shared object may leave symbols undefined, and the loader binds functions lazily: an addon
|
|
2
|
+
# that misses an archive loads, then dies at its first call into the missing code. Every undefined
|
|
3
|
+
# symbol has to be a Node-API function or carry the version of a system library that defines it.
|
|
4
|
+
# musl versions nothing, so a name its C library (UNVERSIONED_LIBRARIES) defines passes as it is.
|
|
5
|
+
set(defined)
|
|
6
|
+
foreach(library IN LISTS UNVERSIONED_LIBRARIES)
|
|
7
|
+
execute_process(
|
|
8
|
+
COMMAND "${NM}" -D --defined-only "${library}"
|
|
9
|
+
OUTPUT_VARIABLE exports
|
|
10
|
+
COMMAND_ERROR_IS_FATAL ANY)
|
|
11
|
+
string(REPLACE "\n" ";" export_lines "${exports}")
|
|
12
|
+
foreach(export_line IN LISTS export_lines)
|
|
13
|
+
if(export_line MATCHES " ([^ ]+)$")
|
|
14
|
+
list(APPEND defined "${CMAKE_MATCH_1}")
|
|
15
|
+
endif()
|
|
16
|
+
endforeach()
|
|
17
|
+
endforeach()
|
|
18
|
+
execute_process(
|
|
19
|
+
COMMAND "${NM}" -D --undefined-only "${ADDON}"
|
|
20
|
+
OUTPUT_VARIABLE listing
|
|
21
|
+
COMMAND_ERROR_IS_FATAL ANY)
|
|
22
|
+
string(REPLACE "\n" ";" lines "${listing}")
|
|
23
|
+
set(unresolved)
|
|
24
|
+
foreach(line IN LISTS lines)
|
|
25
|
+
if(NOT line MATCHES "^ *U ([^@ ]+)$")
|
|
26
|
+
continue()
|
|
27
|
+
endif()
|
|
28
|
+
# A failed MATCHES clears CMAKE_MATCH_1, so the name is kept before the next one.
|
|
29
|
+
set(symbol "${CMAKE_MATCH_1}")
|
|
30
|
+
list(FIND defined "${symbol}" index)
|
|
31
|
+
if(NOT symbol MATCHES "^(napi|node_api)_" AND index EQUAL -1)
|
|
32
|
+
list(APPEND unresolved "${symbol}")
|
|
33
|
+
endif()
|
|
34
|
+
endforeach()
|
|
35
|
+
if(unresolved)
|
|
36
|
+
list(JOIN unresolved ", " names)
|
|
37
|
+
message(FATAL_ERROR "crossbind: ${ADDON} leaves symbols no archive defines: ${names}")
|
|
38
|
+
endif()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Node-API host for the embind runtime in @crossbind/core-embind-jsi. The environment whose start()
|
|
2
|
+
// runs first gets a JSI runtime; embind-jsi keeps one current runtime per process, so until runtimes
|
|
3
|
+
// are kept per environment a second one (a worker thread) is refused rather than left to race it.
|
|
4
|
+
#include <node_api.h>
|
|
5
|
+
|
|
6
|
+
#ifdef _WIN32
|
|
7
|
+
#ifndef WIN32_LEAN_AND_MEAN
|
|
8
|
+
#define WIN32_LEAN_AND_MEAN
|
|
9
|
+
#endif
|
|
10
|
+
#ifndef NOMINMAX
|
|
11
|
+
#define NOMINMAX
|
|
12
|
+
#endif
|
|
13
|
+
#include <windows.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
#include <atomic>
|
|
17
|
+
#include <cstring>
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <string>
|
|
20
|
+
|
|
21
|
+
#include <ApiLoaders/JSRuntimeApi.h>
|
|
22
|
+
#include <NodeApiJsiRuntime.h>
|
|
23
|
+
#include <emscripten/bind.h>
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
using Microsoft::NodeApiJsi::FuncPtr;
|
|
28
|
+
using Microsoft::NodeApiJsi::IFuncResolver;
|
|
29
|
+
using Microsoft::NodeApiJsi::JSRuntimeApi;
|
|
30
|
+
|
|
31
|
+
// node-api-jsi reaches Node-API through a function table; each entry points at the symbol this
|
|
32
|
+
// addon already imports from the host process. On Windows the entries are looked up in the
|
|
33
|
+
// executable instead: node.exe and Electron both export Node-API, so the addon imports nothing from
|
|
34
|
+
// either and needs no import library. jsr_* engine extensions stay null on purpose: JSRuntimeApi
|
|
35
|
+
// falls back to its defaults for them.
|
|
36
|
+
struct ProcessResolver : IFuncResolver {
|
|
37
|
+
FuncPtr getFuncPtr(const char *funcName) override {
|
|
38
|
+
#ifdef _WIN32
|
|
39
|
+
return reinterpret_cast<FuncPtr>(GetProcAddress(GetModuleHandleW(nullptr), funcName));
|
|
40
|
+
#else
|
|
41
|
+
#define NODE_API_FUNC(func) \
|
|
42
|
+
if (std::strcmp(funcName, #func) == 0) return reinterpret_cast<FuncPtr>(&func);
|
|
43
|
+
#include <ApiLoaders/NodeApi.inc>
|
|
44
|
+
return nullptr;
|
|
45
|
+
#endif
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// The Node-API calls this file makes itself resolve the same way. They look the function up on each
|
|
50
|
+
// call rather than through a JSRuntimeApi, whose entries resolve against the current runtime's table,
|
|
51
|
+
// and there is none while the module initializes.
|
|
52
|
+
ProcessResolver processResolver;
|
|
53
|
+
#define NODE_API(func) reinterpret_cast<decltype(&::func)>(processResolver.getFuncPtr(#func))
|
|
54
|
+
|
|
55
|
+
struct EnvState {
|
|
56
|
+
napi_env env = nullptr;
|
|
57
|
+
ProcessResolver resolver;
|
|
58
|
+
JSRuntimeApi api{&resolver};
|
|
59
|
+
std::unique_ptr<facebook::jsi::Runtime> runtime;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
std::atomic<napi_env> owner{nullptr};
|
|
63
|
+
|
|
64
|
+
// Native values can outlive the runtime they point into (thread-local ones are destroyed inside
|
|
65
|
+
// exit()); with no current runtime their release becomes a no-op instead of a call into a dead one.
|
|
66
|
+
void forgetRuntime(EnvState *state) {
|
|
67
|
+
if (state == nullptr) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (emscripten::jsRuntime == state->runtime.get()) {
|
|
71
|
+
emscripten::jsRuntime = nullptr;
|
|
72
|
+
}
|
|
73
|
+
napi_env finished = state->env;
|
|
74
|
+
owner.compare_exchange_strong(finished, nullptr);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
std::string readString(napi_env env, napi_value value) {
|
|
78
|
+
size_t length = 0;
|
|
79
|
+
NODE_API(napi_get_value_string_utf8)(env, value, nullptr, 0, &length);
|
|
80
|
+
std::string result(length, '\0');
|
|
81
|
+
NODE_API(napi_get_value_string_utf8)(env, value, result.data(), length + 1, &length);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// start(dataPath, scope) registers every EMSCRIPTEN_BINDINGS block of the addon into this
|
|
86
|
+
// environment's embind runtime, so embind.js must already be loaded. scope is the object that
|
|
87
|
+
// runtime keeps its state on, which this addon's global() returns. A second call in the same
|
|
88
|
+
// environment is a no-op: registering twice fails with "Cannot register public name ... twice".
|
|
89
|
+
napi_value Start(napi_env env, napi_callback_info info) {
|
|
90
|
+
void *existing = nullptr;
|
|
91
|
+
NODE_API(napi_get_instance_data)(env, &existing);
|
|
92
|
+
if (existing != nullptr) {
|
|
93
|
+
return nullptr;
|
|
94
|
+
}
|
|
95
|
+
// The same environment may retry after a start that failed before it took ownership.
|
|
96
|
+
napi_env current = nullptr;
|
|
97
|
+
if (!owner.compare_exchange_strong(current, env) && current != env) {
|
|
98
|
+
NODE_API(napi_throw_error)(env, nullptr,
|
|
99
|
+
"crossbind: the native addon already runs in another Node environment; "
|
|
100
|
+
"worker_threads are not supported yet");
|
|
101
|
+
return nullptr;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
size_t argc = 2;
|
|
105
|
+
napi_value argv[2];
|
|
106
|
+
NODE_API(napi_get_cb_info)(env, info, &argc, argv, nullptr, nullptr);
|
|
107
|
+
const std::string dataPath = argc > 0 ? readString(env, argv[0]) : std::string();
|
|
108
|
+
napi_valuetype scopeType = napi_undefined;
|
|
109
|
+
if (argc > 1) NODE_API(napi_typeof)(env, argv[1], &scopeType);
|
|
110
|
+
napi_value scope = scopeType == napi_object ? argv[1] : nullptr;
|
|
111
|
+
|
|
112
|
+
// An exception must not unwind through Node-API, which would end the process.
|
|
113
|
+
try {
|
|
114
|
+
auto state = std::make_unique<EnvState>();
|
|
115
|
+
state->env = env;
|
|
116
|
+
JSRuntimeApi::setCurrent(&state->api);
|
|
117
|
+
state->runtime = Microsoft::NodeApiJsi::makeNodeApiJsiRuntime(env, &state->api, [] {}, scope);
|
|
118
|
+
// The environment owns it before any binding registers: registered functions point into it,
|
|
119
|
+
// so it has to outlive a registration that fails halfway.
|
|
120
|
+
EnvState *owned = state.release();
|
|
121
|
+
NODE_API(napi_set_instance_data)(
|
|
122
|
+
env, owned,
|
|
123
|
+
[](napi_env, void *data, void *) {
|
|
124
|
+
auto *finished = static_cast<EnvState *>(data);
|
|
125
|
+
forgetRuntime(finished);
|
|
126
|
+
delete finished;
|
|
127
|
+
},
|
|
128
|
+
nullptr);
|
|
129
|
+
emscripten::internal::_embind_initialize_bindings(*owned->runtime, dataPath);
|
|
130
|
+
} catch (const std::exception &error) {
|
|
131
|
+
NODE_API(napi_throw_error)(env, nullptr, error.what());
|
|
132
|
+
} catch (...) {
|
|
133
|
+
NODE_API(napi_throw_error)(env, nullptr, "crossbind: the native module failed to start");
|
|
134
|
+
}
|
|
135
|
+
return nullptr;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// stop() runs from the environment's 'exit' event: no JS runs here afterwards, and process.exit()
|
|
139
|
+
// skips the teardown that would otherwise release the instance data.
|
|
140
|
+
napi_value Stop(napi_env env, napi_callback_info) {
|
|
141
|
+
void *data = nullptr;
|
|
142
|
+
NODE_API(napi_get_instance_data)(env, &data);
|
|
143
|
+
forgetRuntime(static_cast<EnvState *>(data));
|
|
144
|
+
return nullptr;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} // namespace
|
|
148
|
+
|
|
149
|
+
NAPI_MODULE_INIT() {
|
|
150
|
+
napi_value start;
|
|
151
|
+
NODE_API(napi_create_function)(env, "start", NAPI_AUTO_LENGTH, Start, nullptr, &start);
|
|
152
|
+
NODE_API(napi_set_named_property)(env, exports, "start", start);
|
|
153
|
+
napi_value stop;
|
|
154
|
+
NODE_API(napi_create_function)(env, "stop", NAPI_AUTO_LENGTH, Stop, nullptr, &stop);
|
|
155
|
+
NODE_API(napi_set_named_property)(env, exports, "stop", stop);
|
|
156
|
+
return exports;
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// A project's own build leaves the addon beside the loader. A package that publishes one addon per
|
|
4
|
+
// platform ships it in <package>-<platform>-<arch>, an optional dependency whose main is the addon.
|
|
5
|
+
export default function addonLocation({ dir, fileName, packageName, exists, resolve }) {
|
|
6
|
+
const beside = path.join(dir, fileName);
|
|
7
|
+
if (exists(beside) || !packageName) return beside;
|
|
8
|
+
try {
|
|
9
|
+
return resolve(packageName);
|
|
10
|
+
} catch {
|
|
11
|
+
return beside;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The platform part of an addon file name. Node reports musl and glibc systems alike as 'linux'; a
|
|
2
|
+
// process linked against glibc names that glibc in its report header, a musl one does not.
|
|
3
|
+
export default function addonPlatform(platform = process.platform, readReport = processReport) {
|
|
4
|
+
if (platform !== 'linux') return platform;
|
|
5
|
+
const header = readReport()?.header;
|
|
6
|
+
return !header || header.glibcVersionRuntime ? 'linux' : 'linuxmusl';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function processReport() {
|
|
10
|
+
if (!process.report) return undefined;
|
|
11
|
+
// Only the header is read; the network section can stall on DNS lookups.
|
|
12
|
+
const { excludeNetwork } = process.report;
|
|
13
|
+
process.report.excludeNetwork = true;
|
|
14
|
+
try {
|
|
15
|
+
return process.report.getReport();
|
|
16
|
+
} finally {
|
|
17
|
+
process.report.excludeNetwork = excludeNetwork;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/js/loader.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Entry of the per-project loader crossbind bundles next to the native addons
|
|
2
|
+
// (dist/<name>.native.cjs). It keeps the wasm build's initNative() contract, so the same app code
|
|
3
|
+
// runs on either build.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import Module, { crossbindScope } from '@crossbind/core-embind-jsi';
|
|
8
|
+
import systemConfig from 'crossbind/systemConfig';
|
|
9
|
+
import addonPlatform from './addonPlatform.js';
|
|
10
|
+
import addonLocation from './addonLocation.js';
|
|
11
|
+
import missingAddonRemedy from './missingAddon.js';
|
|
12
|
+
import stopOnExit from './stopOnExit.js';
|
|
13
|
+
import unpackedPath from './unpackedPath.js';
|
|
14
|
+
|
|
15
|
+
const platform = addonPlatform();
|
|
16
|
+
const fill = (pattern) => pattern.replace('{platform}', platform).replace('{arch}', process.arch);
|
|
17
|
+
const addonPackage = systemConfig.paths.addonPackage ? fill(systemConfig.paths.addonPackage) : null;
|
|
18
|
+
|
|
19
|
+
function addonFile(config) {
|
|
20
|
+
return config.addonPath ?? addonLocation({
|
|
21
|
+
dir: __dirname,
|
|
22
|
+
fileName: fill(systemConfig.paths.addon),
|
|
23
|
+
packageName: addonPackage,
|
|
24
|
+
exists: fs.existsSync,
|
|
25
|
+
resolve: createRequire(__filename).resolve,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadAddon(file) {
|
|
30
|
+
const addon = { exports: {} };
|
|
31
|
+
try {
|
|
32
|
+
process.dlopen(addon, file);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`crossbind: cannot load ${file} - ${fs.existsSync(file) ? error.message : missingAddonRemedy({ platform, arch: process.arch, addonPackage, published: Object.keys(systemConfig.env) })}.`, { cause: error });
|
|
35
|
+
}
|
|
36
|
+
return addon.exports;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setEnv(dataPath, runtimeEnv = {}) {
|
|
40
|
+
const fillDataPath = (value) => String(value).replace('_CROSSBIND_DATA_PATH_', dataPath);
|
|
41
|
+
// Build-time values are keyed by addon: a target-scoped value must not leak from one arch's
|
|
42
|
+
// build into another's. They never replace a variable the process already has; values passed to
|
|
43
|
+
// initNative() do, as in the wasm runtime, even after a package entry booted the addon.
|
|
44
|
+
Object.entries(systemConfig.env[`${platform}-${process.arch}`] ?? {}).forEach(([key, value]) => {
|
|
45
|
+
Module.Crossbind.setEnv(key, fillDataPath(value), false);
|
|
46
|
+
});
|
|
47
|
+
Object.entries(runtimeEnv).forEach(([key, value]) => {
|
|
48
|
+
Module.Crossbind.setEnv(key, fillDataPath(value), true);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let addon = null;
|
|
53
|
+
let addonDataPath = null;
|
|
54
|
+
|
|
55
|
+
function boot(config) {
|
|
56
|
+
// Loaded and started once per process: Node cannot unload an addon, and a second dlopen gets a
|
|
57
|
+
// fresh environment whose start() would register every binding twice.
|
|
58
|
+
if (!addon) {
|
|
59
|
+
addonDataPath = config.dataPath ?? unpackedPath(path.join(__dirname, 'data'));
|
|
60
|
+
const loaded = loadAddon(addonFile(config));
|
|
61
|
+
// The embind runtime this bundle carries keeps its state on crossbindScope (crossbind's utils/scopedEmbind.js).
|
|
62
|
+
loaded.start(addonDataPath, crossbindScope);
|
|
63
|
+
// Native values released after this (thread-local ones die inside exit()) must not call
|
|
64
|
+
// back into the runtime.
|
|
65
|
+
stopOnExit(() => loaded.stop());
|
|
66
|
+
addon = loaded;
|
|
67
|
+
} else if (config.dataPath && config.dataPath !== addonDataPath) {
|
|
68
|
+
throw new Error(`crossbind: the addon started with its data at ${addonDataPath}; a process cannot move it to ${config.dataPath}.`);
|
|
69
|
+
}
|
|
70
|
+
setEnv(addonDataPath, config.env);
|
|
71
|
+
return Module;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let booted = null;
|
|
75
|
+
|
|
76
|
+
export default function initNative(config = {}) {
|
|
77
|
+
if (!booted) {
|
|
78
|
+
try {
|
|
79
|
+
booted = Promise.resolve(boot(config));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Not remembered: a later call retries, for example once the addon is built.
|
|
82
|
+
return Promise.reject(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return booted;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Loading an addon is synchronous, so a package entry boots it on require.
|
|
89
|
+
initNative.sync = function sync(config = {}) {
|
|
90
|
+
return boot(config);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// The wasm runtime's contract: forget the booted module so the next call boots again. Native state
|
|
94
|
+
// cannot be reset (Node cannot unload an addon), so that call resolves the same module.
|
|
95
|
+
initNative.terminate = function terminate() {
|
|
96
|
+
booted = null;
|
|
97
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// What a process without its addon is told: install the addon package npm left out, learn that the package
|
|
2
|
+
// publishes none for this machine, or build the addon an app makes itself. published lists the
|
|
3
|
+
// <platform>-<arch> of every addon the package ships.
|
|
4
|
+
export default function missingAddonRemedy({ platform, arch, addonPackage, published }) {
|
|
5
|
+
const target = `${platform}-${arch}`;
|
|
6
|
+
if (!addonPackage) return `build it with \`crossbind build -p ${platform} -a ${arch}\``;
|
|
7
|
+
if (!published.includes(target)) return `this package publishes no addon for ${target}, only for ${published.join(', ')}`;
|
|
8
|
+
return `install ${addonPackage}, which npm leaves out with --omit=optional; addons exist for ${published.join(', ')}`;
|
|
9
|
+
}
|
package/js/stopOnExit.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Every package bundles a loader of its own, and Node warns at an eleventh 'exit' listener, so the addons share one,
|
|
2
|
+
// found by a registered symbol. Each boot moves it to the end of the list: the stops then run after every handler
|
|
3
|
+
// registered so far, as a listener added at that boot would, since a call into an addon after its stop crashes.
|
|
4
|
+
const ADDON_STOPS = Symbol.for('crossbind.addonStops.v1');
|
|
5
|
+
|
|
6
|
+
export default function stopOnExit(stop, proc = process) {
|
|
7
|
+
if (!proc[ADDON_STOPS]) {
|
|
8
|
+
const stops = [];
|
|
9
|
+
proc[ADDON_STOPS] = { stops, onExit: () => stops.forEach((stopAddon) => stopAddon()) };
|
|
10
|
+
}
|
|
11
|
+
const { stops, onExit } = proc[ADDON_STOPS];
|
|
12
|
+
stops.push(stop);
|
|
13
|
+
proc.removeListener('exit', onExit);
|
|
14
|
+
proc.on('exit', onExit);
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Native code cannot read inside an Electron asar archive, so a packaged app keeps what an addon
|
|
2
|
+
// reads in <archive>.asar.unpacked beside it.
|
|
3
|
+
export default function unpackedPath(file, isElectron = Boolean(process.versions.electron)) {
|
|
4
|
+
return isElectron ? file.replace(/\.asar(?=[\\/]|$)/, '.asar.unpacked') : file;
|
|
5
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"name": "@crossbind/core-embind-napi",
|
|
3
|
+
"version": "2.0.0-beta.63",
|
|
4
|
+
"description": "Node-API host for the crossbind embind runtime: native Node.js addons from the same C++ and Rust bindings.",
|
|
5
|
+
"homepage": "https://github.com/crossbind/crossbind/tree/main/core/embind-napi#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/crossbind/crossbind.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"cpp",
|
|
13
|
+
"js",
|
|
14
|
+
"third_party"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@crossbind/core-embind-jsi": "^2.0.0-beta.63"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"embind",
|
|
21
|
+
"node-api",
|
|
22
|
+
"napi",
|
|
23
|
+
"addon",
|
|
24
|
+
"bind",
|
|
25
|
+
"c++",
|
|
26
|
+
"rust",
|
|
27
|
+
"node",
|
|
28
|
+
"electron"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# node-api-jsi (vendored)
|
|
2
|
+
|
|
3
|
+
A JSI (`facebook::jsi`) implementation on top of Node-API. It lets the embind runtime in
|
|
4
|
+
`@crossbind/core-embind-jsi` run inside a Node.js addon unchanged.
|
|
5
|
+
|
|
6
|
+
- Upstream: https://github.com/microsoft/node-api-jsi
|
|
7
|
+
- Commit: `4b8a4a59c4e1b7b988b809dedffd89a7662aa785` (upstream publishes no releases)
|
|
8
|
+
- Archive: `https://codeload.github.com/microsoft/node-api-jsi/tar.gz/4b8a4a5`,
|
|
9
|
+
sha256 `701dfaac5b281b9b67986611ba612d6fe253c434f65459c877b24debf41d3c43`
|
|
10
|
+
- License: MIT, see `LICENSE`
|
|
11
|
+
|
|
12
|
+
Only the files the addon compiles are kept: `src/NodeApiJsiRuntime.*`, the `src/ApiLoaders`
|
|
13
|
+
Node-API and JSRuntime loaders, `jsi/jsi/{jsi.h,jsi-inl.h,jsi.cpp,instrumentation.h}` and the
|
|
14
|
+
`node-api` headers. `jsi.h` falls back to `JSI_VERSION` 21 when no compile definition sets it, so
|
|
15
|
+
no `jsi-version.h` is needed.
|
|
16
|
+
|
|
17
|
+
## Local patches
|
|
18
|
+
|
|
19
|
+
- `src/NodeApiJsiRuntime.cpp` includes `<atomic>` and `<utility>`, which GCC and Clang do not
|
|
20
|
+
pull in transitively.
|
|
21
|
+
- `jsiHostFunctionCallback` is declared `NAPI_CDECL` instead of `__cdecl`, so the calling
|
|
22
|
+
convention only applies where Node-API defines one (Windows).
|
|
23
|
+
- `createStringFromAscii`, `createStringFromUtf8`, `createBigIntFromInt64`,
|
|
24
|
+
`createBigIntFromUint64`, `bigintIsInt64`, `bigintIsUint64`, `truncate`, `utf8(const String&)`
|
|
25
|
+
and `call` skip their own `NodeApiScope` while a pointer value scope is open and has taken fewer
|
|
26
|
+
than `MaxUsesPerOpenScope` (64) such calls. Upstream wraps every primitive they return in an
|
|
27
|
+
object behind a `napi_ref` when that scope closes and reads it back on use; a string-returning
|
|
28
|
+
call went from about 370 to 120 ns and a `const char*` argument, seven emval round trips, from
|
|
29
|
+
4.2 to 1.3 µs. The cap keeps a loop inside one call bounded: without it, a million callbacks
|
|
30
|
+
from one call raised peak memory from 96 to 649 MB. `getProperty`, `instanceOf`,
|
|
31
|
+
`isArrayBuffer`, `size` and `data` of an `ArrayBuffer`, and `callAsConstructor` skip it the same
|
|
32
|
+
way: reading a typed array from C++ opened a Node-API handle scope (a heap allocation in Node)
|
|
33
|
+
for each of these, and a Rust `&[u8]` argument went from 1.16 to 0.94 µs.
|
|
34
|
+
- `NodeApiPendingDeletions` keeps an atomic flag, so `deletePointerValues` takes its mutex only
|
|
35
|
+
when something waits for deletion (about 10 to 20 ns off every call).
|
|
36
|
+
- `NodeApiRefCountedPointerValue::decRefCount` marks a value released on the JS thread while it
|
|
37
|
+
is still on the value stack and has no `napi_ref` for the stack to delete, which is all the
|
|
38
|
+
pending deletion would do, and skips the recursive mutex it takes twice. embind reads every
|
|
39
|
+
`this` pointer and string argument through such a clone: a four-double method call went from
|
|
40
|
+
384 to 362 ns and a `std::string` argument from 211 to 179 ns.
|
|
41
|
+
- `JsiValueViewArgs` constructs each argument in place and destroys only those, instead of
|
|
42
|
+
building `MaxStackArgCount` `jsi::Value`s, move-assigning into them and destroying all of them
|
|
43
|
+
out of line on every call: 369 to 347 ns for the same method call.
|
|
44
|
+
- `NodeApiJsiRuntime` answers `castInterface` for `crossbind::IDirectReads`
|
|
45
|
+
(`core/embind-jsi/cpp/src/crossbind/direct_reads.h`). `typedArrayElements` makes one
|
|
46
|
+
`napi_get_typedarray_info` call where reading the view's `buffer`, `byteOffset` and `length`
|
|
47
|
+
through the object API took about 440 ns, most of a Rust `xxh32` call on 32 bytes, which went
|
|
48
|
+
from 591 to 87 ns. `bigIntToUint64` and `bigIntToInt64` read a BigInt argument without cloning
|
|
49
|
+
it into a heap pointer value first; embind-jsi reads `this` and its other 64-bit values through
|
|
50
|
+
them, and a method call such as `vector.size()` went from 61 to 51 ns.
|
|
51
|
+
- `NodeApiRefCountedPointerValue` takes its memory from a per-thread list of up to 64 blocks
|
|
52
|
+
instead of malloc: most live for one host call, a string, BigInt or `this` read or made during
|
|
53
|
+
it. A string return went from 75 to 69 ns and a string argument from 95 to 84 ns; together with
|
|
54
|
+
the direct BigInt read, a Rust `xxh64` call on 32 bytes with a BigInt seed and result went from
|
|
55
|
+
141 to 110 ns. A million callbacks in one call peak at the same 70 MB.
|
|
56
|
+
- `makeNodeApiJsiRuntime` takes an optional object that `global()` returns in place of the
|
|
57
|
+
global object. Each addon's loader bundles its own embind runtime, which keeps its `Module` and
|
|
58
|
+
the functions the addon calls by name on such an object, so addons of several packages run in
|
|
59
|
+
one process. The runtime's own lookups (`Error`, `Proxy`, `Symbol`) still use the global object.
|