@gjsify/node-gi 0.13.0 → 0.20.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/src/common.h ADDED
@@ -0,0 +1,624 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi — GObject-Introspection runtime for Node.js.
3
+ //
4
+ // Reference: refs/node-gtk (romgrk and node-gtk contributors, MIT). The design
5
+ // of this binding derives from node-gtk; this file is an original N-API
6
+ // implementation retargeted to the modern girepository-2.0 API (the
7
+ // GLib-integrated `gi_*` GIRepository merged into GLib >= 2.80; the standalone
8
+ // libgirepository-1.0 node-gtk linked no longer ships). GJS's gi/repo.cpp is
9
+ // the reference for the girepository-2.0 API surface.
10
+ //
11
+ // Milestone 1 (headless core): the modern GIRepository API end to end — resolve
12
+ // the default repository, require namespaces, marshal functions/methods/props/
13
+ // signals/callbacks/variants/containers, the libuv↔GLib mainloop bridge, and the
14
+ // toggle-ref instance GC bridge (single canonical wrapper per GObject; toggle ref
15
+ // flips strong↔weak so wrappers are collectable yet identity-stable + rooted
16
+ // while C owns them; idle-deferred teardown; resurrection; thread-marshalled
17
+ // toggles). GTK/Adwaita layering lands on top.
18
+
19
+ #ifndef NODE_GI_SRC_COMMON_H_
20
+ #define NODE_GI_SRC_COMMON_H_
21
+
22
+ #ifndef _WIN32
23
+ #include <dlfcn.h> // dlopen/dlsym the GtkWidgetClass template API (no GTK link)
24
+ #endif
25
+ #include <napi.h>
26
+ #include <uv.h>
27
+
28
+ #include <girepository/girepository.h>
29
+ #include <girepository/girffi.h> // gi_callable_info_create_closure + ffi_cif
30
+ #include <glib-object.h>
31
+ #include <glib.h>
32
+
33
+ #include <atomic>
34
+ #include <deque>
35
+ #include <memory>
36
+ #include <mutex>
37
+ #include <string>
38
+ #include <thread>
39
+ #include <vector>
40
+
41
+ #ifdef _WIN32
42
+ // libuv's uv.h transitively includes <windows.h>, whose <winuser.h> defines the
43
+ // object-like macro `RegisterClass` (→ RegisterClassW, the Win32 window-class API).
44
+ // It clobbers our identically-named N-API export `RegisterClass`. We never call the
45
+ // Win32 windowing API, so undefine it (it is the only Win32 UI macro that collides
46
+ // with a node-gi identifier — `RegisterClassFromGType` is a distinct token).
47
+ #undef RegisterClass
48
+ #endif
49
+
50
+ namespace nodegi {
51
+
52
+ // Forward declaration: GetConstantValue (defined early, near the namespace
53
+ // helpers) marshals through GIArgumentToJs, whose definition lives further down
54
+ // with the rest of the value-marshalling boundary.
55
+ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
56
+ GITransfer transfer);
57
+
58
+ GIRepository* DupDefaultRepository();
59
+
60
+ // Per-env state, stored in N-API instance data so each env (incl. a
61
+ // worker_threads worker) holds + derefs its OWN GLib.Error builder ref. A napi_ref
62
+ // is env-specific: keying it per-env (not a file-static slot shared across every
63
+ // env) removes both the cross-env clobber (last loader wins, prior envs lose the
64
+ // builder) AND the cross-env deref (env A reading env B's ref = UB). Created in
65
+ // Init, freed by the instance-data finalizer at env teardown.
66
+ struct NodeGiEnvData {
67
+ napi_ref errorBuilder = nullptr;
68
+ // L1-registered resolver for Gtk.Template `<signal handler="…">` callbacks: given
69
+ // (instanceHandle, handlerName) it returns the instance's bound JS method (or
70
+ // undefined). Stored per-env for the same reason as errorBuilder (a napi_ref is
71
+ // env-specific). See the Gtk.Widget composite-template scope further down.
72
+ napi_ref templateCallbackResolver = nullptr;
73
+ // The cairo L1 module's JS wrapper factories ({context, surface, pattern}), set
74
+ // by the native cairo `setup()` when `import 'cairo'` first loads. The
75
+ // foreign-struct from_func calls them to wrap a returned/callback cairo pointer
76
+ // into the JS Context/Surface(/ImageSurface)/Pattern class instance. Per-env for
77
+ // the same reason as errorBuilder (a napi_ref is env-specific). See cairo.cc.
78
+ napi_ref cairoWrappers = nullptr;
79
+ // The runtime-native microtask drain (Bun: `bun:jsc` drainMicrotasks; Deno:
80
+ // core.runMicrotasks), registered by index.js on non-Node runtimes only — see
81
+ // setMicrotaskDrain / NodeGiMaybeDrainMicrotasks in loop.cc. Never set on Node
82
+ // (napi_make_callback already runs the checkpoint there). Per-env for the same
83
+ // reason as errorBuilder (a napi_ref is env-specific).
84
+ napi_ref microtaskDrain = nullptr;
85
+ };
86
+
87
+ void NodeGiEnvDataFinalize(napi_env env, void* data, void* hint);
88
+ NodeGiEnvData* EnvData(napi_env env);
89
+
90
+ // ---- boxed / struct handles (milestone: mainloop) ----
91
+ //
92
+ // Boxed/struct instances (e.g. GMainLoop) are wrapped as type-tagged Externals
93
+ // over a small heap record carrying the pointer + its boxed GType, so the
94
+ // finalizer can g_boxed_free a fully-owned boxed and method resolution can find
95
+ // the struct's GIStructInfo by GType. Distinct tag from the GObject handle so
96
+ // the two never cross-dereference. Full general struct support (field access,
97
+ // copy semantics for non-registered C structs) lands with the broader
98
+ // structs/boxed drop; this is the slice the GLib main loop needs.
99
+ struct BoxedHandle {
100
+ gpointer ptr;
101
+ GType gtype; // boxed GType, or G_TYPE_INVALID when unknown/non-registered
102
+ bool owns; // g_boxed_free(gtype, ptr) on finalize when true
103
+ // The struct/union GIBaseInfo backing this handle (a held ref, unref'd on
104
+ // finalize), or nullptr. Carried so field + method resolution works for an
105
+ // UNREGISTERED struct whose runtime GType is G_TYPE_NONE (e.g. GIMarshalling
106
+ // SimpleStruct) — find_by_gtype can't recover the info there, so the static
107
+ // info from the wrap site is stored. A registered type may leave this nullptr
108
+ // and be re-resolved via find_by_gtype(gtype).
109
+ GIBaseInfo* info;
110
+ // g_free(ptr) on finalize. For a NON-boxed plain C struct whose storage the
111
+ // engine g_malloc0'd itself (a caller-allocates OUT param, e.g. a filled
112
+ // PangoRectangle): there is no boxed free-func, the handle simply owns the
113
+ // malloc'd block. Mutually exclusive with `owns` (boxed ownership).
114
+ bool rawOwned = false;
115
+ };
116
+
117
+ extern const napi_type_tag kBoxedHandleTag;
118
+ extern const napi_type_tag kGObjectHandleTag;
119
+
120
+ // `info` (optional): the struct/union GIBaseInfo backing the handle. When passed,
121
+ // MakeBoxedHandle takes its OWN ref (the caller keeps its own), unref'd on finalize.
122
+ // `rawOwned` (optional): the handle owns a plain g_malloc'd block — g_free(ptr) on
123
+ // finalize (the non-boxed caller-allocates OUT case; see BoxedHandle::rawOwned).
124
+ Napi::Value MakeBoxedHandle(Napi::Env env, gpointer ptr, GType gtype, bool owns,
125
+ GIBaseInfo* info = nullptr, bool rawOwned = false);
126
+ Napi::Value WrapVariant(Napi::Env env, GVariant* var, GITransfer transfer);
127
+ BoxedHandle* TryGetBoxedHandle(Napi::Value v);
128
+ bool TryGetBoxedPtr(Napi::Value v, gpointer* out);
129
+ Napi::Value MakeGTypeHandle(Napi::Env env, GType gtype);
130
+ GType ReadGTypeHandle(Napi::Value v);
131
+ bool UnwrapGTypeArg(Napi::Env env, Napi::Value v, GType* out);
132
+
133
+ // ---- foreign-struct seam (cairo) --------------------------------------------
134
+ //
135
+ // A "foreign" struct (gi_struct_info_is_foreign) is one whose layout GI does not
136
+ // know — it delegates marshalling to a module. cairo's Context/Surface/Pattern are
137
+ // the canonical case: a GI function taking/returning a cairo_t*/cairo_surface_t*/
138
+ // cairo_pattern_t* (e.g. a Gtk.DrawingArea draw-func's cairo_t) round-trips through
139
+ // the cairo module's converters, NOT the generic boxed path. Mirrors GJS's
140
+ // gi/foreign.cpp seam (gjs_struct_foreign_register / lookup / to/from_gi_argument).
141
+ //
142
+ // A registered module (cairo.cc) supplies `to` (JS wrapper -> GIArgument.v_pointer)
143
+ // and `from` (GIArgument.v_pointer -> a fresh JS wrapper). Ownership follows the
144
+ // node-gi adopt-or-ref model (no separate release call, matching WrapBoxed).
145
+ struct ForeignStructOps {
146
+ bool (*to)(Napi::Env, Napi::Value, GITransfer, GIArgument*);
147
+ Napi::Value (*from)(Napi::Env, gpointer, GITransfer);
148
+ };
149
+ void RegisterForeignStruct(const char* ns, const char* type_name, const ForeignStructOps* ops);
150
+ const ForeignStructOps* LookupForeignStruct(const char* ns, const char* type_name);
151
+ // The foreign ops for a struct/union GIBaseInfo, or null when it is not a
152
+ // registered foreign type — used by the INTERFACE marshalling branches to route a
153
+ // cairo-typed arg/return/callback-arg to the module instead of the boxed path.
154
+ const ForeignStructOps* ForeignOpsForInfo(GIBaseInfo* iface);
155
+
156
+ // cairo.cc — the native cairo binding + foreign-struct registration. Called once
157
+ // from addon.cc's Init; sets the `__cairo` export + registers Context/Surface/
158
+ // Pattern foreign converters.
159
+ void InitCairo(Napi::Env env, Napi::Object exports);
160
+
161
+ // ---- GJS-exact 64-bit integer marshalling helpers (shared) -------------------
162
+ //
163
+ // Single source of truth for BigInt/Number ⇄ 64-bit C int, used by the GI scalar
164
+ // + array-element marshaller (marshal.cc), the GValue property marshaller
165
+ // (object.cc), and the GVariant packer/unpacker (variant.cc).
166
+ //
167
+ // IN (JsValueTo{Int,Uint}64): a BigInt is read LOSSLESSLY — GJS uses
168
+ // JS::ToBigInt64 / JS::ToBigUint64 (refs/gjs/gi/js-value-inl.h:126-146): a BigInt
169
+ // is exact by construction, a wrapping conversion, NOT an error, so `lossless` is
170
+ // intentionally ignored. A plain Number is truncated via node-addon-api's
171
+ // Int64Value (GJS truncates a Number via JS::ToInt64 — also not an error). The
172
+ // BigInt branch is load-bearing: `ToNumber()` on a BigInt sets a pending N-API
173
+ // error under NAPI_DISABLE_CPP_EXCEPTIONS, and the follow-up `Napi::Error::New`
174
+ // would fatally abort the process (exit 134) instead of marshalling — a crash the
175
+ // repo invariant forbids.
176
+
177
+ // ---- terminate-safe coercion helpers ----------------------------------------
178
+ //
179
+ // Under NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS a fallible node-addon-api call on
180
+ // an env that can no longer run JS (worker.terminate() landing while a JS→GI call
181
+ // is inside this addon, or env teardown) returns a DEFAULT-CONSTRUCTED value
182
+ // (`_env == nullptr`) instead of aborting — the throw is swallowed, the value is
183
+ // empty. Chaining the NEXT fallible wrapper call onto that empty value funnels
184
+ // into node-addon-api's Error::New(napi_get_last_error_info) NAPI_FATAL_IF_FAILED
185
+ // sites — which are OUTSIDE the swallow valve — and aborts the process
186
+ // ("FATAL ERROR: Error::New napi_get_last_error_info"). Every coercion chain
187
+ // (`v.ToNumber().Int32Value()`, `v.ToString().Utf8Value()`, …) on a path
188
+ // reachable mid-terminate must go through these helpers: they check the input AND
189
+ // the coercion result and degrade to a zero value instead. On a LIVE env a
190
+ // successful napi call never yields an empty value, so behaviour is unchanged;
191
+ // the guards trip only on the swallowed-failure residue.
192
+ inline bool NodeGiToBool(Napi::Value v) {
193
+ if (v.IsEmpty()) return false;
194
+ Napi::Boolean b = v.ToBoolean();
195
+ return b.IsEmpty() ? false : b.Value();
196
+ }
197
+ inline int32_t NodeGiToInt32(Napi::Value v) {
198
+ if (v.IsEmpty()) return 0;
199
+ Napi::Number n = v.ToNumber();
200
+ return n.IsEmpty() ? 0 : n.Int32Value();
201
+ }
202
+ inline uint32_t NodeGiToUint32(Napi::Value v) {
203
+ if (v.IsEmpty()) return 0;
204
+ Napi::Number n = v.ToNumber();
205
+ return n.IsEmpty() ? 0 : n.Uint32Value();
206
+ }
207
+ inline double NodeGiToDouble(Napi::Value v) {
208
+ if (v.IsEmpty()) return 0;
209
+ Napi::Number n = v.ToNumber();
210
+ return n.IsEmpty() ? 0 : n.DoubleValue();
211
+ }
212
+ inline std::string NodeGiToUtf8(Napi::Value v) {
213
+ if (v.IsEmpty()) return std::string();
214
+ Napi::String s = v.ToString();
215
+ return s.IsEmpty() ? std::string() : s.Utf8Value();
216
+ }
217
+
218
+ inline int64_t JsValueToInt64(Napi::Value v) {
219
+ if (v.IsEmpty()) return 0; // swallowed-failure residue (see helpers above)
220
+ if (v.IsBigInt()) {
221
+ bool lossless = false;
222
+ return v.As<Napi::BigInt>().Int64Value(&lossless);
223
+ }
224
+ Napi::Number n = v.ToNumber();
225
+ return n.IsEmpty() ? 0 : n.Int64Value();
226
+ }
227
+ inline uint64_t JsValueToUint64(Napi::Value v) {
228
+ if (v.IsEmpty()) return 0; // swallowed-failure residue (see helpers above)
229
+ if (v.IsBigInt()) {
230
+ bool lossless = false;
231
+ return v.As<Napi::BigInt>().Uint64Value(&lossless);
232
+ }
233
+ Napi::Number n = v.ToNumber();
234
+ return n.IsEmpty() ? 0 : static_cast<uint64_t>(n.Int64Value());
235
+ }
236
+
237
+ // OUT (WarnIfUnsafe{Int,Uint}64): GJS ALWAYS returns a JS Number for a 64-bit int
238
+ // (never a BigInt) and emits this g_warning when the value falls outside the range
239
+ // a double represents exactly (|v| > 2^53 - 1 == Number.MAX_SAFE_INTEGER), because
240
+ // the returned double may be rounded. Mirrors refs/gjs/gi/arg-inl.h:222-228 +
241
+ // js-value-inl.h:223-236, where max_safe_big_number == (1 << DBL_MANT_DIG) - 1 and
242
+ // DBL_MANT_DIG (std::numeric_limits<double>::digits) == 53. The message text is
243
+ // byte-for-byte GJS's so a warning-capturing consumer sees the identical string.
244
+ constexpr int64_t kMaxSafeJsInteger = (int64_t{1} << 53) - 1; // 9007199254740991
245
+ inline void WarnIfUnsafeInt64(int64_t v) {
246
+ if (v > kMaxSafeJsInteger || v < -kMaxSafeJsInteger)
247
+ g_warning("Value %s cannot be safely stored in a JS Number and may be rounded",
248
+ std::to_string(v).c_str());
249
+ }
250
+ inline void WarnIfUnsafeUint64(uint64_t v) {
251
+ if (v > static_cast<uint64_t>(kMaxSafeJsInteger))
252
+ g_warning("Value %s cannot be safely stored in a JS Number and may be rounded",
253
+ std::to_string(v).c_str());
254
+ }
255
+
256
+ // ---- JS function -> GClosure IN-args ----------------------------------------
257
+ //
258
+ // A JS function passed for a `GObject.Closure`-typed IN parameter (e.g.
259
+ // g_signal_connect_closure, g_source_set_closure, GIMarshallingTests.gclosure_in,
260
+ // g_dbus_connection_register_object_with_closures2) is marshalled as a REAL
261
+ // GClosure whose marshal converts the invocation's GValue params to JS, calls the
262
+ // function, and writes the JS return into the return GValue — mirroring GJS's
263
+ // Gjs::Closure::create_marshaled "boxed" closures (refs/gjs/gi/arg-cache.cpp
264
+ // GClosureInTransferNone::in + gi/closure.cpp). Lifetime follows gjs exactly:
265
+ // the closure is ref'd + sunk at marshal time; a transfer-none arg drops that ref
266
+ // after the invoke (the callee keeps its own if it stored the closure), a
267
+ // transfer-full arg leaves it with the callee. Closures created during an invoke
268
+ // are recorded here so calls.cc can release them on the right path.
269
+ struct CreatedClosures {
270
+ std::vector<GClosure*> transferNone; // unref after the invoke (gjs BoxedInTransferNone::release)
271
+ std::vector<GClosure*> transferFull; // callee adopts; unref only when the callee never ran
272
+ };
273
+
274
+ // ---- JS bytes -> GBytes IN-args ----------------------------------------------
275
+ //
276
+ // A JS Uint8Array (or any TypedArray/DataView/ArrayBuffer) passed for a
277
+ // `GLib.Bytes`-typed IN parameter (e.g. GdkPixbuf.Pixbuf.new_from_bytes,
278
+ // g_compute_checksum_for_bytes) is COPIED into a fresh GBytes — exactly what GJS
279
+ // does (refs/gjs/gi/arg-cache.cpp GBytesIn::in → gjs_byte_array_get_bytes →
280
+ // g_bytes_new). Lifetime follows gjs: a transfer-none arg drops the fresh ref
281
+ // after the invoke (GBytesInTransferNone::release → g_boxed_free; a callee that
282
+ // kept the bytes holds its own ref), a transfer-full arg leaves the ref with the
283
+ // callee (GBytesIn::release skips via the ignore_release mark). GBytes created
284
+ // during an invoke are recorded here so calls.cc can release them on the right
285
+ // path (mirrors CreatedClosures).
286
+ struct CreatedBytes {
287
+ std::vector<GBytes*> transferNone; // unref after the invoke (callee borrowed/ref'd)
288
+ std::vector<GBytes*> transferFull; // callee adopts; unref only when the callee never ran
289
+ };
290
+
291
+ // Build the (floating) marshaled GClosure for a JS function (signals.cc — shares
292
+ // the battle-tested JsClosure marshal/finalize machinery with `.connect()`).
293
+ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
294
+
295
+ // `ownedStrings` (optional): when a transfer-full string IN/INOUT arg is g_strdup'd
296
+ // here, the freshly-allocated pointer is appended so the caller can g_free it if the
297
+ // invoke never adopts it (an arg-marshal error before the call, or a failed invoke).
298
+ // nullptr (the default) → no tracking, for the vfunc-return / signal-arg callers.
299
+ // `closures` (optional): enables the JS-function→GClosure IN-arg marshalling above;
300
+ // nullptr (the default) keeps the previous behaviour (function → TypeError) on
301
+ // paths that cannot release a created closure.
302
+ // `bytes` (optional): enables the JS-bytes→GBytes IN-arg marshalling above;
303
+ // nullptr (the default) keeps the previous behaviour (typed array → TypeError) on
304
+ // paths that cannot release a created GBytes.
305
+ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument* out,
306
+ std::string* heldString,
307
+ GITransfer transfer = GI_TRANSFER_NOTHING,
308
+ std::vector<gpointer>* ownedStrings = nullptr,
309
+ CreatedClosures* closures = nullptr,
310
+ CreatedBytes* bytes = nullptr);
311
+
312
+ // ---- IN container building -----------------------------------------
313
+ //
314
+ // Each built container is recorded so it can be freed after the invoke for
315
+ // TRANSFER_NOTHING (the callee borrowed it). TRANSFER_EVERYTHING/CONTAINER are
316
+ // adopted by the callee → never freed here.
317
+ struct InContainer {
318
+ GITypeInfo* type; // ref'd; unref'd in FreeInContainer
319
+ gpointer ptr;
320
+ GITransfer transfer;
321
+ long count; // element count (to free C-array strings)
322
+ };
323
+
324
+ bool IsSupportedContainerType(GITypeInfo* type, std::string* why);
325
+ // Whether `type` is a supported OUT/INOUT marshalling type (fundamentals, strings,
326
+ // object/interface/enum/flags, struct/boxed/union, and containers). Shared by the
327
+ // function-invoke path (calls.cc) and the vfunc chain-up path (class.cc).
328
+ bool IsSupportedOutType(GITypeInfo* type, std::string* why);
329
+ size_t CElementSize(GITypeInfo* elem);
330
+ void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n);
331
+ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo* ti,
332
+ GIArgument* arg, GITransfer transfer,
333
+ std::vector<GIArgument>* slots);
334
+ void FreeInContainer(const InContainer& c);
335
+ bool JsToInContainer(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
336
+ gpointer* outPtr, long* outCount,
337
+ std::vector<InContainer>* containers);
338
+
339
+ extern std::atomic<napi_env> g_owner_env;
340
+ extern std::atomic<bool> g_toggle_shutdown;
341
+ extern std::recursive_mutex g_queue_mutex;
342
+ extern napi_threadsafe_function g_drain_tsfn;
343
+ extern bool g_drain_async_inited;
344
+
345
+ // ---- env-teardown safety (toggle.cc) ----------------------------------------
346
+ //
347
+ // True iff `env` may enter JS right now. Probes napi_strict_equals(undef, undef)
348
+ // — NAPI_PREAMBLE-gated upstream (refs/node src/js_native_api_v8.cc), so it fails
349
+ // with napi_pending_exception / napi_cannot_run_js exactly when the env must not
350
+ // run JS: after node::FreeEnvironment / Environment::ExitEnv set
351
+ // can_call_into_js=false (env teardown, worker.terminate()), or while a JS
352
+ // exception is pending. Side-effect-free and pure N-API (portable to Bun/Deno).
353
+ //
354
+ // Every C->JS re-entry that can fire OUTSIDE a JS-initiated frame (the drain
355
+ // TSFN callback, the vfunc/callback ffi trampolines, the signal closure marshal)
356
+ // MUST check this first and degrade to a no-op when false: a node-addon-api call
357
+ // that fails on a dead env escalates to Error::ThrowAsJavaScriptException, whose
358
+ // napi_throw fails the same way -> NAPI_FATAL_IF_FAILED -> process abort
359
+ // ("FATAL ERROR: Error::ThrowAsJavaScriptException napi_throw").
360
+ bool NodeGiJsAvailable(napi_env env);
361
+
362
+ // Opt-in stderr tracing of the toggle/teardown machinery (env NODE_GI_TOGGLE_DEBUG,
363
+ // parsed once). Call sites guard with NodeGiToggleDebugEnabled() so argument
364
+ // evaluation costs nothing when off.
365
+ bool NodeGiToggleDebugEnabled();
366
+ void NodeGiToggleDebugLog(const char* fmt, ...) G_GNUC_PRINTF(1, 2);
367
+
368
+ Napi::Value MakeGObjectHandle(Napi::Env env, GObject* obj);
369
+ Napi::Value WrapGObject(Napi::Env env, GObject* obj, GITransfer transfer);
370
+
371
+ extern int g_syncEmitDepth;
372
+
373
+ // Nesting depth of loop-dispatched C->JS callbacks (the napi_make_callback sites
374
+ // in signals.cc / calls.cc / class.cc). Used to run the cross-runtime microtask
375
+ // checkpoint (NodeGiMaybeDrainMicrotasks) only at the OUTERMOST callback
376
+ // boundary — a nested dispatch (a handler re-entering the loop via a nested
377
+ // run()) defers draining to the outer boundary, exactly as Node's nested
378
+ // InternalCallbackScopes and GJS's "drain when the last JS frame exits" do.
379
+ extern int g_loopDispatchDepth;
380
+
381
+ // Cross-runtime microtask checkpoint (Bun/Deno — see loop.cc). Invokes the
382
+ // runtime-native drain registered via setMicrotaskDrain after an outermost
383
+ // loop-dispatched callback returns; a no-op on Node (never registered), with a
384
+ // pending JS exception, or when re-entered.
385
+ void NodeGiMaybeDrainMicrotasks(napi_env env);
386
+
387
+ bool SurfacePendingException(napi_env env, const char* context);
388
+ void ThrowGError(Napi::Env env, GError* error, const std::string& context);
389
+
390
+ // Forward declaration: JsToGValue (below) marshals object/boxed-typed property
391
+ // values, which need to unwrap a node-gi GObject handle; UnwrapGObject is defined
392
+ // further down (it shares the validation logic with the property/method paths).
393
+ GObject* UnwrapGObject(Napi::Env env, Napi::Value handle);
394
+
395
+ Napi::Value GValueToJs(Napi::Env env, const GValue* v);
396
+ bool JsToGValue(Napi::Env env, Napi::Value js, GValue* v);
397
+ Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
398
+ const std::string& displayName);
399
+
400
+ struct NodeGiSignalDef {
401
+ std::string name;
402
+ std::vector<GType> paramTypes;
403
+ GType returnType;
404
+ GSignalFlags flags;
405
+ };
406
+
407
+ struct NodeGiVFunc; // defined in class.cc (registerClass vfunc overrides)
408
+
409
+ // ---- Gtk.Widget composite-template API (resolved via dlsym, no GTK link) ----
410
+ //
411
+ // The engine links only girepository-2.0; GTK is dlopen'd at runtime by the
412
+ // typelib. The composite-template entry points are GtkWidgetClass / GtkWidget
413
+ // calls that take the klass pointer class_init already holds (set_template,
414
+ // bind_template_child_full) or a constructed instance (init_template,
415
+ // get_template_child) — they are not naturally reachable through the introspected
416
+ // method-invoke paths, so they are resolved by symbol from the already-loaded
417
+ // libgtk-4. All argument types are plain GLib/GObject types (GBytes, GType,
418
+ // GObject, gpointer for the opaque GtkWidgetClass/GtkWidget), so no GTK headers
419
+ // are needed. Self-contained to the template feature; nothing else dlopens GTK.
420
+ struct GtkTemplateApi {
421
+ void (*set_template)(gpointer widget_class, GBytes* template_bytes);
422
+ void (*set_template_from_resource)(gpointer widget_class, const char* resource_name);
423
+ void (*bind_template_child_full)(gpointer widget_class, const char* name, gboolean internal,
424
+ gssize struct_offset);
425
+ void (*set_css_name)(gpointer widget_class, const char* name);
426
+ void (*init_template)(gpointer widget);
427
+ GObject* (*get_template_child)(gpointer widget, GType widget_type, const char* name);
428
+ // Template-callback dispatch (`<signal handler="…">`): a custom GtkBuilderScope
429
+ // is set on the class so GtkBuilder resolves any handler name to the instance's
430
+ // JS method (mirrors GJS's TemplateBuilderScope). set_template_scope installs it;
431
+ // builder_get_current_object yields the widget being built (the handler `this`);
432
+ // builder_scope_get_type is the interface GType our scope subtype implements.
433
+ void (*set_template_scope)(gpointer widget_class, gpointer scope);
434
+ GObject* (*builder_get_current_object)(gpointer builder);
435
+ GType (*builder_scope_get_type)(void);
436
+ bool ok;
437
+ };
438
+
439
+ const GtkTemplateApi* GetGtkTemplateApi();
440
+
441
+ // Per-registered-type metadata, passed as GTypeInfo.class_data → class_init.
442
+ // Heap-allocated and intentionally never freed (a GType is process-permanent).
443
+ struct NodeGiClassData {
444
+ std::vector<GParamSpec*> properties; // ownership transfers to the class on install
445
+ std::vector<NodeGiSignalDef> signals;
446
+ std::vector<NodeGiVFunc*> vfuncs; // class-lifetime vfunc overrides (never freed)
447
+ void (*parentGet)(GObject*, guint, GValue*, GParamSpec*);
448
+ void (*parentSet)(GObject*, guint, const GValue*, GParamSpec*);
449
+ // Gtk.Widget composite template (when registerClass meta carried a Template).
450
+ bool hasTemplate = false;
451
+ GBytes* templateBytes = nullptr; // owned inline UI-XML (g_bytes_new copy)
452
+ std::string templateResource; // resource path (e.g. "/eu/app/win.ui")
453
+ std::string cssName; // gtk_widget_class_set_css_name (optional)
454
+ std::vector<std::string> children; // public Children ids
455
+ std::vector<std::string> internalChildren; // InternalChildren ids
456
+ // The generic template-callback scope set on the class (set_template_scope). Owned
457
+ // for the class lifetime (process-permanent, like cd itself); its JS env qdata is
458
+ // refreshed per construction so create_closure resolves handlers in the live env.
459
+ GObject* templateScope = nullptr;
460
+
461
+ // The cold `delete cd` failure paths (a bad GParamSpec, or g_type_register_static
462
+ // returning 0) must not leak the owned inline-template GBytes. A destructor frees
463
+ // it by construction → every `delete cd` path is covered. On the SUCCESS path cd
464
+ // is stored as the type's qdata for the class lifetime and is NEVER deleted, so
465
+ // this destructor does not run there and templateBytes stays alive for the
466
+ // template install. (properties/vfuncs are released explicitly at the delete
467
+ // sites — kept out of here to avoid double-freeing them.)
468
+ ~NodeGiClassData() {
469
+ if (templateBytes != nullptr) g_bytes_unref(templateBytes);
470
+ }
471
+ };
472
+
473
+ NodeGiClassData* FindClassData(GType type);
474
+ GQuark NodeGiScopeEnvQuark();
475
+
476
+ // Forward declaration: ConstructGObject calls gtk_widget_init_template on a
477
+ // freshly-built widget whose registered type carries a Gtk.Widget template. The
478
+ // helper (and the NodeGiClassData/GtkTemplateApi it reads) is defined with the
479
+ // registerClass machinery further down; a no-op for any non-templated type. The
480
+ // env is threaded through so the per-construction JS env can be stamped on the
481
+ // template-callback scope before init_template builds (and connects) the tree.
482
+ struct NodeGiClassData;
483
+ void MaybeInitTemplate(Napi::Env env, GObject* obj);
484
+
485
+ // Forward declarations for the Gtk.Widget composite-template callback scope. The
486
+ // implementation lives further down (alongside the JsClosure signal machinery it
487
+ // reuses), but NodeGiClassInit installs the scope and MaybeInitTemplate refreshes
488
+ // its env, both above that point.
489
+ void NodeGiInstallTemplateScopeOnClass(NodeGiClassData* cd, gpointer g_class);
490
+
491
+ // ---- N-API entry points (registered in addon.cc's Init) ----
492
+
493
+ // repo.cc
494
+ Napi::Value RequireNamespace(const Napi::CallbackInfo& info);
495
+ Napi::Value ListInfoNames(const Napi::CallbackInfo& info);
496
+ Napi::Value FindInfo(const Napi::CallbackInfo& info);
497
+ Napi::Value GetConstantValue(const Napi::CallbackInfo& info);
498
+ Napi::Value GetEnumValues(const Napi::CallbackInfo& info);
499
+ Napi::Value GetErrorDomain(const Napi::CallbackInfo& info);
500
+ Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info);
501
+ Napi::Value PrependSearchPath(const Napi::CallbackInfo& info);
502
+
503
+ // calls.cc
504
+ Napi::Value CallFunction(const Napi::CallbackInfo& info);
505
+ Napi::Value CallMethod(const Napi::CallbackInfo& info);
506
+ Napi::Value HasMethod(const Napi::CallbackInfo& info);
507
+ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info);
508
+ // The `new <Struct>()` [[Construct]] path: route to the struct's 'new'
509
+ // constructor when it has one, else zero-allocate (GJS gi/boxed.cpp parity).
510
+ Napi::Value ConstructStruct(const Napi::CallbackInfo& info);
511
+ Napi::Value CallBoxedMethod(const Napi::CallbackInfo& info);
512
+ Napi::Value IsBoxedHandle(const Napi::CallbackInfo& info);
513
+ // Struct/boxed/union FIELD access (marshal.cc). boxedMemberKind(handle, name) →
514
+ // 0 (neither) | 1 (method) | 2 (field); getBoxedField(handle, name) → the field
515
+ // value; setBoxedField(handle, name, value) writes it.
516
+ Napi::Value BoxedMemberKind(const Napi::CallbackInfo& info);
517
+ Napi::Value GetBoxedField(const Napi::CallbackInfo& info);
518
+ Napi::Value SetBoxedField(const Napi::CallbackInfo& info);
519
+ Napi::Value BoxedTypeName(const Napi::CallbackInfo& info);
520
+ // GParamSpec wrapping (object.cc): a tagged GObject-fundamental handle plus its
521
+ // name/nick/blurb/flags/value_type/owner_type/default_value accessors.
522
+ Napi::Value MakeParamSpecHandle(Napi::Env env, GParamSpec* pspec, GITransfer transfer);
523
+ Napi::Value IsParamSpecHandle(const Napi::CallbackInfo& info);
524
+ Napi::Value ParamSpecProp(const Napi::CallbackInfo& info);
525
+
526
+ // Non-GObject GObject-fundamental wrapping (object.cc): a tagged External carrying
527
+ // the raw pointer + the introspected unref func, for fundamentals like
528
+ // GskRenderNode / GdkEvent that are introspected as object info but are NOT
529
+ // GObjects (their own ref/unref, G_IS_OBJECT false — see the object.cc comment).
530
+ Napi::Value MakeFundamentalHandle(Napi::Env env, gpointer ptr, GIObjectInfo* info, GITransfer transfer);
531
+ Napi::Value IsFundamentalHandle(const Napi::CallbackInfo& info);
532
+
533
+ // object.cc
534
+ Napi::Value NewObject(const Napi::CallbackInfo& info);
535
+ Napi::Value GetProperty(const Napi::CallbackInfo& info);
536
+ Napi::Value SetProperty(const Napi::CallbackInfo& info);
537
+ Napi::Value HasProperty(const Napi::CallbackInfo& info);
538
+ Napi::Value GetTypeName(const Napi::CallbackInfo& info);
539
+ Napi::Value GetGType(const Napi::CallbackInfo& info);
540
+ Napi::Value IsInstanceOf(const Napi::CallbackInfo& info);
541
+ Napi::Value IsGObjectHandle(const Napi::CallbackInfo& info);
542
+ Napi::Value NewGValue(const Napi::CallbackInfo& info);
543
+
544
+ // class.cc
545
+ Napi::Value RegisterClass(const Napi::CallbackInfo& info);
546
+ Napi::Value RegisterClassFromGType(const Napi::CallbackInfo& info);
547
+ Napi::Value ConstructType(const Napi::CallbackInfo& info);
548
+ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info);
549
+
550
+ // template.cc
551
+ Napi::Value GetTemplateChild(const Napi::CallbackInfo& info);
552
+
553
+ // toggle.cc (TEST-ONLY cross-thread GC stress)
554
+ Napi::Value StressRefUnrefOffThread(const Napi::CallbackInfo& info);
555
+ Napi::Value StressRefUnrefRunning(const Napi::CallbackInfo& info);
556
+ Napi::Value StressRefUnrefProgress(const Napi::CallbackInfo& info);
557
+ Napi::Value StressRefUnrefStop(const Napi::CallbackInfo& info);
558
+
559
+ // variant.cc
560
+ Napi::Value VariantNew(const Napi::CallbackInfo& info);
561
+ Napi::Value VariantUnpack(const Napi::CallbackInfo& info);
562
+ Napi::Value VariantGetTypeString(const Napi::CallbackInfo& info);
563
+ Napi::Value IsVariantHandle(const Napi::CallbackInfo& info);
564
+
565
+ // signals.cc
566
+ Napi::Value ConnectSignal(const Napi::CallbackInfo& info);
567
+ Napi::Value EmitSignal(const Napi::CallbackInfo& info);
568
+ Napi::Value DisconnectSignal(const Napi::CallbackInfo& info);
569
+ Napi::Value SetTemplateCallbackResolver(const Napi::CallbackInfo& info);
570
+
571
+ // private.cc — GjsPrivate-mirroring helpers (refs/gjs/libgjs-private/gjs-util.c).
572
+ // The structured-log writer func + the bind_property_full/bind_full transform
573
+ // trampolines, which need a C wrapper for the same reasons GJS uses GjsPrivate:
574
+ // the GLogWriterFunc can fire on ANY thread (JS only runs on the registration
575
+ // thread; other threads fall back to the default writer) and the GLogField array
576
+ // is not generically introspectable; the binding transform's `to_value` is a
577
+ // write-back OUT GValue no marshaled GClosure can reach.
578
+ Napi::Value LogSetWriterFunc(const Napi::CallbackInfo& info);
579
+ Napi::Value LogSetWriterDefault(const Napi::CallbackInfo& info);
580
+ Napi::Value BindPropertyFull(const Napi::CallbackInfo& info);
581
+ Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info);
582
+
583
+ // loop.cc
584
+ Napi::Value StartMainLoop(const Napi::CallbackInfo& info);
585
+ Napi::Value IterateMainContext(const Napi::CallbackInfo& info);
586
+ Napi::Value PumpKick(const Napi::CallbackInfo& info);
587
+ Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info);
588
+
589
+ // ---- uv-driven GLib auto-pump (loop.cc) -------------------------------------
590
+ //
591
+ // In-flight scope=async keep-alive: while a GI scope=async callback (e.g. a
592
+ // GAsyncReadyCallback) is pending, the pump holds a libuv ref so a plain Node
593
+ // script survives until the completion dispatches (the top-level-await case).
594
+ // Begin returns whether the callback was counted (only on the pump-owning env,
595
+ // i.e. Node's main env after startMainLoop); the caller must call End exactly
596
+ // once for each counted callback. Main-thread only.
597
+ bool NodeGiPumpAsyncBegin(napi_env env);
598
+ void NodeGiPumpAsyncEnd();
599
+ // Close the pump's uv handles at env teardown (no-op for non-owner envs).
600
+ void NodeGiPumpShutdown(napi_env env);
601
+
602
+ // RAII window for every C→JS dispatch (the GI callback trampoline, the signal
603
+ // closure marshal, a vfunc invocation). The pump holds g_in_uv_pump while it
604
+ // iterates the context so the uv-in-GLib UvLoopSource stays parked (no reentrant
605
+ // uv_run out of the pump's own uv callbacks) — but JS dispatched FROM that
606
+ // iteration, including every microtask continuation napi_make_callback runs at
607
+ // its boundary, must not inherit the flag: user code there may legitimately
608
+ // start a blocking GLib.MainLoop.run(), which needs the UvLoopSource co-pump
609
+ // live (Node timers keep firing during the run). Clears the flag on entry,
610
+ // restores it on scope exit — mirroring SyncEmitDepthReset.
611
+ class NodeGiPumpJsDispatchScope {
612
+ public:
613
+ NodeGiPumpJsDispatchScope();
614
+ ~NodeGiPumpJsDispatchScope();
615
+ NodeGiPumpJsDispatchScope(const NodeGiPumpJsDispatchScope&) = delete;
616
+ NodeGiPumpJsDispatchScope& operator=(const NodeGiPumpJsDispatchScope&) = delete;
617
+
618
+ private:
619
+ gboolean saved_;
620
+ };
621
+
622
+ } // namespace nodegi
623
+
624
+ #endif // NODE_GI_SRC_COMMON_H_