@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.
Files changed (32) hide show
  1. package/README.md +45 -3
  2. package/cpp/CMakeLists.txt +76 -0
  3. package/cpp/check-undefined.cmake +38 -0
  4. package/cpp/src/node_api_module.cpp +157 -0
  5. package/js/addonLocation.js +13 -0
  6. package/js/addonPlatform.js +19 -0
  7. package/js/loader.js +97 -0
  8. package/js/missingAddon.js +9 -0
  9. package/js/stopOnExit.js +15 -0
  10. package/js/unpackedPath.js +5 -0
  11. package/package.json +29 -6
  12. package/third_party/node-api-jsi/LICENSE +21 -0
  13. package/third_party/node-api-jsi/README.md +59 -0
  14. package/third_party/node-api-jsi/jsi/jsi/instrumentation.h +150 -0
  15. package/third_party/node-api-jsi/jsi/jsi/jsi-inl.h +425 -0
  16. package/third_party/node-api-jsi/jsi/jsi/jsi.cpp +990 -0
  17. package/third_party/node-api-jsi/jsi/jsi/jsi.h +2031 -0
  18. package/third_party/node-api-jsi/node-api/js_native_api.h +627 -0
  19. package/third_party/node-api-jsi/node-api/js_native_api_types.h +211 -0
  20. package/third_party/node-api-jsi/node-api/js_runtime_api.h +214 -0
  21. package/third_party/node-api-jsi/node-api/node_api.h +270 -0
  22. package/third_party/node-api-jsi/node-api/node_api_types.h +52 -0
  23. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.cpp +79 -0
  24. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.h +51 -0
  25. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.inc +50 -0
  26. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.cpp +41 -0
  27. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.h +127 -0
  28. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.inc +125 -0
  29. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_posix.cpp +16 -0
  30. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_win.cpp +23 -0
  31. package/third_party/node-api-jsi/src/NodeApiJsiRuntime.cpp +3872 -0
  32. 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
- This version, 0.0.0-bootstrap.0, holds no code. npm lets a trusted publisher be configured only for a
4
- package that exists, so it reserves the name for the crossbind release workflow.
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
- Install the real package: `npm install @crossbind/core-embind-napi@beta`. Source: https://github.com/crossbind/crossbind
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
+ }
@@ -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
- "name": "@crossbind/core-embind-napi",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Placeholder that let npm trust the crossbind release workflow; install @crossbind/core-embind-napi@beta.",
5
- "license": "MIT",
6
- "repository": "https://github.com/crossbind/crossbind.git"
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.