@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/object.cc ADDED
@@ -0,0 +1,814 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // GObject lifecycle + properties: GValue <-> JS, construction and the property/type entry points.
3
+
4
+ #include "common.h"
5
+
6
+ namespace nodegi {
7
+
8
+ // ---- GParamSpec wrapping (phase 2.7a) --------------------------------------
9
+ //
10
+ // A GParamSpec is a GObject FUNDAMENTAL (its own G_TYPE_PARAM hierarchy), NOT a
11
+ // GObject and NOT a boxed type — it is reference-counted via g_param_spec_ref /
12
+ // g_param_spec_unref. So it gets its own type-tagged External (distinct from the
13
+ // GObject + boxed handles so the three never cross-dereference), carrying a held
14
+ // ref dropped on GC. The L1 layer (gi.js wrapParamSpec) turns this handle into a
15
+ // GObject.ParamSpec-shaped object exposing name/get_name/nick/blurb/flags/
16
+ // value_type/owner_type/default_value — mirroring gjs's gi/param.cpp + the
17
+ // GObject.js ParamSpec.prototype. This is what a `notify` handler's second arg
18
+ // (the changed property's pspec) and a GParamSpec-typed GValue now surface as.
19
+ static const napi_type_tag kParamSpecHandleTag = {0x7c1e9a4b2f6d8035ULL,
20
+ 0xd2b8f0a3e5c74196ULL};
21
+
22
+ Napi::Value MakeParamSpecHandle(Napi::Env env, GParamSpec* pspec, GITransfer transfer) {
23
+ if (pspec == nullptr) return env.Null();
24
+ // transfer-everything hands us an owned ref (adopt it); a borrow → add our own.
25
+ if (transfer != GI_TRANSFER_EVERYTHING) g_param_spec_ref(pspec);
26
+ Napi::External<GParamSpec> ext = Napi::External<GParamSpec>::New(
27
+ env, pspec, [](Napi::Env, GParamSpec* p) { g_param_spec_unref(p); });
28
+ if (ext.IsEmpty()) {
29
+ // napi_create_external failed with the throw swallowed (terminating env /
30
+ // pending exception). TypeTag on the empty External would abort via
31
+ // Error::New(nullptr)'s fatal sites — release the ref we took and bail with
32
+ // the empty value (see the common.h terminate-safe helpers). The finalizer
33
+ // was never registered, so this g_param_spec_unref balances the ref above.
34
+ g_param_spec_unref(pspec);
35
+ return ext;
36
+ }
37
+ ext.TypeTag(&kParamSpecHandleTag);
38
+ return ext;
39
+ }
40
+
41
+ // Read a GParamSpec from a kParamSpecHandleTag External (no dereference on a
42
+ // non-paramspec). Returns nullptr when `v` is not a node-gi paramspec handle.
43
+ static GParamSpec* TryGetParamSpec(Napi::Value v) {
44
+ if (!v.IsExternal()) return nullptr;
45
+ Napi::External<GParamSpec> ext = v.As<Napi::External<GParamSpec>>();
46
+ if (!ext.CheckTypeTag(&kParamSpecHandleTag)) return nullptr;
47
+ return ext.Data();
48
+ }
49
+
50
+ // isParamSpecHandle(value) → boolean (tag-checked; no dereference).
51
+ Napi::Value IsParamSpecHandle(const Napi::CallbackInfo& info) {
52
+ Napi::Env env = info.Env();
53
+ bool is = info.Length() >= 1 && TryGetParamSpec(info[0]) != nullptr;
54
+ return Napi::Boolean::New(env, is);
55
+ }
56
+
57
+ // ---- non-GObject GObject-fundamental wrapping (GskRenderNode, GdkEvent, …) --
58
+ //
59
+ // Some types introspected as OBJECT_INFO are GObject FUNDAMENTALS that do NOT
60
+ // derive from GObject — e.g. GskRenderNode (Gtk.Snapshot.to_node()), GdkEvent.
61
+ // They carry their OWN ref/unref funcs (gsk_render_node_ref/unref) in the
62
+ // introspection, NOT g_object_ref/unref, and G_IS_OBJECT(them) is FALSE — so
63
+ // routing them through WrapGObject runs the toggle-ref/qdata dance on a
64
+ // non-GObject → a cascade of `g_object_*: assertion 'G_IS_OBJECT (object)'
65
+ // failed` criticals AND a leaked ref (the closing g_object_unref no-ops). They
66
+ // get their own type-tagged External carrying the raw pointer as its Data (so an
67
+ // IN arg round-trips via the OBJECT External branch) + the introspected unref
68
+ // func boxed as the finalizer hint, the held ref dropped on GC via that func.
69
+ // Opaque at L1 (a pass-through intermediate); mirrors gjs's gi/fundamental.cpp
70
+ // pointer lifecycle. GParamSpec + GValue keep their dedicated branches BEFORE
71
+ // this one, so this catches the remaining fundamentals.
72
+ static const napi_type_tag kFundamentalHandleTag = {0x3b7f2e1c9a4d6058ULL,
73
+ 0x8e5a0c7b1f2d6493ULL};
74
+
75
+ Napi::Value MakeFundamentalHandle(Napi::Env env, gpointer ptr, GIObjectInfo* info,
76
+ GITransfer transfer) {
77
+ if (ptr == nullptr) return env.Null();
78
+ GIObjectInfoRefFunction refFn = gi_object_info_get_ref_function_pointer(info);
79
+ GIObjectInfoUnrefFunction unrefFn = gi_object_info_get_unref_function_pointer(info);
80
+ // We own a ref to drop on GC when the type gives us a way to take one: a
81
+ // transfer-everything return hands us an owned ref (adopt it), a borrow gets our
82
+ // own via refFn. A fundamental exposing neither ref nor a transfer-full handoff
83
+ // (unusual) is held without an owned ref → no finalizer unref (never over-unref).
84
+ bool owns = (transfer == GI_TRANSFER_EVERYTHING) || (refFn != nullptr);
85
+ if (transfer != GI_TRANSFER_EVERYTHING && refFn != nullptr) refFn(ptr);
86
+ // Box the unref func so the finalizer (which only receives data + hint) drops
87
+ // exactly the one ref we own with the RIGHT function. nullptr = don't unref.
88
+ GIObjectInfoUnrefFunction* unrefBox =
89
+ new GIObjectInfoUnrefFunction(owns ? unrefFn : nullptr);
90
+ Napi::External<void> ext = Napi::External<void>::New(
91
+ env, ptr,
92
+ [](Napi::Env, void* p, GIObjectInfoUnrefFunction* box) {
93
+ if (box != nullptr) {
94
+ if (*box != nullptr && p != nullptr) (*box)(p);
95
+ delete box;
96
+ }
97
+ },
98
+ unrefBox);
99
+ if (ext.IsEmpty()) {
100
+ // napi_create_external failed with the throw swallowed (terminating env /
101
+ // pending exception). The finalizer was never registered — drop the ref we own
102
+ // + free the box here (mirrors MakeParamSpecHandle / MakeBoxedHandle).
103
+ if (owns && unrefFn != nullptr) unrefFn(ptr);
104
+ delete unrefBox;
105
+ return ext;
106
+ }
107
+ ext.TypeTag(&kFundamentalHandleTag);
108
+ return ext;
109
+ }
110
+
111
+ // Read a fundamental pointer from a kFundamentalHandleTag External (no dereference
112
+ // on a non-fundamental). Returns nullptr when `v` is not a node-gi fundamental.
113
+ static gpointer TryGetFundamental(Napi::Value v) {
114
+ if (!v.IsExternal()) return nullptr;
115
+ Napi::External<void> ext = v.As<Napi::External<void>>();
116
+ if (!ext.CheckTypeTag(&kFundamentalHandleTag)) return nullptr;
117
+ return ext.Data();
118
+ }
119
+
120
+ // isFundamentalHandle(value) → boolean (tag-checked; no dereference).
121
+ Napi::Value IsFundamentalHandle(const Napi::CallbackInfo& info) {
122
+ Napi::Env env = info.Env();
123
+ bool is = info.Length() >= 1 && TryGetFundamental(info[0]) != nullptr;
124
+ return Napi::Boolean::New(env, is);
125
+ }
126
+
127
+ // paramSpecProp(handle, which) → the requested accessor value. `which` ∈
128
+ // name | nick | blurb | flags | valueType | ownerType | defaultValue. A single
129
+ // dispatcher keeps the native surface small; the L1 wrapper maps it to the
130
+ // GObject.ParamSpec getters + get_name()/get_nick()/get_blurb() methods.
131
+ Napi::Value ParamSpecProp(const Napi::CallbackInfo& info) {
132
+ Napi::Env env = info.Env();
133
+ GParamSpec* p = info.Length() >= 1 ? TryGetParamSpec(info[0]) : nullptr;
134
+ if (p == nullptr || info.Length() < 2 || !info[1].IsString()) {
135
+ Napi::TypeError::New(env, "paramSpecProp(handle, which: string)")
136
+ .ThrowAsJavaScriptException();
137
+ return env.Null();
138
+ }
139
+ std::string which = info[1].As<Napi::String>().Utf8Value();
140
+ if (which == "name") return Napi::String::New(env, g_param_spec_get_name(p));
141
+ if (which == "nick") {
142
+ const char* s = g_param_spec_get_nick(p);
143
+ return s != nullptr ? Napi::Value(Napi::String::New(env, s)) : env.Null();
144
+ }
145
+ if (which == "blurb") {
146
+ const char* s = g_param_spec_get_blurb(p);
147
+ return s != nullptr ? Napi::Value(Napi::String::New(env, s)) : env.Null();
148
+ }
149
+ if (which == "flags") return Napi::Number::New(env, static_cast<double>(p->flags));
150
+ if (which == "valueType") return MakeGTypeHandle(env, p->value_type);
151
+ if (which == "ownerType") return MakeGTypeHandle(env, p->owner_type);
152
+ if (which == "defaultValue") {
153
+ const GValue* dv = g_param_spec_get_default_value(p);
154
+ if (dv == nullptr || !G_IS_VALUE(dv)) return env.Null();
155
+ return GValueToJs(env, dv);
156
+ }
157
+ Napi::TypeError::New(env, std::string("unknown paramspec property '") + which + "'")
158
+ .ThrowAsJavaScriptException();
159
+ return env.Null();
160
+ }
161
+
162
+ // ---- GObject lifecycle + properties (milestone 1) ----
163
+ //
164
+ // Construct GObjects and read/write their properties. Ownership now flows through
165
+ // the toggle-ref instance GC bridge above: ConstructGObject hands its single
166
+ // construction ref to the cache-aware MakeGObjectHandle, which installs the
167
+ // canonical toggle ref (so the wrapper is collectable when JS is the sole owner,
168
+ // rooted when C also holds the object, and identity-stable + resurrectable).
169
+ //
170
+ // Instances are handed back as opaque Napi::External<GObject> handles; the
171
+ // ergonomic class/prototype surface is layered in the GJS-compat runtime.
172
+
173
+ // Marshal a GValue into a JS value (fundamental types).
174
+ Napi::Value GValueToJs(Napi::Env env, const GValue* v) {
175
+ GType ft = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(v));
176
+ // G_TYPE_GTYPE is a runtime-resolved type (g_gtype_get_type()), so it cannot be
177
+ // a switch case label — handle it up front. A GType-valued GValue marshals to a
178
+ // node-gi GType handle (0 → null).
179
+ if (G_VALUE_HOLDS_GTYPE(v)) {
180
+ GType gt = g_value_get_gtype(v);
181
+ return gt != 0 ? MakeGTypeHandle(env, gt) : env.Null();
182
+ }
183
+ // An interface-typed property/signal value (e.g. Adw.ComboRow:model →
184
+ // GListModel, a GObject INTERFACE). G_TYPE_FUNDAMENTAL(an interface) ==
185
+ // G_TYPE_INTERFACE, which has no switch case below → the default branch would
186
+ // reject it ("Unsupported property GType GListModel"). The object lives in the
187
+ // value's pointer slot: the interface inherits GObject's GTypeValueTable via
188
+ // its GObject prerequisite, so the slot IS an owned-object ref. Read it
189
+ // directly — g_value_get_object g_return_val_if_fail's G_VALUE_HOLDS_OBJECT,
190
+ // which is FALSE for an interface type (g_type_is_a(GListModel, G_TYPE_OBJECT)
191
+ // == false). Mirrors GJS (refs/gjs/gi/value.cpp:1071 + gi/value.h:110) +
192
+ // JsToGValue's interface branch. Borrow (transfer-none; WrapGObject refs).
193
+ if (G_TYPE_IS_INTERFACE(G_VALUE_TYPE(v))) {
194
+ return WrapGObject(env, static_cast<GObject*>(v->data[0].v_pointer),
195
+ GI_TRANSFER_NOTHING);
196
+ }
197
+ switch (ft) {
198
+ case G_TYPE_BOOLEAN: return Napi::Boolean::New(env, g_value_get_boolean(v));
199
+ case G_TYPE_CHAR: return Napi::Number::New(env, g_value_get_schar(v));
200
+ case G_TYPE_UCHAR: return Napi::Number::New(env, g_value_get_uchar(v));
201
+ case G_TYPE_INT: return Napi::Number::New(env, g_value_get_int(v));
202
+ case G_TYPE_UINT: return Napi::Number::New(env, g_value_get_uint(v));
203
+ // 64-bit OUT: GJS returns a Number, warning when it can't be stored exactly.
204
+ case G_TYPE_LONG: {
205
+ glong x = g_value_get_long(v);
206
+ WarnIfUnsafeInt64(x);
207
+ return Napi::Number::New(env, static_cast<double>(x));
208
+ }
209
+ case G_TYPE_ULONG: {
210
+ gulong x = g_value_get_ulong(v);
211
+ WarnIfUnsafeUint64(x);
212
+ return Napi::Number::New(env, static_cast<double>(x));
213
+ }
214
+ case G_TYPE_INT64: {
215
+ gint64 x = g_value_get_int64(v);
216
+ WarnIfUnsafeInt64(x);
217
+ return Napi::Number::New(env, static_cast<double>(x));
218
+ }
219
+ case G_TYPE_UINT64: {
220
+ guint64 x = g_value_get_uint64(v);
221
+ WarnIfUnsafeUint64(x);
222
+ return Napi::Number::New(env, static_cast<double>(x));
223
+ }
224
+ case G_TYPE_FLOAT: return Napi::Number::New(env, g_value_get_float(v));
225
+ case G_TYPE_DOUBLE: return Napi::Number::New(env, g_value_get_double(v));
226
+ case G_TYPE_ENUM: return Napi::Number::New(env, g_value_get_enum(v));
227
+ case G_TYPE_FLAGS: return Napi::Number::New(env, g_value_get_flags(v));
228
+ case G_TYPE_STRING: {
229
+ const char* s = g_value_get_string(v);
230
+ return s != nullptr ? Napi::Value(Napi::String::New(env, s)) : env.Null();
231
+ }
232
+ case G_TYPE_VARIANT: {
233
+ // A GVariant-typed property (e.g. Gio.SimpleAction:state) or signal
234
+ // parameter (Gio.SimpleAction::change-state). g_value_get_variant borrows
235
+ // (transfer none) → wrap as a node-gi GLib.Variant handle (own a ref).
236
+ GVariant* var = g_value_get_variant(v);
237
+ if (var == nullptr) return env.Null();
238
+ return WrapVariant(env, var, GI_TRANSFER_NOTHING);
239
+ }
240
+ case G_TYPE_BOXED: {
241
+ // A boxed-typed property/signal value (e.g. Gio.SimpleAction:parameter-type →
242
+ // GVariantType, a boxed `G_TYPE_VARIANT_TYPE`). g_value_get_boxed BORROWS the
243
+ // payload; copy so our handle owns its own and can g_boxed_free it on GC.
244
+ // An UNSET boxed property (NULL pointer) → null, matching GJS — NOT a throw.
245
+ // Verified against gjs 1.88:
246
+ // new Gio.SimpleAction({name:'x'}).parameterType → null
247
+ // Gio.SimpleAction.new('y', GLib.VariantType.new('s')).parameterType
248
+ // .dup_string() → 's'
249
+ // The wrapped boxed handle carries its GType, so L1 wrapBoxed routes
250
+ // `.dup_string()` etc. through CallBoxedMethod (gi_repository_find_by_gtype →
251
+ // GLib.VariantType struct methods). (GVariant is fundamental, handled by its
252
+ // own G_TYPE_VARIANT case above.)
253
+ //
254
+ // G_TYPE_BYTE_ARRAY (a GByteArray) is special-cased to a Node Buffer (Uint8Array)
255
+ // — NOT a boxed handle — matching gjs (refs/gjs/gi/value.cpp:1091
256
+ // gjs_byte_array_from_byte_array). Verified vs gjs 1.88 (a GByteArray signal
257
+ // param → a Uint8Array in the handler). GBytes (G_TYPE_BYTES) is deliberately
258
+ // NOT special-cased here: gjs leaves it as a GLib.Bytes boxed handle
259
+ // (value.cpp has no GBytes case → the generic boxed branch), which is what the
260
+ // MakeBoxedHandle below already produces (verified: a GBytes signal param →
261
+ // `instanceof GLib.Bytes`). A GStrv-as-array read stays a follow-up.
262
+ if (G_VALUE_HOLDS(v, G_TYPE_BYTE_ARRAY)) {
263
+ GByteArray* ba = static_cast<GByteArray*>(g_value_get_boxed(v));
264
+ if (ba == nullptr) return env.Null();
265
+ return Napi::Buffer<uint8_t>::Copy(env, ba->data, ba->len);
266
+ }
267
+ gpointer boxed = g_value_get_boxed(v);
268
+ if (boxed == nullptr) return env.Null();
269
+ GType bt = G_VALUE_TYPE(v);
270
+ return MakeBoxedHandle(env, g_boxed_copy(bt, boxed), bt, true);
271
+ }
272
+ case G_TYPE_OBJECT:
273
+ // Signal/property object values are transfer-none borrows; WrapGObject refs.
274
+ return WrapGObject(env, static_cast<GObject*>(g_value_get_object(v)), GI_TRANSFER_NOTHING);
275
+ case G_TYPE_PARAM: {
276
+ // GParamSpec (e.g. the `notify` signal's second argument, or a
277
+ // GParamSpec-typed property/value). Surface a real, tagged GObject.ParamSpec
278
+ // handle (borrow → own a ref) so a handler can read
279
+ // .name/.get_name()/.value_type/.nick/.blurb/.flags/.owner_type — matching
280
+ // gjs. g_value_get_param BORROWS (transfer none).
281
+ GParamSpec* p = g_value_get_param(v);
282
+ return MakeParamSpecHandle(env, p, GI_TRANSFER_NOTHING);
283
+ }
284
+ default:
285
+ Napi::TypeError::New(env, std::string("Unsupported property GType ") +
286
+ g_type_name(G_VALUE_TYPE(v)) + " (milestone 1: fundamentals only)")
287
+ .ThrowAsJavaScriptException();
288
+ return env.Undefined();
289
+ }
290
+ }
291
+
292
+ // Marshal a JS value into an already-g_value_init'd GValue.
293
+ bool JsToGValue(Napi::Env env, Napi::Value js, GValue* v) {
294
+ if (js.IsEmpty()) {
295
+ // Residue of a swallowed napi failure upstream (a fallible props.Get() /
296
+ // coercion failed on a terminating env, or a throwing getter left the
297
+ // exception pending). Coercing it would abort via Error::New(nullptr)'s
298
+ // fatal sites — fail the marshal cleanly (see the common.h terminate-safe
299
+ // helpers). This is the terminate-mid-`newObject` path: ConstructGObject's
300
+ // props loop calls this per property value. Gate the diagnostic throw on
301
+ // NodeGiJsAvailable — false on a dying env (a Napi::Error built there aborts
302
+ // via the SAME funnel) AND on a live env with a pending exception (propagate
303
+ // that one) — so we only throw when the env can safely build the error.
304
+ if (NodeGiJsAvailable(env)) {
305
+ Napi::Error::New(env, "property value unavailable (env is terminating)")
306
+ .ThrowAsJavaScriptException();
307
+ }
308
+ return false;
309
+ }
310
+ GType ft = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(v));
311
+ // G_TYPE_GTYPE is a runtime-resolved type (not constexpr) → handle before the
312
+ // switch. A GType handle (or null → 0) into a GType-valued GValue.
313
+ if (G_VALUE_HOLDS_GTYPE(v)) {
314
+ GType gt = 0;
315
+ if (!UnwrapGTypeArg(env, js, &gt)) return false;
316
+ g_value_set_gtype(v, gt);
317
+ return true;
318
+ }
319
+ // G_TYPE_BYTE_ARRAY (a GByteArray boxed value, e.g. a byte-array signal param or
320
+ // property). G_TYPE_FUNDAMENTAL == G_TYPE_BOXED, so the switch below would route a
321
+ // Uint8Array to the boxed-HANDLE case and reject it. Special-case it FIRST — a JS
322
+ // Uint8Array/Buffer → a freshly-allocated GByteArray, matching gjs
323
+ // (refs/gjs/gi/value.cpp:783 gjs_byte_array_get_byte_array, gated on
324
+ // JS_IsUint8Array). A non-Uint8Array value falls through to the generic boxed
325
+ // handling below (so a real GByteArray boxed handle still works), exactly as gjs
326
+ // does. g_value_take_boxed makes the GValue OWN the GByteArray; g_value_unset frees
327
+ // it. null/undefined → NULL. Verified vs gjs 1.88 (Uint8Array → GByteArray signal
328
+ // param round-trips to a Uint8Array in the handler).
329
+ if (G_VALUE_HOLDS(v, G_TYPE_BYTE_ARRAY)) {
330
+ if (js.IsNull() || js.IsUndefined()) {
331
+ g_value_set_boxed(v, nullptr);
332
+ return true;
333
+ }
334
+ const uint8_t* bytes = nullptr;
335
+ size_t len = 0;
336
+ bool isU8 = false;
337
+ if (js.IsBuffer()) {
338
+ Napi::Buffer<uint8_t> b = js.As<Napi::Buffer<uint8_t>>();
339
+ bytes = b.Data();
340
+ len = b.Length();
341
+ isU8 = true;
342
+ } else if (js.IsTypedArray() && js.As<Napi::TypedArray>().TypedArrayType() == napi_uint8_array) {
343
+ Napi::TypedArray ta = js.As<Napi::TypedArray>();
344
+ bytes = static_cast<const uint8_t*>(ta.ArrayBuffer().Data()) + ta.ByteOffset();
345
+ len = ta.ByteLength();
346
+ isU8 = true;
347
+ }
348
+ if (isU8) {
349
+ GByteArray* ba = g_byte_array_new();
350
+ if (len > 0) g_byte_array_append(ba, bytes, static_cast<guint>(len));
351
+ g_value_take_boxed(v, ba);
352
+ return true;
353
+ }
354
+ // else: fall through to the generic G_TYPE_BOXED handling (a boxed handle).
355
+ }
356
+ // G_TYPE_STRV (a GStrv / NULL-terminated char** boxed property, e.g.
357
+ // Gtk.Widget:css-classes). G_TYPE_FUNDAMENTAL(G_TYPE_STRV) == G_TYPE_BOXED, so
358
+ // the switch below would route it to the boxed-HANDLE case and reject a JS array
359
+ // ("expected a boxed handle for a GStrv property"). Special-case it FIRST — a JS
360
+ // string[] → a freshly-allocated GStrv — exactly as GJS does before its generic
361
+ // g_type_is_a(gtype, G_TYPE_BOXED) handling (refs/gjs/gi/value.cpp:704-725).
362
+ // Ownership: g_value_take_boxed makes the GValue OWN the GStrv; g_object_new
363
+ // COPIES it into the property (g_strdupv), and ConstructGObject's g_value_unset
364
+ // frees the GValue's copy via g_strfreev — no double-free, no leak.
365
+ if (G_VALUE_HOLDS(v, G_TYPE_STRV)) {
366
+ if (js.IsNull() || js.IsUndefined()) {
367
+ g_value_set_boxed(v, nullptr);
368
+ return true;
369
+ }
370
+ if (!js.IsArray()) {
371
+ Napi::TypeError::New(env, "expected a string[] for a GStrv property")
372
+ .ThrowAsJavaScriptException();
373
+ return false;
374
+ }
375
+ Napi::Array arr = js.As<Napi::Array>();
376
+ guint len = arr.Length();
377
+ gchar** strv = g_new0(gchar*, len + 1); // +1 for the NULL terminator
378
+ for (guint i = 0; i < len; i++) {
379
+ // NodeGiToUtf8: terminate-safe (a swallowed Get/coercion failure must not
380
+ // cascade into Error::New(nullptr) — see common.h).
381
+ strv[i] = g_strdup(NodeGiToUtf8(arr.Get(i)).c_str());
382
+ }
383
+ g_value_take_boxed(v, strv);
384
+ return true;
385
+ }
386
+ // An interface-typed property (e.g. Adw.ComboRow:model is GListModel, a GObject
387
+ // INTERFACE; Gtk.MenuButton:menu-model, …). G_TYPE_FUNDAMENTAL(an interface) ==
388
+ // G_TYPE_INTERFACE, which has no switch case below → the default branch would
389
+ // reject it ("Unsupported property GType GListModel"). The JS value is a wrapped
390
+ // GObject that IMPLEMENTS the interface (e.g. a Gtk.StringList / Gio.ListStore
391
+ // implementing GListModel). GObject's g_value_set_object g_return_if_fail's
392
+ // G_VALUE_HOLDS_OBJECT, which is FALSE for an interface-typed GValue
393
+ // (g_type_is_a(GListModel, G_TYPE_OBJECT) == false), so mirror GJS
394
+ // (refs/gjs/gi/value.cpp:684 + gi/value.h:165): set the value's object slot
395
+ // directly with g_set_object — the interface inherits GObject's value table via
396
+ // its GObject prerequisite, so the slot IS an owned-object ref. Same ownership
397
+ // as the G_TYPE_OBJECT case (#659): g_set_object refs the object (our wrapper
398
+ // keeps its own ref → no double-free), ConstructGObject's g_value_unset drops
399
+ // the GValue's ref. null/undefined → clear; a non-implementing / non-GObject
400
+ // value → clean TypeError.
401
+ if (G_TYPE_IS_INTERFACE(G_VALUE_TYPE(v))) {
402
+ GObject* obj = nullptr;
403
+ if (!(js.IsNull() || js.IsUndefined())) {
404
+ obj = UnwrapGObject(env, js);
405
+ if (obj == nullptr) return false; // UnwrapGObject threw a TypeError
406
+ if (!g_type_is_a(G_OBJECT_TYPE(obj), G_VALUE_TYPE(v))) {
407
+ Napi::TypeError::New(env, std::string("expected an object implementing ") +
408
+ g_type_name(G_VALUE_TYPE(v)) + ", got " +
409
+ g_type_name(G_OBJECT_TYPE(obj)))
410
+ .ThrowAsJavaScriptException();
411
+ return false;
412
+ }
413
+ }
414
+ g_set_object(&v->data[0].v_pointer, obj);
415
+ return true;
416
+ }
417
+ switch (ft) {
418
+ // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
419
+ // napi failure mid worker.terminate() must degrade to a zero, not cascade
420
+ // into Error::New(nullptr)'s fatal sites (the terminate-mid-`newObject` path).
421
+ case G_TYPE_BOOLEAN: g_value_set_boolean(v, NodeGiToBool(js)); return true;
422
+ case G_TYPE_CHAR: g_value_set_schar(v, static_cast<gint8>(NodeGiToInt32(js))); return true;
423
+ case G_TYPE_UCHAR: g_value_set_uchar(v, static_cast<guchar>(NodeGiToUint32(js))); return true;
424
+ case G_TYPE_INT: g_value_set_int(v, NodeGiToInt32(js)); return true;
425
+ case G_TYPE_UINT: g_value_set_uint(v, NodeGiToUint32(js)); return true;
426
+ // 64-bit (glong/gulong are 64-bit on LP64): accept a BigInt losslessly, else a
427
+ // truncated Number — never let a BigInt reach ToNumber(). Shared with the GI
428
+ // scalar marshaller via JsValueTo{Int,Uint}64 (common.h).
429
+ case G_TYPE_LONG: g_value_set_long(v, static_cast<glong>(JsValueToInt64(js))); return true;
430
+ case G_TYPE_ULONG: g_value_set_ulong(v, static_cast<gulong>(JsValueToUint64(js))); return true;
431
+ case G_TYPE_INT64: g_value_set_int64(v, JsValueToInt64(js)); return true;
432
+ case G_TYPE_UINT64: g_value_set_uint64(v, JsValueToUint64(js)); return true;
433
+ case G_TYPE_FLOAT: g_value_set_float(v, static_cast<float>(NodeGiToDouble(js))); return true;
434
+ case G_TYPE_DOUBLE: g_value_set_double(v, NodeGiToDouble(js)); return true;
435
+ case G_TYPE_ENUM: g_value_set_enum(v, NodeGiToInt32(js)); return true;
436
+ case G_TYPE_FLAGS: g_value_set_flags(v, NodeGiToUint32(js)); return true;
437
+ case G_TYPE_STRING:
438
+ if (js.IsNull() || js.IsUndefined()) {
439
+ g_value_set_string(v, nullptr);
440
+ } else {
441
+ std::string s = NodeGiToUtf8(js);
442
+ g_value_set_string(v, s.c_str()); // g_value_set_string copies
443
+ }
444
+ return true;
445
+ case G_TYPE_VARIANT: {
446
+ // A GLib.Variant handle (or null) into a GVariant-typed GValue.
447
+ // g_value_set_variant refs (and sinks a floating ref); our handle keeps
448
+ // its own ref, so no double-free.
449
+ if (js.IsNull() || js.IsUndefined()) {
450
+ g_value_set_variant(v, nullptr);
451
+ return true;
452
+ }
453
+ gpointer p = nullptr;
454
+ if (!TryGetBoxedPtr(js, &p) || p == nullptr) {
455
+ Napi::TypeError::New(env, "expected a GLib.Variant for a GVariant value")
456
+ .ThrowAsJavaScriptException();
457
+ return false;
458
+ }
459
+ g_value_set_variant(v, static_cast<GVariant*>(p));
460
+ return true;
461
+ }
462
+ case G_TYPE_OBJECT: {
463
+ // An object-typed property (e.g. Gtk.ApplicationWindow:application,
464
+ // Gtk.Widget:child, :transient-for, :model …). Mirrors GValueToJs's
465
+ // G_TYPE_OBJECT case in the other direction: unwrap the node-gi GObject
466
+ // handle and hand it to g_value_set_object, which takes its OWN ref (our
467
+ // wrapper keeps its handle ref → no double-free). null/undefined clears it.
468
+ if (js.IsNull() || js.IsUndefined()) {
469
+ g_value_set_object(v, nullptr);
470
+ return true;
471
+ }
472
+ GObject* obj = UnwrapGObject(env, js);
473
+ if (obj == nullptr) return false; // UnwrapGObject threw a TypeError
474
+ // Type-safety: g_value_set_object only g_warning's on a type mismatch (then
475
+ // sets NULL) — but that warning ABORTS under G_DEBUG=fatal-criticals. Pre-check
476
+ // with g_type_is_a and throw a clean, catchable JS TypeError instead (#659).
477
+ if (!g_type_is_a(G_OBJECT_TYPE(obj), G_VALUE_TYPE(v))) {
478
+ Napi::TypeError::New(env, std::string("expected a ") + g_type_name(G_VALUE_TYPE(v)) +
479
+ ", got " + g_type_name(G_OBJECT_TYPE(obj)))
480
+ .ThrowAsJavaScriptException();
481
+ return false;
482
+ }
483
+ g_value_set_object(v, obj);
484
+ return true;
485
+ }
486
+ case G_TYPE_BOXED: {
487
+ // A boxed-typed property (e.g. a GdkRGBA, Gtk.Border, …). g_value_set_boxed
488
+ // COPIES the boxed payload, so handing it our handle's pointer is safe — the
489
+ // handle retains ownership. null/undefined clears it.
490
+ if (js.IsNull() || js.IsUndefined()) {
491
+ g_value_set_boxed(v, nullptr);
492
+ return true;
493
+ }
494
+ BoxedHandle* h = TryGetBoxedHandle(js);
495
+ if (h == nullptr || h->ptr == nullptr) {
496
+ Napi::TypeError::New(env, std::string("expected a boxed handle for a ") +
497
+ g_type_name(G_VALUE_TYPE(v)) + " property")
498
+ .ThrowAsJavaScriptException();
499
+ return false;
500
+ }
501
+ // Type-safety: g_value_set_boxed blind-copies per the GValue's boxed GType, so
502
+ // a wrong boxed handle is undefined behaviour (no GLib guard at all). Reject a
503
+ // mismatch with a clean TypeError. Only checkable when the handle carries a
504
+ // known boxed GType (non-registered C structs are G_TYPE_INVALID) (#659).
505
+ if (h->gtype != G_TYPE_INVALID && !g_type_is_a(h->gtype, G_VALUE_TYPE(v))) {
506
+ Napi::TypeError::New(env, std::string("expected a ") + g_type_name(G_VALUE_TYPE(v)) +
507
+ " boxed handle, got " + g_type_name(h->gtype))
508
+ .ThrowAsJavaScriptException();
509
+ return false;
510
+ }
511
+ g_value_set_boxed(v, h->ptr);
512
+ return true;
513
+ }
514
+ default:
515
+ Napi::TypeError::New(env, std::string("Unsupported property GType ") +
516
+ g_type_name(G_VALUE_TYPE(v)))
517
+ .ThrowAsJavaScriptException();
518
+ return false;
519
+ }
520
+ }
521
+
522
+ GObject* UnwrapGObject(Napi::Env env, Napi::Value handle) {
523
+ // Tag-check before touching Data() — a GType handle (registerClass) is also an
524
+ // External but holds a non-dereferenceable integer; G_IS_OBJECT on it crashes.
525
+ if (!handle.IsExternal() ||
526
+ !handle.As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag)) {
527
+ Napi::TypeError::New(env, "expected a node-gi GObject handle").ThrowAsJavaScriptException();
528
+ return nullptr;
529
+ }
530
+ GObject* obj = handle.As<Napi::External<GObject>>().Data();
531
+ if (obj == nullptr || !G_IS_OBJECT(obj)) {
532
+ Napi::TypeError::New(env, "invalid GObject handle").ThrowAsJavaScriptException();
533
+ return nullptr;
534
+ }
535
+ return obj;
536
+ }
537
+
538
+ // Shared constructor core: realise the class, marshal `props` into a GValue
539
+ // vector against the type's (inherited) GParamSpecs, g_object_new_with_properties,
540
+ // and hand back an owned External<GObject>. Used by both newObject (resolve by
541
+ // namespace.typeName) and constructType (resolve by a registered GType handle).
542
+ Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
543
+ const std::string& displayName) {
544
+ Napi::Array names = props.GetPropertyNames();
545
+ if (names.IsEmpty()) {
546
+ // napi_get_property_names failed with the throw swallowed — worker.terminate()
547
+ // landed while inside this native construct (the dominant terminate-mid-
548
+ // `newObject` funnel: the hot loop is here when the env dies). names.Length()
549
+ // on the empty Array would abort via Error::New(nullptr)'s fatal sites. Bail
550
+ // with the empty value; gate the diagnostic throw on NodeGiJsAvailable —
551
+ // constructing a Napi::Error on the dying env would abort via the SAME funnel
552
+ // (false there AND on a live env with a pending exception, which we let
553
+ // propagate). Nothing was allocated yet.
554
+ if (NodeGiJsAvailable(env)) {
555
+ Napi::Error::New(env, "construction unavailable (env is terminating)")
556
+ .ThrowAsJavaScriptException();
557
+ }
558
+ return env.Null();
559
+ }
560
+ guint n = names.Length();
561
+ std::vector<GValue> values(n); // zero-initialised == G_VALUE_INIT
562
+ std::vector<std::string> nameStorage(n);
563
+ std::vector<const char*> cnames(n);
564
+
565
+ gpointer klass = g_type_class_ref(gtype); // realises the class so pspecs exist
566
+ guint initialised = 0;
567
+ bool ok = true;
568
+ for (guint i = 0; i < n; i++) {
569
+ // A per-property name read can come back EMPTY when worker.terminate() lands
570
+ // mid-loop (a swallowed napi failure). Bail WITHOUT throwing — the pre-existing
571
+ // "has no property ''" TypeError below would run Error::New on the now-dying
572
+ // env and abort via the funnel. On a live env names.Get(i) is never empty.
573
+ Napi::Value nameVal = names.Get(i);
574
+ if (nameVal.IsEmpty()) {
575
+ ok = false;
576
+ break;
577
+ }
578
+ nameStorage[i] = NodeGiToUtf8(nameVal);
579
+ GParamSpec* pspec =
580
+ g_object_class_find_property(reinterpret_cast<GObjectClass*>(klass), nameStorage[i].c_str());
581
+ if (pspec == nullptr) {
582
+ Napi::TypeError::New(env, displayName + " has no property '" + nameStorage[i] + "'")
583
+ .ThrowAsJavaScriptException();
584
+ ok = false;
585
+ break;
586
+ }
587
+ g_value_init(&values[i], pspec->value_type);
588
+ initialised = i + 1;
589
+ if (!JsToGValue(env, props.Get(nameStorage[i]), &values[i])) {
590
+ ok = false;
591
+ break;
592
+ }
593
+ cnames[i] = nameStorage[i].c_str();
594
+ }
595
+
596
+ GObject* obj = nullptr;
597
+ if (ok) {
598
+ obj = g_object_new_with_properties(gtype, n, cnames.data(), values.data());
599
+ }
600
+ for (guint j = 0; j < initialised; j++) g_value_unset(&values[j]);
601
+ g_type_class_unref(klass);
602
+
603
+ if (!ok) return env.Null();
604
+ if (obj == nullptr) {
605
+ Napi::Error::New(env, "Failed to construct " + displayName).ThrowAsJavaScriptException();
606
+ return env.Null();
607
+ }
608
+ // Take a single strong, non-floating ref; the finalizer releases it.
609
+ if (g_object_is_floating(obj)) {
610
+ g_object_ref_sink(obj);
611
+ }
612
+ // Gtk.Widget composite template: instantiate the template tree on this
613
+ // instance. The canonical GTK call is from instance_init; calling it here —
614
+ // right after construction, before the wrapper reaches JS — is equivalent for a
615
+ // templated leaf type (the widget is fully constructed; init_template builds the
616
+ // declared children + binds them so get_template_child resolves). A no-op unless
617
+ // the constructed type carries node-gi template data (defined below; forward-
618
+ // declared above), so plain introspected construction (newObject) is untouched.
619
+ MaybeInitTemplate(env, obj);
620
+ return MakeGObjectHandle(env, obj);
621
+ }
622
+
623
+ // newObject(namespace, typeName, props?: Record<string, unknown>) -> External<GObject>
624
+ Napi::Value NewObject(const Napi::CallbackInfo& info) {
625
+ Napi::Env env = info.Env();
626
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
627
+ Napi::TypeError::New(env, "newObject(namespace: string, typeName: string, props?: object)")
628
+ .ThrowAsJavaScriptException();
629
+ return env.Null();
630
+ }
631
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
632
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
633
+ Napi::Object props =
634
+ (info.Length() >= 3 && info[2].IsObject()) ? info[2].As<Napi::Object>() : Napi::Object::New(env);
635
+
636
+ GIRepository* repo = DupDefaultRepository();
637
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
638
+ bool isObject = base != nullptr && GI_IS_OBJECT_INFO(base);
639
+ GType gtype = isObject
640
+ ? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base))
641
+ : G_TYPE_INVALID;
642
+ if (base != nullptr) gi_base_info_unref(base);
643
+ g_object_unref(repo);
644
+ if (!isObject || gtype == G_TYPE_INVALID || gtype == G_TYPE_NONE) {
645
+ // On a terminating env the ns/tn string reads above degrade to "" (a swallowed
646
+ // napi failure), so find_by_name misses and we land here — but building a
647
+ // Napi::TypeError on the dying env would abort via Error::New's fatal funnel.
648
+ // Gate on NodeGiJsAvailable (false on a dying env AND on a live env with a
649
+ // pending exception); a genuine bad-name on a live env still throws.
650
+ if (NodeGiJsAvailable(env)) {
651
+ Napi::TypeError::New(env, ns + "." + tn + " is not a constructible GObject type")
652
+ .ThrowAsJavaScriptException();
653
+ }
654
+ return env.Null();
655
+ }
656
+
657
+ return ConstructGObject(env, gtype, props, ns + "." + tn);
658
+ }
659
+
660
+ // getProperty(handle, name) -> unknown
661
+ Napi::Value GetProperty(const Napi::CallbackInfo& info) {
662
+ Napi::Env env = info.Env();
663
+ if (info.Length() < 2 || !info[1].IsString()) {
664
+ Napi::TypeError::New(env, "getProperty(handle, name: string)").ThrowAsJavaScriptException();
665
+ return env.Null();
666
+ }
667
+ GObject* obj = UnwrapGObject(env, info[0]);
668
+ if (obj == nullptr) return env.Null();
669
+ std::string name = info[1].As<Napi::String>().Utf8Value();
670
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
671
+ if (pspec == nullptr) {
672
+ Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
673
+ return env.Null();
674
+ }
675
+ GValue v = G_VALUE_INIT;
676
+ g_value_init(&v, pspec->value_type);
677
+ g_object_get_property(obj, name.c_str(), &v);
678
+ Napi::Value result = GValueToJs(env, &v);
679
+ g_value_unset(&v);
680
+ return result;
681
+ }
682
+
683
+ // setProperty(handle, name, value) -> void
684
+ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
685
+ Napi::Env env = info.Env();
686
+ if (info.Length() < 3 || !info[1].IsString()) {
687
+ Napi::TypeError::New(env, "setProperty(handle, name: string, value)").ThrowAsJavaScriptException();
688
+ return env.Undefined();
689
+ }
690
+ GObject* obj = UnwrapGObject(env, info[0]);
691
+ if (obj == nullptr) return env.Undefined();
692
+ std::string name = info[1].As<Napi::String>().Utf8Value();
693
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
694
+ if (pspec == nullptr) {
695
+ Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
696
+ return env.Undefined();
697
+ }
698
+ GValue v = G_VALUE_INIT;
699
+ g_value_init(&v, pspec->value_type);
700
+ if (JsToGValue(env, info[2], &v)) {
701
+ g_object_set_property(obj, name.c_str(), &v);
702
+ }
703
+ g_value_unset(&v);
704
+ return env.Undefined();
705
+ }
706
+
707
+ // getTypeName(handle) -> string (the runtime GType name of an instance)
708
+ Napi::Value GetTypeName(const Napi::CallbackInfo& info) {
709
+ Napi::Env env = info.Env();
710
+ GObject* obj = UnwrapGObject(env, info[0]);
711
+ if (obj == nullptr) return env.Null();
712
+ return Napi::String::New(env, G_OBJECT_TYPE_NAME(obj));
713
+ }
714
+
715
+ // getGType(namespace, name) -> GType handle | null
716
+ // The runtime GType of an introspected registered type (object/interface/struct/
717
+ // union/enum/flags), as a node-gi GType handle. The L1 layer surfaces it as a
718
+ // lazy `Ns.Type.$gtype` getter (so `Adw.Clamp.$gtype` / `GObject.type_ensure(...)`
719
+ // work). Returns null for an unknown or unregistered name.
720
+ Napi::Value GetGType(const Napi::CallbackInfo& info) {
721
+ Napi::Env env = info.Env();
722
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
723
+ Napi::TypeError::New(env, "getGType(namespace: string, name: string)")
724
+ .ThrowAsJavaScriptException();
725
+ return env.Null();
726
+ }
727
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
728
+ std::string nm = info[1].As<Napi::String>().Utf8Value();
729
+ GIRepository* repo = DupDefaultRepository();
730
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), nm.c_str());
731
+ GType gt = (base != nullptr && GI_IS_REGISTERED_TYPE_INFO(base))
732
+ ? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base))
733
+ : G_TYPE_INVALID;
734
+ if (base != nullptr) gi_base_info_unref(base);
735
+ g_object_unref(repo);
736
+ if (gt == G_TYPE_INVALID || gt == G_TYPE_NONE) return env.Null();
737
+ return MakeGTypeHandle(env, gt);
738
+ }
739
+
740
+ // isInstanceOf(handle, namespace, typeName) -> boolean
741
+ // Whether the instance's GType is-a `namespace.typeName` (g_type_is_a, which also
742
+ // returns true when the type IMPLEMENTS an interface). The L1 wrapper uses it to
743
+ // pick the right Gio._promisify registration when two classes promisify a method
744
+ // of the same name (resolve by the instance's class). False for an unknown type.
745
+ Napi::Value IsInstanceOf(const Napi::CallbackInfo& info) {
746
+ Napi::Env env = info.Env();
747
+ if (info.Length() < 3 || !info[1].IsString() || !info[2].IsString()) {
748
+ Napi::TypeError::New(env, "isInstanceOf(handle, namespace: string, typeName: string)")
749
+ .ThrowAsJavaScriptException();
750
+ return env.Null();
751
+ }
752
+ GObject* obj = UnwrapGObject(env, info[0]);
753
+ if (obj == nullptr) return env.Null(); // already threw
754
+ std::string ns = info[1].As<Napi::String>().Utf8Value();
755
+ std::string tn = info[2].As<Napi::String>().Utf8Value();
756
+ GIRepository* repo = DupDefaultRepository();
757
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
758
+ GType target = (base != nullptr && GI_IS_REGISTERED_TYPE_INFO(base))
759
+ ? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base))
760
+ : 0;
761
+ bool result = target != 0 && g_type_is_a(G_OBJECT_TYPE(obj), target);
762
+ if (base != nullptr) gi_base_info_unref(base);
763
+ g_object_unref(repo);
764
+ return Napi::Boolean::New(env, result);
765
+ }
766
+
767
+ // hasProperty(handle, name) -> boolean
768
+ // Whether the instance's type has a GObject property by this name. The L1
769
+ // wrapper uses it to route `obj.foo` to a property read vs an `obj.foo()` method.
770
+ Napi::Value HasProperty(const Napi::CallbackInfo& info) {
771
+ Napi::Env env = info.Env();
772
+ if (info.Length() < 2 || !info[1].IsString()) {
773
+ Napi::TypeError::New(env, "hasProperty(handle, name: string)").ThrowAsJavaScriptException();
774
+ return env.Null();
775
+ }
776
+ GObject* obj = UnwrapGObject(env, info[0]);
777
+ if (obj == nullptr) return env.Null();
778
+ std::string name = info[1].As<Napi::String>().Utf8Value();
779
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
780
+ return Napi::Boolean::New(env, pspec != nullptr);
781
+ }
782
+
783
+ // isGObjectHandle(value) -> boolean
784
+ // Whether `value` is one of node-gi's GObject-instance handles (tag-checked, no
785
+ // dereference). Lets the L1 wrapper detect object-typed return values and wrap
786
+ // them as instances for chaining, without misclassifying a GType handle.
787
+ Napi::Value IsGObjectHandle(const Napi::CallbackInfo& info) {
788
+ Napi::Env env = info.Env();
789
+ bool is = info.Length() >= 1 && info[0].IsExternal() &&
790
+ info[0].As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag);
791
+ return Napi::Boolean::New(env, is);
792
+ }
793
+
794
+ // newGValue() -> a fresh, zero-initialised GObject.Value boxed handle.
795
+ //
796
+ // GObject.Value has no g_value_new(): a GValue is a plain struct the caller owns,
797
+ // so GJS's `new GObject.Value()` allocates one specially (refs/gjs gi/value.cpp).
798
+ // Allocate a zeroed GValue THROUGH the G_TYPE_VALUE boxed system —
799
+ // g_boxed_copy(G_TYPE_VALUE, &zero) slice-dups an all-zero G_VALUE_INIT struct into
800
+ // a fresh GValue (G_IS_VALUE(&zero) is false, so value_copy just dups the zeroed
801
+ // bytes) — so the handle's finalizer g_boxed_free(G_TYPE_VALUE, ...) frees it via
802
+ // the MATCHING allocator (value_free: g_value_unset + slice-free). The L1 layer
803
+ // (gi.js makeValueClass) wraps it with the GObject.Value ergonomics
804
+ // (.init/.set_*/.get_*/.copy/.unset via the boxed-method path). The Node twin of
805
+ // GJS's `new GObject.Value()` (refs/gjs/modules/core/overrides/GObject.js
806
+ // gValueConstructorFunc over realGValueClass).
807
+ Napi::Value NewGValue(const Napi::CallbackInfo& info) {
808
+ Napi::Env env = info.Env();
809
+ GValue zero = G_VALUE_INIT;
810
+ GValue* v = static_cast<GValue*>(g_boxed_copy(G_TYPE_VALUE, &zero));
811
+ return MakeBoxedHandle(env, v, G_TYPE_VALUE, true);
812
+ }
813
+
814
+ } // namespace nodegi