@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.
Files changed (27) hide show
  1. package/README.md +41 -3
  2. package/cpp/CMakeLists.txt +69 -0
  3. package/cpp/check-undefined.cmake +18 -0
  4. package/cpp/src/node_api_module.cpp +153 -0
  5. package/js/loader.js +70 -0
  6. package/package.json +29 -6
  7. package/third_party/node-api-jsi/LICENSE +21 -0
  8. package/third_party/node-api-jsi/README.md +55 -0
  9. package/third_party/node-api-jsi/jsi/jsi/instrumentation.h +150 -0
  10. package/third_party/node-api-jsi/jsi/jsi/jsi-inl.h +425 -0
  11. package/third_party/node-api-jsi/jsi/jsi/jsi.cpp +990 -0
  12. package/third_party/node-api-jsi/jsi/jsi/jsi.h +2031 -0
  13. package/third_party/node-api-jsi/node-api/js_native_api.h +627 -0
  14. package/third_party/node-api-jsi/node-api/js_native_api_types.h +211 -0
  15. package/third_party/node-api-jsi/node-api/js_runtime_api.h +214 -0
  16. package/third_party/node-api-jsi/node-api/node_api.h +270 -0
  17. package/third_party/node-api-jsi/node-api/node_api_types.h +52 -0
  18. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.cpp +79 -0
  19. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.h +51 -0
  20. package/third_party/node-api-jsi/src/ApiLoaders/JSRuntimeApi.inc +50 -0
  21. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.cpp +41 -0
  22. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.h +127 -0
  23. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi.inc +125 -0
  24. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_posix.cpp +16 -0
  25. package/third_party/node-api-jsi/src/ApiLoaders/NodeApi_win.cpp +23 -0
  26. package/third_party/node-api-jsi/src/NodeApiJsiRuntime.cpp +3861 -0
  27. 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
- 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?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 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
- "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.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