@gjsify/napi 0.22.0
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 +203 -0
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/types/index.d.ts +25 -0
- package/meson.build +126 -0
- package/package.json +100 -0
- package/prebuilds/linux-x86_64/GjsifyNapi-1.0.gir +19 -0
- package/prebuilds/linux-x86_64/GjsifyNapi-1.0.typelib +0 -0
- package/prebuilds/linux-x86_64/libgjsifynapi.so +0 -0
- package/src/cc/arraybuffer.cc +772 -0
- package/src/cc/async_work.cc +439 -0
- package/src/cc/common.h +519 -0
- package/src/cc/env.cc +262 -0
- package/src/cc/error.cc +300 -0
- package/src/cc/function.cc +358 -0
- package/src/cc/module.cc +625 -0
- package/src/cc/napi.def +22 -0
- package/src/cc/napi.export.map +16 -0
- package/src/cc/napi.exported-symbols.txt +15 -0
- package/src/cc/object.cc +728 -0
- package/src/cc/ref.cc +510 -0
- package/src/cc/stubs.cc +100 -0
- package/src/cc/tsfn.cc +602 -0
- package/src/cc/value.cc +656 -0
- package/src/cc/wrap.cc +321 -0
- package/src/napi-headers/js_native_api.h +639 -0
- package/src/napi-headers/js_native_api_types.h +249 -0
- package/src/napi-headers/node_api.h +270 -0
- package/src/napi-headers/node_api_types.h +63 -0
- package/src/vala/gjs-1.0.vapi +6 -0
- package/src/vala/mozjs-140.vapi +6 -0
- package/src/vala/napi.vala +49 -0
package/src/cc/module.cc
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/napi — bootstrap, addon loader, module registration, teardown seam.
|
|
3
|
+
//
|
|
4
|
+
// §3a bootstrap: gjsify_napi_install() reaches the running JSContext via the
|
|
5
|
+
// public GJS accessors (refs/gjs/gjs/context.h:79,84) and installs ONE
|
|
6
|
+
// JSNative `globalThis.__gjsifyNapiLoadAddon` (non-enumerable, deleted by the
|
|
7
|
+
// L1 after capture). From then on loadAddon(path) is a plain JS→JSNative
|
|
8
|
+
// call: exports is a normal return value, load failures are real JS
|
|
9
|
+
// exceptions. No global side-channel per load; naturally reentrant.
|
|
10
|
+
//
|
|
11
|
+
// §3b symbol binding: the shim self-promotes its own napi_* into the global
|
|
12
|
+
// dynamic-symbol scope (dlopen(self, RTLD_NOLOAD|RTLD_NOW|RTLD_GLOBAL)), the
|
|
13
|
+
// addon is dlopen'd RTLD_NOW|RTLD_LOCAL. A version script exports only
|
|
14
|
+
// napi_*/node_api_*/gjsify_napi_* (src/cc/napi.export.map).
|
|
15
|
+
//
|
|
16
|
+
// §3c registration discovery mirrors refs/bun BunProcess.cpp:391-800 and
|
|
17
|
+
// refs/deno ext/napi/lib.rs:688-907; API-version clamp mirrors
|
|
18
|
+
// refs/node/src/node_api.cc:44-54 (Node.js contributors, MIT).
|
|
19
|
+
//
|
|
20
|
+
// §3d teardown seam: g_object_weak_ref on the GjsContext — GObject weak
|
|
21
|
+
// notifications fire inside the parent-class dispose chain-up
|
|
22
|
+
// (refs/gjs/gjs/context.cpp:393), i.e. BEFORE GJS's final GC (:437) and
|
|
23
|
+
// BEFORE JS_DestroyContext (:471), while the JSContext is fully alive. The
|
|
24
|
+
// P0.0 probe logs exactly that.
|
|
25
|
+
|
|
26
|
+
#include "common.h"
|
|
27
|
+
|
|
28
|
+
// §3b portability: the addon loader is written against POSIX dlfcn
|
|
29
|
+
// (dlopen/dlsym/dladdr). Windows has no dlfcn — the equivalent is
|
|
30
|
+
// LoadLibraryEx/GetProcAddress/GetModuleHandleEx. The thin shims below keep
|
|
31
|
+
// load_addon_native single-source; the ONE semantic that does NOT port is the
|
|
32
|
+
// flat-namespace symbol self-promotion (ensure_napi_symbols_global) — Windows
|
|
33
|
+
// resolves an addon's napi_* imports via node-gyp's delay-load hook against the
|
|
34
|
+
// HOST .exe, not a global scope, so that function branches on _WIN32.
|
|
35
|
+
#ifdef _WIN32
|
|
36
|
+
#include <windows.h>
|
|
37
|
+
#else
|
|
38
|
+
#include <dlfcn.h>
|
|
39
|
+
#endif
|
|
40
|
+
#include <limits.h>
|
|
41
|
+
#include <stdlib.h>
|
|
42
|
+
|
|
43
|
+
#include <cerrno>
|
|
44
|
+
#include <cstdio>
|
|
45
|
+
#include <cstring>
|
|
46
|
+
#include <map>
|
|
47
|
+
|
|
48
|
+
#ifdef _WIN32
|
|
49
|
+
#define GJSIFY_NAPI_PATH_MAX _MAX_PATH
|
|
50
|
+
#else
|
|
51
|
+
#define GJSIFY_NAPI_PATH_MAX PATH_MAX
|
|
52
|
+
#endif
|
|
53
|
+
|
|
54
|
+
namespace {
|
|
55
|
+
|
|
56
|
+
// ---- POSIX dlfcn ⇄ Win32 loader abstraction (§3b portability) ----
|
|
57
|
+
// Uniform names so the loader body stays platform-agnostic; each maps to the
|
|
58
|
+
// native dynamic-loader primitive. Addon handles stay `void*` (HMODULE is a
|
|
59
|
+
// pointer type — reinterpret_cast at the Win32 call sites, no struct churn).
|
|
60
|
+
|
|
61
|
+
#ifdef _WIN32
|
|
62
|
+
|
|
63
|
+
// Win32 has no dlerror(); format the thread's last error into a message.
|
|
64
|
+
std::string shim_dl_error() {
|
|
65
|
+
DWORD err = GetLastError();
|
|
66
|
+
if (err == 0) {
|
|
67
|
+
return "unknown load error";
|
|
68
|
+
}
|
|
69
|
+
char* buf = nullptr;
|
|
70
|
+
DWORD n = FormatMessageA(
|
|
71
|
+
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
|
72
|
+
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
73
|
+
nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
74
|
+
reinterpret_cast<char*>(&buf), 0, nullptr);
|
|
75
|
+
std::string msg = (n != 0 && buf != nullptr)
|
|
76
|
+
? std::string(buf, n)
|
|
77
|
+
: ("Win32 error " + std::to_string(err));
|
|
78
|
+
if (buf != nullptr) {
|
|
79
|
+
LocalFree(buf);
|
|
80
|
+
}
|
|
81
|
+
// Trim the trailing CRLF FormatMessage appends.
|
|
82
|
+
while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) {
|
|
83
|
+
msg.pop_back();
|
|
84
|
+
}
|
|
85
|
+
return msg;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// LOAD_WITH_ALTERED_SEARCH_PATH: resolve the addon's sibling DLLs from its own
|
|
89
|
+
// directory — the RTLD_LOCAL-neighbourhood behaviour an addon expects, the same
|
|
90
|
+
// posture Node's uv_dlopen uses on Windows.
|
|
91
|
+
void* shim_dl_open_addon(const char* path) {
|
|
92
|
+
return reinterpret_cast<void*>(
|
|
93
|
+
LoadLibraryExA(path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH));
|
|
94
|
+
}
|
|
95
|
+
void* shim_dl_symbol(void* handle, const char* name) {
|
|
96
|
+
return reinterpret_cast<void*>(
|
|
97
|
+
GetProcAddress(reinterpret_cast<HMODULE>(handle), name));
|
|
98
|
+
}
|
|
99
|
+
void shim_dl_close(void* handle) {
|
|
100
|
+
FreeLibrary(reinterpret_cast<HMODULE>(handle));
|
|
101
|
+
}
|
|
102
|
+
// _fullpath canonicalizes without requiring existence (LoadLibraryEx fails
|
|
103
|
+
// cleanly for a missing path); realpath's ENOENT check is thus deferred to open.
|
|
104
|
+
bool shim_realpath(const char* in, char* out, size_t out_sz) {
|
|
105
|
+
return _fullpath(out, in, static_cast<int>(out_sz)) != nullptr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#else
|
|
109
|
+
|
|
110
|
+
void* shim_dl_open_addon(const char* path) {
|
|
111
|
+
// RTLD_LOCAL so the addon's own symbols do not pollute the global scope or
|
|
112
|
+
// collide with a second addon; LAZY (not NOW) mirrors Node's DLib default —
|
|
113
|
+
// real addons carry references never called on this host (node-gi's
|
|
114
|
+
// Node-only uv_* main-loop bridge), and eager binding would reject them.
|
|
115
|
+
return dlopen(path, RTLD_LAZY | RTLD_LOCAL);
|
|
116
|
+
}
|
|
117
|
+
void* shim_dl_symbol(void* handle, const char* name) {
|
|
118
|
+
return dlsym(handle, name);
|
|
119
|
+
}
|
|
120
|
+
void shim_dl_close(void* handle) { dlclose(handle); }
|
|
121
|
+
std::string shim_dl_error() {
|
|
122
|
+
const char* err = dlerror();
|
|
123
|
+
return err != nullptr ? std::string(err) : "unknown dlopen error";
|
|
124
|
+
}
|
|
125
|
+
bool shim_realpath(const char* in, char* out, size_t /* out_sz */) {
|
|
126
|
+
return realpath(in, out) != nullptr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#endif
|
|
130
|
+
|
|
131
|
+
} // namespace
|
|
132
|
+
|
|
133
|
+
namespace gjsify_napi {
|
|
134
|
+
|
|
135
|
+
// Process-wide env registry — walked by the weak sweep callback and the
|
|
136
|
+
// idle finalizer drain (declared in common.h).
|
|
137
|
+
std::vector<napi_env>& env_registry() {
|
|
138
|
+
static std::vector<napi_env> envs;
|
|
139
|
+
return envs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
namespace {
|
|
143
|
+
// The single armed idle drain source (§5c finalization_scheduled guard).
|
|
144
|
+
guint finalizer_drain_source_id = 0;
|
|
145
|
+
|
|
146
|
+
gboolean drain_all_envs_idle(gpointer /* user_data */) {
|
|
147
|
+
finalizer_drain_source_id = 0; // allow re-arming from the drain itself
|
|
148
|
+
for (napi_env env : env_registry()) {
|
|
149
|
+
drain_finalizer_queue(env);
|
|
150
|
+
}
|
|
151
|
+
return G_SOURCE_REMOVE;
|
|
152
|
+
}
|
|
153
|
+
} // namespace
|
|
154
|
+
|
|
155
|
+
// Called from enqueue_finalizer — including from inside the GC sweep
|
|
156
|
+
// callback, where g_idle_add is safe (it only locks the GLib main context;
|
|
157
|
+
// no JSAPI runs). The drain executes on the main loop, NEVER inside GC
|
|
158
|
+
// (§5c invariant: user finalizers never run during GC).
|
|
159
|
+
void schedule_finalizer_drain() {
|
|
160
|
+
if (finalizer_drain_source_id == 0) {
|
|
161
|
+
finalizer_drain_source_id = g_idle_add(drain_all_envs_idle, nullptr);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static void cancel_finalizer_drain() {
|
|
166
|
+
if (finalizer_drain_source_id != 0) {
|
|
167
|
+
g_source_remove(finalizer_drain_source_id);
|
|
168
|
+
finalizer_drain_source_id = 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
} // namespace gjsify_napi
|
|
173
|
+
|
|
174
|
+
namespace {
|
|
175
|
+
|
|
176
|
+
bool loader_installed = false;
|
|
177
|
+
|
|
178
|
+
// §3c static-constructor path: our exported napi_module_register may run
|
|
179
|
+
// DURING dlopen (old-style NAPI_MODULE constructors). Modules land here.
|
|
180
|
+
std::vector<napi_module*> pending_modules;
|
|
181
|
+
|
|
182
|
+
// realpath → env; a second loadAddon replays the same exports (Bun's
|
|
183
|
+
// DLHandleMap, BunProcess.cpp:599-676).
|
|
184
|
+
std::map<std::string, napi_env>& env_cache() {
|
|
185
|
+
static std::map<std::string, napi_env> cache;
|
|
186
|
+
return cache;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
std::vector<napi_env>& all_envs() {
|
|
190
|
+
return gjsify_napi::env_registry();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Throw a real JS exception from the loader (load failures are normal JS
|
|
194
|
+
// errors with real messages, §3a).
|
|
195
|
+
bool throw_load_error(JSContext* cx, const std::string& message) {
|
|
196
|
+
JS_ReportErrorUTF8(cx, "%s", message.c_str());
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// §3d env-teardown seam + P0.0 probe. Fires while the JSContext is still
|
|
201
|
+
// alive (before the final GC + JS_DestroyContext) — exactly when Node-style
|
|
202
|
+
// env teardown must run (§5e).
|
|
203
|
+
void on_gjs_context_dispose(gpointer data, GObject* /* where_the_object_was */) {
|
|
204
|
+
auto* cx = static_cast<JSContext*>(data);
|
|
205
|
+
// Probe: JSRuntime-level and context-state calls must still succeed here.
|
|
206
|
+
JSRuntime* rt = JS_GetRuntime(cx);
|
|
207
|
+
const bool pending = JS_IsExceptionPending(cx);
|
|
208
|
+
fprintf(stderr,
|
|
209
|
+
"[gjsify-napi] P0.0 TEARDOWN PROBE: GjsContext weak notify fired; "
|
|
210
|
+
"JS_GetRuntime=%p exception_pending=%d — JSContext alive at the "
|
|
211
|
+
"env-teardown seam\n",
|
|
212
|
+
static_cast<void*>(rt), pending ? 1 : 0);
|
|
213
|
+
size_t count = all_envs().size();
|
|
214
|
+
// Teardown may run finalizers that trigger GC — the weak sweep callback
|
|
215
|
+
// must stay registered until every env released its weak refs.
|
|
216
|
+
for (napi_env env : all_envs()) {
|
|
217
|
+
env->teardown();
|
|
218
|
+
}
|
|
219
|
+
for (napi_env env : all_envs()) {
|
|
220
|
+
delete env; // frees bundles + remaining shells (no JS runs anymore)
|
|
221
|
+
}
|
|
222
|
+
all_envs().clear();
|
|
223
|
+
env_cache().clear();
|
|
224
|
+
// A still-armed idle drain must not fire into freed envs (and after
|
|
225
|
+
// dispose the registry is empty anyway — belt and braces).
|
|
226
|
+
gjsify_napi::cancel_finalizer_drain();
|
|
227
|
+
// Remove the weak sweep callback BEFORE GJS's final full GC +
|
|
228
|
+
// JS_DestroyContext (refs/gjs/gjs/context.cpp:437,471).
|
|
229
|
+
JS_RemoveWeakPointerZonesCallback(cx, gjsify_napi::weak_sweep_callback);
|
|
230
|
+
loader_installed = false;
|
|
231
|
+
fprintf(stderr,
|
|
232
|
+
"[gjsify-napi] P0.0 TEARDOWN PROBE: %zu env(s) torn down before "
|
|
233
|
+
"JS_DestroyContext\n",
|
|
234
|
+
count);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// §3b step 1+3: verify no foreign napi_* resolves globally, then promote our
|
|
238
|
+
// own symbols into the global scope so the addon's undefined napi_* bind to
|
|
239
|
+
// ours. Returns false with a thrown JS exception on failure.
|
|
240
|
+
bool ensure_napi_symbols_global(JSContext* cx) {
|
|
241
|
+
#ifdef _WIN32
|
|
242
|
+
// Windows has NO global flat symbol namespace, so there is nothing to
|
|
243
|
+
// promote: this shim DLL already exports napi_*/node_api_* via
|
|
244
|
+
// __declspec(dllexport) (js_native_api.h's NAPI_EXTERN on _WIN32). The OPEN
|
|
245
|
+
// wall is the other half — an UNMODIFIED node-gyp addon does not bind its
|
|
246
|
+
// napi_* imports to THIS DLL by name. node-gyp links Windows addons against
|
|
247
|
+
// a delay-loaded host import lib + win_delay_load_hook that resolves napi_*
|
|
248
|
+
// from the HOST executable (GetModuleHandle(NULL)) at first call; GJS's
|
|
249
|
+
// gjs.exe does not export the napi ABI, so unmodified-addon symbol binding
|
|
250
|
+
// needs the shim to satisfy that delay-load (a napi-exporting host EXE, or a
|
|
251
|
+
// delay-load-hook cooperation). Tracked in STATUS.md → N-API host
|
|
252
|
+
// cross-platform → Windows. Here we only assert our own exports resolve so
|
|
253
|
+
// the rest of the load path stays intact once that wall is solved.
|
|
254
|
+
HMODULE self = nullptr;
|
|
255
|
+
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
256
|
+
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
257
|
+
reinterpret_cast<LPCWSTR>(&napi_create_reference),
|
|
258
|
+
&self) == 0 ||
|
|
259
|
+
GetProcAddress(self, "napi_create_reference") == nullptr) {
|
|
260
|
+
return throw_load_error(
|
|
261
|
+
cx, "gjsify-napi: the shim DLL does not export the Node-API ABI "
|
|
262
|
+
"(napi_create_reference) — check the __declspec(dllexport) / "
|
|
263
|
+
".def export configuration");
|
|
264
|
+
}
|
|
265
|
+
g_debug("gjsify-napi: shim exports the napi ABI (Windows: addon binding "
|
|
266
|
+
"relies on the delay-load host model — see STATUS.md)");
|
|
267
|
+
return true;
|
|
268
|
+
#else
|
|
269
|
+
void* ours = reinterpret_cast<void*>(&napi_create_reference);
|
|
270
|
+
void* found = dlsym(RTLD_DEFAULT, "napi_create_reference");
|
|
271
|
+
if (found == ours) {
|
|
272
|
+
// Already promoted (or GI loaded us RTLD_GLOBAL in the first place).
|
|
273
|
+
g_debug("gjsify-napi: napi_* already resolve globally; no promotion "
|
|
274
|
+
"needed");
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (found != nullptr) {
|
|
278
|
+
return throw_load_error(
|
|
279
|
+
cx,
|
|
280
|
+
"gjsify-napi: a foreign Node-API implementation already resolves "
|
|
281
|
+
"in this process (napi_create_reference found outside the shim); "
|
|
282
|
+
"refusing to load addons into a mixed-host process");
|
|
283
|
+
}
|
|
284
|
+
Dl_info info;
|
|
285
|
+
if (dladdr(ours, &info) == 0 || info.dli_fname == nullptr) {
|
|
286
|
+
return throw_load_error(
|
|
287
|
+
cx, "gjsify-napi: dladdr failed to locate the shim library");
|
|
288
|
+
}
|
|
289
|
+
// Re-opening an already-loaded DSO with RTLD_GLOBAL promotes its symbols
|
|
290
|
+
// into the global scope regardless of how GI loaded it.
|
|
291
|
+
void* self = dlopen(info.dli_fname, RTLD_NOLOAD | RTLD_NOW | RTLD_GLOBAL);
|
|
292
|
+
if (self == nullptr) {
|
|
293
|
+
const char* err = dlerror();
|
|
294
|
+
return throw_load_error(
|
|
295
|
+
cx, std::string("gjsify-napi: failed to promote shim symbols to "
|
|
296
|
+
"the global scope: ") +
|
|
297
|
+
(err != nullptr ? err : "unknown dlopen error"));
|
|
298
|
+
}
|
|
299
|
+
// Leaked on purpose: the shim must stay resident (refcount +1 is fine).
|
|
300
|
+
void* now = dlsym(RTLD_DEFAULT, "napi_create_reference");
|
|
301
|
+
if (now != ours) {
|
|
302
|
+
return throw_load_error(
|
|
303
|
+
cx, "gjsify-napi: symbol self-promotion did not take effect "
|
|
304
|
+
"(napi_create_reference still not ours)");
|
|
305
|
+
}
|
|
306
|
+
g_debug("gjsify-napi: promoted shim napi_* symbols to the global scope "
|
|
307
|
+
"(GI had loaded the shim RTLD_LOCAL)");
|
|
308
|
+
return true;
|
|
309
|
+
#endif // _WIN32
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// The loadAddon JSNative (§3a): dlopen + three-path registration (§3c),
|
|
313
|
+
// one napi_env per addon, exports as the return value.
|
|
314
|
+
bool load_addon_native(JSContext* cx, unsigned argc, JS::Value* vp) {
|
|
315
|
+
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
|
|
316
|
+
if (args.length() < 1 || !args[0].isString()) {
|
|
317
|
+
return throw_load_error(
|
|
318
|
+
cx, "gjsify-napi: loadAddon(path) expects a string path");
|
|
319
|
+
}
|
|
320
|
+
JS::RootedString path_str(cx, args[0].toString());
|
|
321
|
+
JS::UniqueChars path_utf8 = JS_EncodeStringToUTF8(cx, path_str);
|
|
322
|
+
if (!path_utf8) {
|
|
323
|
+
return false; // OOM, exception already pending
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
char resolved[GJSIFY_NAPI_PATH_MAX];
|
|
327
|
+
if (!shim_realpath(path_utf8.get(), resolved, sizeof(resolved))) {
|
|
328
|
+
return throw_load_error(
|
|
329
|
+
cx, std::string("gjsify-napi: cannot resolve addon path '") +
|
|
330
|
+
path_utf8.get() + "': " + strerror(errno));
|
|
331
|
+
}
|
|
332
|
+
const std::string key(resolved);
|
|
333
|
+
|
|
334
|
+
auto cached = env_cache().find(key);
|
|
335
|
+
if (cached != env_cache().end()) {
|
|
336
|
+
args.rval().setObject(*cached->second->exports);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!ensure_napi_symbols_global(cx)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// §3b step 2: the addon imports napi_*; its own symbols must not pollute
|
|
345
|
+
// the global scope or collide with a second addon → local, neighbourhood
|
|
346
|
+
// search (shim_dl_open_addon: RTLD_LAZY|RTLD_LOCAL on POSIX,
|
|
347
|
+
// LoadLibraryEx + LOAD_WITH_ALTERED_SEARCH_PATH on Windows). LAZY (POSIX)
|
|
348
|
+
// mirrors Node's own addon loader (node::DLib kDefaultFlags = RTLD_LAZY):
|
|
349
|
+
// real addons legitimately carry references to symbols that are never
|
|
350
|
+
// called on this host — node-gi's Node-only libuv main-loop bridge (uv_*)
|
|
351
|
+
// is exactly that — and eager binding would reject them at load. Every
|
|
352
|
+
// napi_*/node_api_* symbol still resolves (the shim defines the full ABI,
|
|
353
|
+
// stubs included), so the fail-loud posture is unchanged.
|
|
354
|
+
const size_t pending_before = pending_modules.size();
|
|
355
|
+
void* handle = shim_dl_open_addon(resolved);
|
|
356
|
+
if (handle == nullptr) {
|
|
357
|
+
return throw_load_error(
|
|
358
|
+
cx, std::string("gjsify-napi: failed to load addon '") + resolved +
|
|
359
|
+
"': " + shim_dl_error());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// §3c path 1: static-constructor registration during dlopen. Validate +
|
|
363
|
+
// move every newly registered module into a FIFO work queue; the queue
|
|
364
|
+
// is re-drained after each init call (re-entrancy loop — a register fn
|
|
365
|
+
// may register more, mirroring Bun's pending-module drain).
|
|
366
|
+
std::vector<napi_module*> static_mods;
|
|
367
|
+
auto drain_pending = [&]() -> const char* {
|
|
368
|
+
while (pending_modules.size() > pending_before) {
|
|
369
|
+
napi_module* mod = pending_modules[pending_before];
|
|
370
|
+
pending_modules.erase(pending_modules.begin() +
|
|
371
|
+
static_cast<long>(pending_before));
|
|
372
|
+
if (mod->nm_version != NAPI_MODULE_VERSION) {
|
|
373
|
+
return "legacy";
|
|
374
|
+
}
|
|
375
|
+
static_mods.push_back(mod);
|
|
376
|
+
}
|
|
377
|
+
return nullptr;
|
|
378
|
+
};
|
|
379
|
+
if (drain_pending() != nullptr) {
|
|
380
|
+
shim_dl_close(handle);
|
|
381
|
+
return throw_load_error(
|
|
382
|
+
cx, std::string("gjsify-napi: '") + resolved +
|
|
383
|
+
"' is a legacy (NAN/V8) addon (nm_version != 1); only "
|
|
384
|
+
"pure Node-API addons are supported");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// §3c path 2: per-addon API version; clamp exactly like
|
|
388
|
+
// node_napi_env__::New (refs/node/src/node_api.cc:44-54).
|
|
389
|
+
int32_t api_version = gjsify_napi::kNapiDefaultModuleApiVersion;
|
|
390
|
+
auto get_api_version = reinterpret_cast<node_api_addon_get_api_version_func>(
|
|
391
|
+
shim_dl_symbol(handle, "node_api_module_get_api_version_v1"));
|
|
392
|
+
if (get_api_version != nullptr) {
|
|
393
|
+
api_version = get_api_version();
|
|
394
|
+
}
|
|
395
|
+
if (api_version < gjsify_napi::kNapiDefaultModuleApiVersion) {
|
|
396
|
+
api_version = gjsify_napi::kNapiDefaultModuleApiVersion;
|
|
397
|
+
}
|
|
398
|
+
if (api_version == NAPI_VERSION_EXPERIMENTAL) {
|
|
399
|
+
shim_dl_close(handle);
|
|
400
|
+
return throw_load_error(
|
|
401
|
+
cx, std::string("gjsify-napi: '") + resolved +
|
|
402
|
+
"' targets the experimental Node-API version, which "
|
|
403
|
+
"@gjsify/napi Phase 0 refuses (in-GC finalizer regime "
|
|
404
|
+
"not implemented)");
|
|
405
|
+
}
|
|
406
|
+
if (api_version > gjsify_napi::kNapiVersionMax) {
|
|
407
|
+
shim_dl_close(handle);
|
|
408
|
+
return throw_load_error(
|
|
409
|
+
cx, std::string("gjsify-napi: '") + resolved +
|
|
410
|
+
"' requires Node-API version " +
|
|
411
|
+
std::to_string(api_version) +
|
|
412
|
+
", but this runtime supports up to " +
|
|
413
|
+
std::to_string(gjsify_napi::kNapiVersionMax));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// §3c path 3: modern symbol-based registration.
|
|
417
|
+
napi_addon_register_func modern_init = nullptr;
|
|
418
|
+
if (static_mods.empty()) {
|
|
419
|
+
modern_init = reinterpret_cast<napi_addon_register_func>(
|
|
420
|
+
shim_dl_symbol(handle, "napi_register_module_v1"));
|
|
421
|
+
if (modern_init == nullptr) {
|
|
422
|
+
shim_dl_close(handle);
|
|
423
|
+
return throw_load_error(
|
|
424
|
+
cx,
|
|
425
|
+
std::string("gjsify-napi: '") + resolved +
|
|
426
|
+
"' is not a Node-API module (no napi_register_module_v1 "
|
|
427
|
+
"export and no self-registration during dlopen)");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// One napi_env per addon (node_api.cc:768-770).
|
|
432
|
+
GjsContext* gjs = gjs_context_get_current();
|
|
433
|
+
napi_env env = new napi_env__(cx, gjs, api_version, handle,
|
|
434
|
+
std::string("file://") + resolved);
|
|
435
|
+
all_envs().push_back(env);
|
|
436
|
+
|
|
437
|
+
JS::RootedObject exports_obj(cx, JS_NewPlainObject(cx));
|
|
438
|
+
if (!exports_obj) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Call init(s) under call-into-module discipline: fresh handle scope; a
|
|
443
|
+
// pending exception afterwards propagates as the JS throw it is (§6).
|
|
444
|
+
// Each init receives the current exports; a non-null return replaces it
|
|
445
|
+
// (node_api.cc:777-784). Static modules run in registration order,
|
|
446
|
+
// re-draining after each call (an init may register more).
|
|
447
|
+
napi_handle_scope scope = nullptr;
|
|
448
|
+
napi_open_handle_scope(env, &scope);
|
|
449
|
+
napi_value exports_v =
|
|
450
|
+
gjsify_napi::arena_push(env, JS::ObjectValue(*exports_obj));
|
|
451
|
+
bool pending = false;
|
|
452
|
+
bool legacy_reentry = false;
|
|
453
|
+
if (modern_init != nullptr) {
|
|
454
|
+
napi_value init_ret = modern_init(env, exports_v);
|
|
455
|
+
pending = JS_IsExceptionPending(cx);
|
|
456
|
+
if (!pending && init_ret != nullptr) {
|
|
457
|
+
exports_v = init_ret;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
for (size_t i = 0; i < static_mods.size() && !pending; i++) {
|
|
461
|
+
napi_value init_ret =
|
|
462
|
+
static_mods[i]->nm_register_func(env, exports_v);
|
|
463
|
+
pending = JS_IsExceptionPending(cx);
|
|
464
|
+
if (!pending && init_ret != nullptr) {
|
|
465
|
+
exports_v = init_ret;
|
|
466
|
+
}
|
|
467
|
+
if (drain_pending() != nullptr) {
|
|
468
|
+
legacy_reentry = true;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
JS::RootedValue result_val(cx, gjsify_napi::napi_value_to_js(exports_v));
|
|
474
|
+
napi_close_handle_scope(env, scope);
|
|
475
|
+
if (pending) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
if (legacy_reentry) {
|
|
479
|
+
return throw_load_error(
|
|
480
|
+
cx, std::string("gjsify-napi: '") + resolved +
|
|
481
|
+
"' registered a legacy (nm_version != 1) module during "
|
|
482
|
+
"init; only pure Node-API addons are supported");
|
|
483
|
+
}
|
|
484
|
+
if (!result_val.isObject()) {
|
|
485
|
+
return throw_load_error(
|
|
486
|
+
cx, std::string("gjsify-napi: module init of '") + resolved +
|
|
487
|
+
"' returned a non-object exports value");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
env->exports = &result_val.toObject();
|
|
491
|
+
env_cache()[key] = env;
|
|
492
|
+
args.rval().set(result_val);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
} // namespace
|
|
497
|
+
|
|
498
|
+
// §3c path 1 entry — exported; may be called by addon static constructors
|
|
499
|
+
// during dlopen (node_api.h:36-45).
|
|
500
|
+
void NAPI_CDECL napi_module_register(napi_module* mod) {
|
|
501
|
+
pending_modules.push_back(mod);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// The file:// URL captured at load (node_api.cc:744-766). Pointer valid for
|
|
505
|
+
// the env's lifetime.
|
|
506
|
+
napi_status NAPI_CDECL node_api_get_module_file_name(
|
|
507
|
+
node_api_basic_env basic_env, const char** result) {
|
|
508
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
509
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
510
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
511
|
+
*result = env->filename.c_str();
|
|
512
|
+
return gjsify_napi::clear_last_error(env);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
napi_status NAPI_CDECL napi_get_version(node_api_basic_env basic_env,
|
|
516
|
+
uint32_t* result) {
|
|
517
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
518
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
519
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
520
|
+
*result = gjsify_napi::kNapiVersionMax;
|
|
521
|
+
return gjsify_napi::clear_last_error(env);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// node_api.cc's napi_get_node_version returns a pointer to a static
|
|
525
|
+
// napi_node_version filled from the NODE_*_VERSION macros. We report the Node
|
|
526
|
+
// release line the shim targets (node 24, the tested runtime; see AGENTS.md
|
|
527
|
+
// "node v24"). The struct is static/process-lifetime — the contract is that
|
|
528
|
+
// the returned pointer stays valid (node_api.cc:731-742).
|
|
529
|
+
napi_status NAPI_CDECL napi_get_node_version(node_api_basic_env basic_env,
|
|
530
|
+
const napi_node_version** result) {
|
|
531
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
532
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
533
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
534
|
+
static const napi_node_version version = {24, 0, 0, "node"};
|
|
535
|
+
*result = &version;
|
|
536
|
+
return gjsify_napi::clear_last_error(env);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
NAPI_NO_RETURN void NAPI_CDECL napi_fatal_error(const char* location,
|
|
540
|
+
size_t location_len,
|
|
541
|
+
const char* message,
|
|
542
|
+
size_t message_len) {
|
|
543
|
+
// node_api.cc OnFatalError shape: `location: message` to stderr + abort.
|
|
544
|
+
fputs("FATAL ERROR: ", stderr);
|
|
545
|
+
if (location != nullptr) {
|
|
546
|
+
if (location_len == NAPI_AUTO_LENGTH) {
|
|
547
|
+
fputs(location, stderr);
|
|
548
|
+
} else {
|
|
549
|
+
fwrite(location, 1, location_len, stderr);
|
|
550
|
+
}
|
|
551
|
+
fputs(" ", stderr);
|
|
552
|
+
}
|
|
553
|
+
if (message != nullptr) {
|
|
554
|
+
if (message_len == NAPI_AUTO_LENGTH) {
|
|
555
|
+
fputs(message, stderr);
|
|
556
|
+
} else {
|
|
557
|
+
fwrite(message, 1, message_len, stderr);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
fputs("\n", stderr);
|
|
561
|
+
fflush(stderr);
|
|
562
|
+
abort();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---- the GI bootstrap surface (§3a) ----
|
|
566
|
+
|
|
567
|
+
extern "C" gboolean gjsify_napi_install(void) {
|
|
568
|
+
GjsContext* gjs = gjs_context_get_current();
|
|
569
|
+
if (gjs == nullptr) {
|
|
570
|
+
g_warning("gjsify-napi: no current GjsContext — init() must be "
|
|
571
|
+
"called from JS running under GJS");
|
|
572
|
+
return FALSE;
|
|
573
|
+
}
|
|
574
|
+
auto* cx = static_cast<JSContext*>(gjs_context_get_native_context(gjs));
|
|
575
|
+
if (cx == nullptr) {
|
|
576
|
+
g_warning("gjsify-napi: GjsContext has no native JSContext");
|
|
577
|
+
return FALSE;
|
|
578
|
+
}
|
|
579
|
+
JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
|
|
580
|
+
if (!global) {
|
|
581
|
+
g_warning("gjsify-napi: no current JS global (not inside a realm)");
|
|
582
|
+
return FALSE;
|
|
583
|
+
}
|
|
584
|
+
// (Re-)define the bootstrap property when absent: consumers capture the
|
|
585
|
+
// native and delete the property (§3a), and init() is documented
|
|
586
|
+
// idempotent — a SECOND consumer (e.g. node-gi's gjs host mode after the
|
|
587
|
+
// L1 already captured) must be able to re-obtain the loader.
|
|
588
|
+
bool has_loader = false;
|
|
589
|
+
if (!JS_HasProperty(cx, global, "__gjsifyNapiLoadAddon", &has_loader)) {
|
|
590
|
+
g_warning("gjsify-napi: failed to probe __gjsifyNapiLoadAddon");
|
|
591
|
+
return FALSE;
|
|
592
|
+
}
|
|
593
|
+
if (!has_loader) {
|
|
594
|
+
JSFunction* fn = js::NewFunctionWithReserved(
|
|
595
|
+
cx, load_addon_native, 1, 0, "__gjsifyNapiLoadAddon");
|
|
596
|
+
if (fn == nullptr) {
|
|
597
|
+
g_warning("gjsify-napi: failed to create the loadAddon native");
|
|
598
|
+
return FALSE;
|
|
599
|
+
}
|
|
600
|
+
JS::RootedObject fn_obj(cx, JS_GetFunctionObject(fn));
|
|
601
|
+
// attrs 0 → non-enumerable, writable, configurable — consumers delete
|
|
602
|
+
// the property after capturing the function.
|
|
603
|
+
if (!JS_DefineProperty(cx, global, "__gjsifyNapiLoadAddon", fn_obj,
|
|
604
|
+
0)) {
|
|
605
|
+
g_warning("gjsify-napi: failed to define __gjsifyNapiLoadAddon");
|
|
606
|
+
return FALSE;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (loader_installed) {
|
|
610
|
+
return TRUE;
|
|
611
|
+
}
|
|
612
|
+
// §5b weak machinery: ONE process-wide sweep callback (GJS registers its
|
|
613
|
+
// own compartment variant the same way, gi/object.cpp:2503). Removed in
|
|
614
|
+
// on_gjs_context_dispose before the final GC.
|
|
615
|
+
if (!JS_AddWeakPointerZonesCallback(cx, gjsify_napi::weak_sweep_callback,
|
|
616
|
+
nullptr)) {
|
|
617
|
+
g_warning("gjsify-napi: failed to register the weak sweep callback");
|
|
618
|
+
return FALSE;
|
|
619
|
+
}
|
|
620
|
+
// §3d teardown seam: weak notification fires pre-final-GC,
|
|
621
|
+
// pre-JS_DestroyContext (probe in on_gjs_context_dispose).
|
|
622
|
+
g_object_weak_ref(G_OBJECT(gjs), on_gjs_context_dispose, cx);
|
|
623
|
+
loader_installed = true;
|
|
624
|
+
return TRUE;
|
|
625
|
+
}
|
package/src/cc/napi.def
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
; @gjsify/napi — export hygiene, Windows (MSVC / clang-cl link.exe) counterpart
|
|
3
|
+
; of napi.export.map (GNU version-script) and napi.exported-symbols.txt (ld64).
|
|
4
|
+
;
|
|
5
|
+
; Unlike the POSIX linkers, MSVC link.exe / lld-link take a .def EXPORTS list
|
|
6
|
+
; and support NO glob patterns, so the napi_*/node_api_* ABI families cannot be
|
|
7
|
+
; wildcarded here. They do not NEED to be: the vendored headers already mark
|
|
8
|
+
; every ABI entry point __declspec(dllexport) on _WIN32 (js_native_api.h's
|
|
9
|
+
; NAPI_EXTERN), so link.exe exports them automatically, and MSVC hides every
|
|
10
|
+
; other (non-dllexport) symbol by DEFAULT — the mozjs-140 / gjs internals the
|
|
11
|
+
; shim statically references are never re-exported. That already reproduces the
|
|
12
|
+
; version-script's "napi_*/node_api_* visible, everything else local" contract.
|
|
13
|
+
;
|
|
14
|
+
; What the header macro does NOT cover is the Vala-generated GI surface: valac
|
|
15
|
+
; emits plain C functions with no dllexport, yet g_module_open + g_module_symbol
|
|
16
|
+
; (GetProcAddress) must resolve them from the typelib at import. This .def adds
|
|
17
|
+
; exactly those three — the whole introspectable surface is GjsifyNapi.init()
|
|
18
|
+
; (see src/vala/napi.vala) plus the extern-C bridge it calls.
|
|
19
|
+
EXPORTS
|
|
20
|
+
gjsify_napi_init
|
|
21
|
+
gjsify_napi_is_available
|
|
22
|
+
gjsify_napi_install
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# @gjsify/napi — export hygiene (§3b step 3).
|
|
3
|
+
#
|
|
4
|
+
# Only the Node-API ABI (napi_*/node_api_*) and the GObject/GI surface
|
|
5
|
+
# (gjsify_napi_*, incl. the Vala-generated type + namespace symbols GI
|
|
6
|
+
# resolves via dlsym) are exported; everything internal stays hidden. This is
|
|
7
|
+
# the counterpart of Node/Bun exporting the ABI from the main executable
|
|
8
|
+
# (Bun: src/symbols.dyn), realized as a shared-library version script.
|
|
9
|
+
{
|
|
10
|
+
global:
|
|
11
|
+
napi_*;
|
|
12
|
+
node_api_*;
|
|
13
|
+
gjsify_napi_*;
|
|
14
|
+
local:
|
|
15
|
+
*;
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# @gjsify/napi — export hygiene, macOS (ld64) counterpart of napi.export.map.
|
|
3
|
+
#
|
|
4
|
+
# ld64's -exported_symbols_list keeps ONLY the listed symbols visible; the C
|
|
5
|
+
# ABI (napi_*/node_api_*) + the GObject/GI surface (gjsify_napi_*, incl. the
|
|
6
|
+
# Vala-generated type + namespace symbols GI resolves via dlsym) stay exported,
|
|
7
|
+
# everything else is hidden. Mach-O C symbols carry a leading underscore, and
|
|
8
|
+
# ld64 honours `*` wildcards here — so these three globs mirror the `global:`
|
|
9
|
+
# block of the GNU version-script exactly. This is what lets the shim
|
|
10
|
+
# self-promote its napi_* into the flat namespace (module.cc
|
|
11
|
+
# ensure_napi_symbols_global) so an addon built with `-undefined
|
|
12
|
+
# dynamic_lookup` binds its undefined napi_* to ours.
|
|
13
|
+
_napi_*
|
|
14
|
+
_node_api_*
|
|
15
|
+
_gjsify_napi_*
|