@crossbind/core-embind-napi 0.0.0-bootstrap.0 → 2.0.0-beta.62
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 +41 -3
- package/cpp/CMakeLists.txt +69 -0
- package/cpp/check-undefined.cmake +18 -0
- package/cpp/src/node_api_module.cpp +153 -0
- package/js/loader.js +70 -0
- package/package.json +29 -6
- package/third_party/node-api-jsi/LICENSE +21 -0
- package/third_party/node-api-jsi/README.md +55 -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 +3861 -0
- package/third_party/node-api-jsi/src/NodeApiJsiRuntime.h +38 -0
package/README.md
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
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?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 native platform:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D crossbind @crossbind/core-embind-napi
|
|
19
|
+
pnpm crossbind build -p darwin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`dist/` then holds one addon per platform and architecture (`<name>.darwin-arm64.node`,
|
|
23
|
+
`<name>.darwin-x64.node`) and the loader that picks one at runtime:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const initNative = require('./dist/<name>.native.cjs');
|
|
27
|
+
|
|
28
|
+
const { Native } = await initNative();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
crossbind resolves this package from your project; you never import it yourself. See the
|
|
32
|
+
[Node.js integration guide](https://github.com/crossbind/crossbind/blob/main/docs/playbooks/integration/nodejs.md#native-addon-node-api) for
|
|
33
|
+
requirements and current limits (macOS only, no `worker_threads` yet).
|
|
34
|
+
|
|
35
|
+
## Contents
|
|
36
|
+
|
|
37
|
+
- `cpp/` — the addon's CMake project and its Node-API entry point.
|
|
38
|
+
- `js/loader.js` — the loader crossbind bundles into `dist/<name>.native.cjs`.
|
|
39
|
+
- `third_party/node-api-jsi/` — Microsoft's JSI implementation over Node-API (MIT), vendored at a
|
|
40
|
+
pinned commit; provenance and local patches are in its README.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
add_custom_command(TARGET addon POST_BUILD
|
|
49
|
+
COMMAND "${CMAKE_COMMAND}" "-DNM=${CMAKE_NM}" "-DADDON=$<TARGET_FILE:addon>"
|
|
50
|
+
-P "${CMAKE_CURRENT_SOURCE_DIR}/check-undefined.cmake"
|
|
51
|
+
VERBATIM)
|
|
52
|
+
endif()
|
|
53
|
+
|
|
54
|
+
# The C++ runtime, its unwinder and winpthreads go into the addon; the CRT stays the system's UCRT.
|
|
55
|
+
# node-api-jsi's pooled values call the sized operator delete, which clang leaves off for MinGW.
|
|
56
|
+
if(MINGW)
|
|
57
|
+
target_compile_options(addon PRIVATE -fsized-deallocation)
|
|
58
|
+
target_link_options(addon PRIVATE -static)
|
|
59
|
+
endif()
|
|
60
|
+
|
|
61
|
+
# Node-API symbols resolve against the node (or Electron) process that loads the addon. Everything
|
|
62
|
+
# else must resolve at link time, so a stale or missing archive fails the build instead of the load.
|
|
63
|
+
if(APPLE)
|
|
64
|
+
file(STRINGS "${NODE_API_JSI}/src/ApiLoaders/NodeApi.inc" NODE_API_FUNCS REGEX "^NODE_API_FUNC\\(")
|
|
65
|
+
foreach(line IN LISTS NODE_API_FUNCS)
|
|
66
|
+
string(REGEX REPLACE "^NODE_API_FUNC\\(([A-Za-z0-9_]+)\\).*" "\\1" func "${line}")
|
|
67
|
+
target_link_options(addon PRIVATE "LINKER:-U,_${func}")
|
|
68
|
+
endforeach()
|
|
69
|
+
endif()
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
execute_process(
|
|
5
|
+
COMMAND "${NM}" -D --undefined-only "${ADDON}"
|
|
6
|
+
OUTPUT_VARIABLE listing
|
|
7
|
+
COMMAND_ERROR_IS_FATAL ANY)
|
|
8
|
+
string(REPLACE "\n" ";" lines "${listing}")
|
|
9
|
+
set(unresolved)
|
|
10
|
+
foreach(line IN LISTS lines)
|
|
11
|
+
if(line MATCHES "^ *U ([^@ ]+)$" AND NOT CMAKE_MATCH_1 MATCHES "^(napi|node_api)_")
|
|
12
|
+
list(APPEND unresolved "${CMAKE_MATCH_1}")
|
|
13
|
+
endif()
|
|
14
|
+
endforeach()
|
|
15
|
+
if(unresolved)
|
|
16
|
+
list(JOIN unresolved ", " names)
|
|
17
|
+
message(FATAL_ERROR "crossbind: ${ADDON} leaves symbols no archive defines: ${names}")
|
|
18
|
+
endif()
|
|
@@ -0,0 +1,153 @@
|
|
|
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) registers every EMSCRIPTEN_BINDINGS block of the addon into this environment's
|
|
86
|
+
// embind runtime, so embind.js must already be loaded. A second call in the same environment is a
|
|
87
|
+
// no-op: registering twice fails with "Cannot register public name ... twice".
|
|
88
|
+
napi_value Start(napi_env env, napi_callback_info info) {
|
|
89
|
+
void *existing = nullptr;
|
|
90
|
+
NODE_API(napi_get_instance_data)(env, &existing);
|
|
91
|
+
if (existing != nullptr) {
|
|
92
|
+
return nullptr;
|
|
93
|
+
}
|
|
94
|
+
// The same environment may retry after a start that failed before it took ownership.
|
|
95
|
+
napi_env current = nullptr;
|
|
96
|
+
if (!owner.compare_exchange_strong(current, env) && current != env) {
|
|
97
|
+
NODE_API(napi_throw_error)(env, nullptr,
|
|
98
|
+
"crossbind: the native addon already runs in another Node environment; "
|
|
99
|
+
"worker_threads are not supported yet");
|
|
100
|
+
return nullptr;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
size_t argc = 1;
|
|
104
|
+
napi_value argv[1];
|
|
105
|
+
NODE_API(napi_get_cb_info)(env, info, &argc, argv, nullptr, nullptr);
|
|
106
|
+
const std::string dataPath = argc > 0 ? readString(env, argv[0]) : std::string();
|
|
107
|
+
|
|
108
|
+
// An exception must not unwind through Node-API, which would end the process.
|
|
109
|
+
try {
|
|
110
|
+
auto state = std::make_unique<EnvState>();
|
|
111
|
+
state->env = env;
|
|
112
|
+
JSRuntimeApi::setCurrent(&state->api);
|
|
113
|
+
state->runtime = Microsoft::NodeApiJsi::makeNodeApiJsiRuntime(env, &state->api, [] {});
|
|
114
|
+
// The environment owns it before any binding registers: registered functions point into it,
|
|
115
|
+
// so it has to outlive a registration that fails halfway.
|
|
116
|
+
EnvState *owned = state.release();
|
|
117
|
+
NODE_API(napi_set_instance_data)(
|
|
118
|
+
env, owned,
|
|
119
|
+
[](napi_env, void *data, void *) {
|
|
120
|
+
auto *finished = static_cast<EnvState *>(data);
|
|
121
|
+
forgetRuntime(finished);
|
|
122
|
+
delete finished;
|
|
123
|
+
},
|
|
124
|
+
nullptr);
|
|
125
|
+
emscripten::internal::_embind_initialize_bindings(*owned->runtime, dataPath);
|
|
126
|
+
} catch (const std::exception &error) {
|
|
127
|
+
NODE_API(napi_throw_error)(env, nullptr, error.what());
|
|
128
|
+
} catch (...) {
|
|
129
|
+
NODE_API(napi_throw_error)(env, nullptr, "crossbind: the native module failed to start");
|
|
130
|
+
}
|
|
131
|
+
return nullptr;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// stop() runs from the environment's 'exit' event: no JS runs here afterwards, and process.exit()
|
|
135
|
+
// skips the teardown that would otherwise release the instance data.
|
|
136
|
+
napi_value Stop(napi_env env, napi_callback_info) {
|
|
137
|
+
void *data = nullptr;
|
|
138
|
+
NODE_API(napi_get_instance_data)(env, &data);
|
|
139
|
+
forgetRuntime(static_cast<EnvState *>(data));
|
|
140
|
+
return nullptr;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
} // namespace
|
|
144
|
+
|
|
145
|
+
NAPI_MODULE_INIT() {
|
|
146
|
+
napi_value start;
|
|
147
|
+
NODE_API(napi_create_function)(env, "start", NAPI_AUTO_LENGTH, Start, nullptr, &start);
|
|
148
|
+
NODE_API(napi_set_named_property)(env, exports, "start", start);
|
|
149
|
+
napi_value stop;
|
|
150
|
+
NODE_API(napi_create_function)(env, "stop", NAPI_AUTO_LENGTH, Stop, nullptr, &stop);
|
|
151
|
+
NODE_API(napi_set_named_property)(env, exports, "stop", stop);
|
|
152
|
+
return exports;
|
|
153
|
+
}
|
package/js/loader.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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 path from 'node:path';
|
|
5
|
+
import Module from '@crossbind/core-embind-jsi';
|
|
6
|
+
import systemConfig from 'crossbind/systemConfig';
|
|
7
|
+
|
|
8
|
+
function addonFileName() {
|
|
9
|
+
return systemConfig.paths.addon.replace('{platform}', process.platform).replace('{arch}', process.arch);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadAddon(file) {
|
|
13
|
+
const addon = { exports: {} };
|
|
14
|
+
try {
|
|
15
|
+
process.dlopen(addon, file);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`crossbind: cannot load ${file} - build it with \`crossbind build -p ${process.platform} -a ${process.arch}\`.`,
|
|
19
|
+
{ cause: error },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return addon.exports;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setEnv(dataPath, runtimeEnv) {
|
|
26
|
+
// Build-time values are keyed by addon: a target-scoped value must not leak from one arch's
|
|
27
|
+
// build into another's. Values passed to initNative() win, as in the wasm runtime.
|
|
28
|
+
const env = { ...systemConfig.env[`${process.platform}-${process.arch}`], ...runtimeEnv };
|
|
29
|
+
Object.entries(env).forEach(([key, value]) => {
|
|
30
|
+
Module.Crossbind.setEnv(key, String(value).replace('_CROSSBIND_DATA_PATH_', dataPath), false);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let addon = null;
|
|
35
|
+
|
|
36
|
+
function boot(config) {
|
|
37
|
+
const dataPath = config.dataPath ?? path.join(__dirname, 'data');
|
|
38
|
+
// Loaded and started once per process: Node cannot unload an addon, and a second dlopen gets a
|
|
39
|
+
// fresh environment whose start() would register every binding twice.
|
|
40
|
+
if (!addon) {
|
|
41
|
+
const loaded = loadAddon(config.addonPath ?? path.join(__dirname, addonFileName()));
|
|
42
|
+
loaded.start(dataPath);
|
|
43
|
+
// Native values released after this (thread-local ones die inside exit()) must not call
|
|
44
|
+
// back into the runtime.
|
|
45
|
+
process.once('exit', () => loaded.stop());
|
|
46
|
+
addon = loaded;
|
|
47
|
+
}
|
|
48
|
+
setEnv(dataPath, config.env);
|
|
49
|
+
return Module;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let booted = null;
|
|
53
|
+
|
|
54
|
+
export default function initNative(config = {}) {
|
|
55
|
+
if (!booted) {
|
|
56
|
+
try {
|
|
57
|
+
booted = Promise.resolve(boot(config));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Not remembered: a later call retries, for example once the addon is built.
|
|
60
|
+
return Promise.reject(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return booted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The wasm runtime's contract: forget the booted module so the next call boots again. Native state
|
|
67
|
+
// cannot be reset (Node cannot unload an addon), so that call resolves the same module.
|
|
68
|
+
initNative.terminate = function terminate() {
|
|
69
|
+
booted = null;
|
|
70
|
+
};
|
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.62",
|
|
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.62"
|
|
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,55 @@
|
|
|
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.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <chrono>
|
|
11
|
+
#include <iosfwd>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <tuple>
|
|
14
|
+
#include <unordered_map>
|
|
15
|
+
|
|
16
|
+
#include <jsi/jsi.h>
|
|
17
|
+
|
|
18
|
+
namespace facebook {
|
|
19
|
+
namespace jsi {
|
|
20
|
+
|
|
21
|
+
/// Methods for starting and collecting instrumentation, an \c Instrumentation
|
|
22
|
+
/// instance is associated with a particular \c Runtime instance, which it
|
|
23
|
+
/// controls the instrumentation of.
|
|
24
|
+
/// None of these functions should return newly created jsi values, nor should
|
|
25
|
+
/// it modify the values of any jsi values in the heap (although GCs are fine).
|
|
26
|
+
class JSI_EXPORT Instrumentation {
|
|
27
|
+
public:
|
|
28
|
+
#if JSI_VERSION >= 13
|
|
29
|
+
/// Additional options controlling what to include when capturing a heap
|
|
30
|
+
/// snapshot.
|
|
31
|
+
struct HeapSnapshotOptions {
|
|
32
|
+
bool captureNumericValue{false};
|
|
33
|
+
};
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
virtual ~Instrumentation() = default;
|
|
37
|
+
|
|
38
|
+
/// Returns GC statistics as a JSON-encoded string, with an object containing
|
|
39
|
+
/// "type" and "version" fields outermost. "type" is a string, unique to a
|
|
40
|
+
/// particular implementation of \c jsi::Instrumentation, and "version" is a
|
|
41
|
+
/// number to indicate any revision to that implementation and its output
|
|
42
|
+
/// format.
|
|
43
|
+
///
|
|
44
|
+
/// \pre This call can only be made on the instrumentation instance of a
|
|
45
|
+
/// runtime initialised to collect GC statistics.
|
|
46
|
+
///
|
|
47
|
+
/// \post All cumulative measurements mentioned in the output are accumulated
|
|
48
|
+
/// across the entire lifetime of the Runtime.
|
|
49
|
+
///
|
|
50
|
+
/// \return the GC statistics collected so far, as a JSON-encoded string.
|
|
51
|
+
virtual std::string getRecordedGCStats() = 0;
|
|
52
|
+
|
|
53
|
+
/// Request statistics about the current state of the runtime's heap. This
|
|
54
|
+
/// function can be called at any time, and should produce information that is
|
|
55
|
+
/// correct at the instant it is called (i.e, not stale).
|
|
56
|
+
///
|
|
57
|
+
/// \return a map from a string key to a number associated with that
|
|
58
|
+
/// statistic.
|
|
59
|
+
virtual std::unordered_map<std::string, int64_t> getHeapInfo(
|
|
60
|
+
bool includeExpensive) = 0;
|
|
61
|
+
|
|
62
|
+
/// Perform a full garbage collection.
|
|
63
|
+
/// \param cause The cause of this collection, as it should be reported in
|
|
64
|
+
/// logs.
|
|
65
|
+
virtual void collectGarbage(std::string cause) = 0;
|
|
66
|
+
|
|
67
|
+
/// A HeapStatsUpdate is a tuple of the fragment index, the number of objects
|
|
68
|
+
/// in that fragment, and the number of bytes used by those objects.
|
|
69
|
+
/// A "fragment" is a view of all objects allocated within a time slice.
|
|
70
|
+
using HeapStatsUpdate = std::tuple<uint64_t, uint64_t, uint64_t>;
|
|
71
|
+
|
|
72
|
+
/// Start capturing JS stack-traces for all JS heap allocated objects. These
|
|
73
|
+
/// can be accessed via \c ::createSnapshotToFile().
|
|
74
|
+
/// \param fragmentCallback If present, invoke this callback every so often
|
|
75
|
+
/// with the most recently seen object ID, and a list of fragments that have
|
|
76
|
+
/// been updated. This callback will be invoked on the same thread that the
|
|
77
|
+
/// runtime is using.
|
|
78
|
+
virtual void startTrackingHeapObjectStackTraces(
|
|
79
|
+
std::function<void(
|
|
80
|
+
uint64_t lastSeenObjectID,
|
|
81
|
+
std::chrono::microseconds timestamp,
|
|
82
|
+
std::vector<HeapStatsUpdate> stats)> fragmentCallback) = 0;
|
|
83
|
+
|
|
84
|
+
/// Stop capture JS stack-traces for JS heap allocated objects.
|
|
85
|
+
virtual void stopTrackingHeapObjectStackTraces() = 0;
|
|
86
|
+
|
|
87
|
+
/// Start a heap sampling profiler that will sample heap allocations, and the
|
|
88
|
+
/// stack trace they were allocated at. Reports a summary of which functions
|
|
89
|
+
/// allocated the most.
|
|
90
|
+
/// \param samplingInterval The number of bytes allocated to wait between
|
|
91
|
+
/// samples. This will be used as the expected value of a poisson
|
|
92
|
+
/// distribution.
|
|
93
|
+
virtual void startHeapSampling(size_t samplingInterval) = 0;
|
|
94
|
+
|
|
95
|
+
/// Turns off the heap sampling profiler previously enabled via
|
|
96
|
+
/// \c startHeapSampling. Writes the output of the sampling heap profiler to
|
|
97
|
+
/// \p os. The output is a JSON formatted string.
|
|
98
|
+
virtual void stopHeapSampling(std::ostream& os) = 0;
|
|
99
|
+
|
|
100
|
+
#if JSI_VERSION >= 13
|
|
101
|
+
/// Captures the heap to a file
|
|
102
|
+
///
|
|
103
|
+
/// \param path to save the heap capture.
|
|
104
|
+
/// \param options additional options for what to capture.
|
|
105
|
+
virtual void createSnapshotToFile(
|
|
106
|
+
const std::string& path,
|
|
107
|
+
const HeapSnapshotOptions& options = {false}) = 0;
|
|
108
|
+
#else
|
|
109
|
+
/// Captures the heap to a file
|
|
110
|
+
///
|
|
111
|
+
/// \param path to save the heap capture
|
|
112
|
+
virtual void createSnapshotToFile(const std::string& path) = 0;
|
|
113
|
+
#endif
|
|
114
|
+
|
|
115
|
+
#if JSI_VERSION >= 13
|
|
116
|
+
/// Captures the heap to an output stream
|
|
117
|
+
///
|
|
118
|
+
/// \param os output stream to write to.
|
|
119
|
+
/// \param options additional options for what to capture.
|
|
120
|
+
virtual void createSnapshotToStream(
|
|
121
|
+
std::ostream& os,
|
|
122
|
+
const HeapSnapshotOptions& options = {false}) = 0;
|
|
123
|
+
#else
|
|
124
|
+
/// Captures the heap to an output stream
|
|
125
|
+
///
|
|
126
|
+
/// \param os output stream to write to.
|
|
127
|
+
virtual void createSnapshotToStream(std::ostream& os) = 0;
|
|
128
|
+
#endif
|
|
129
|
+
|
|
130
|
+
/// If the runtime has been created to trace to a temp file, flush
|
|
131
|
+
/// any unwritten parts of the trace of bridge traffic to the file,
|
|
132
|
+
/// and return the name of the file. Otherwise, return the empty string.
|
|
133
|
+
/// Tracing is disabled after this call.
|
|
134
|
+
virtual std::string flushAndDisableBridgeTrafficTrace() = 0;
|
|
135
|
+
|
|
136
|
+
/// Write basic block profile trace to the given file name.
|
|
137
|
+
virtual void writeBasicBlockProfileTraceToFile(
|
|
138
|
+
const std::string& fileName) const = 0;
|
|
139
|
+
|
|
140
|
+
#if JSI_VERSION >= 21
|
|
141
|
+
/// Write the opcode stats to the given stream.
|
|
142
|
+
virtual void dumpOpcodeStats(std::ostream& os) const = 0;
|
|
143
|
+
#endif
|
|
144
|
+
|
|
145
|
+
/// Dump external profiler symbols to the given file name.
|
|
146
|
+
virtual void dumpProfilerSymbolsToFile(const std::string& fileName) const = 0;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
} // namespace jsi
|
|
150
|
+
} // namespace facebook
|