@gjsify/node-gi 0.13.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/addon.cc ADDED
@@ -0,0 +1,2110 @@
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, scaffold step): prove the toolchain + the modern
12
+ // GIRepository API end to end — resolve the default repository, require a
13
+ // namespace, read back its resolved version + introspection-info count, and
14
+ // enumerate the top-level info names. Marshalling, GObject/signals, the
15
+ // toggle-ref GC dance, and the mainloop bridge land in subsequent steps.
16
+
17
+ #include <napi.h>
18
+ #include <uv.h>
19
+
20
+ #include <girepository/girepository.h>
21
+ #include <girepository/girffi.h> // gi_callable_info_create_closure + ffi_cif
22
+ #include <glib-object.h>
23
+ #include <glib.h>
24
+
25
+ #include <string>
26
+ #include <vector>
27
+
28
+ namespace {
29
+
30
+ // Forward declaration: GetConstantValue (defined early, near the namespace
31
+ // helpers) marshals through GIArgumentToJs, whose definition lives further down
32
+ // with the rest of the value-marshalling boundary.
33
+ static Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
34
+ GITransfer transfer);
35
+
36
+ // gi_repository_dup_default() returns a new owning ref to the process-default
37
+ // GIRepository (lazily created). Callers must g_object_unref() it.
38
+ GIRepository* DupDefaultRepository() { return gi_repository_dup_default(); }
39
+
40
+ // requireNamespace(namespace: string, version?: string)
41
+ // -> { namespace: string, version: string, infoCount: number }
42
+ //
43
+ // Mirrors the load step every GJS gi:// / imports.gi access performs:
44
+ // gi_repository_require() with an optional pinned version (null = let
45
+ // GIRepository resolve the system default), then read back the resolved
46
+ // version and the top-level info count.
47
+ Napi::Value RequireNamespace(const Napi::CallbackInfo& info) {
48
+ Napi::Env env = info.Env();
49
+
50
+ if (info.Length() < 1 || !info[0].IsString()) {
51
+ Napi::TypeError::New(env, "requireNamespace(namespace: string, version?: string)")
52
+ .ThrowAsJavaScriptException();
53
+ return env.Null();
54
+ }
55
+
56
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
57
+ bool has_version = info.Length() >= 2 && info[1].IsString();
58
+ std::string version = has_version ? info[1].As<Napi::String>().Utf8Value() : std::string();
59
+
60
+ GIRepository* repo = DupDefaultRepository();
61
+ GError* error = nullptr;
62
+ GITypelib* typelib = gi_repository_require(
63
+ repo, ns.c_str(), has_version ? version.c_str() : nullptr,
64
+ GI_REPOSITORY_LOAD_FLAG_NONE, &error);
65
+
66
+ if (typelib == nullptr) {
67
+ std::string message = error != nullptr ? error->message : "unknown error";
68
+ if (error != nullptr) g_error_free(error);
69
+ g_object_unref(repo);
70
+ Napi::Error::New(env, "Failed to require " + ns +
71
+ (has_version ? (" " + version) : "") + ": " + message)
72
+ .ThrowAsJavaScriptException();
73
+ return env.Null();
74
+ }
75
+
76
+ const char* resolved = gi_repository_get_version(repo, ns.c_str());
77
+ unsigned int n_infos = gi_repository_get_n_infos(repo, ns.c_str());
78
+
79
+ Napi::Object result = Napi::Object::New(env);
80
+ result.Set("namespace", Napi::String::New(env, ns));
81
+ result.Set("version", Napi::String::New(env, resolved != nullptr ? resolved : ""));
82
+ result.Set("infoCount", Napi::Number::New(env, static_cast<double>(n_infos)));
83
+
84
+ g_object_unref(repo);
85
+ return result;
86
+ }
87
+
88
+ // listInfoNames(namespace: string) -> string[]
89
+ // Enumerates the top-level introspection infos of an already-required
90
+ // namespace. Proves gi_repository_get_info() + gi_base_info_get_name()/unref().
91
+ Napi::Value ListInfoNames(const Napi::CallbackInfo& info) {
92
+ Napi::Env env = info.Env();
93
+
94
+ if (info.Length() < 1 || !info[0].IsString()) {
95
+ Napi::TypeError::New(env, "listInfoNames(namespace: string)")
96
+ .ThrowAsJavaScriptException();
97
+ return env.Null();
98
+ }
99
+
100
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
101
+ GIRepository* repo = DupDefaultRepository();
102
+ unsigned int n_infos = gi_repository_get_n_infos(repo, ns.c_str());
103
+
104
+ Napi::Array names = Napi::Array::New(env, n_infos);
105
+ for (unsigned int i = 0; i < n_infos; i++) {
106
+ GIBaseInfo* base = gi_repository_get_info(repo, ns.c_str(), i);
107
+ const char* name = gi_base_info_get_name(base);
108
+ names.Set(i, Napi::String::New(env, name != nullptr ? name : ""));
109
+ gi_base_info_unref(base);
110
+ }
111
+
112
+ g_object_unref(repo);
113
+ return names;
114
+ }
115
+
116
+ // findInfo(namespace, name) -> { kind: string } | null
117
+ // Classify a top-level namespace member so the L1 wrapper knows whether to treat
118
+ // it as a constructible class, a callable function, an enum, a constant, etc.
119
+ // kind ∈ function|object|interface|struct|union|enum|flags|constant|callback|other.
120
+ Napi::Value FindInfo(const Napi::CallbackInfo& info) {
121
+ Napi::Env env = info.Env();
122
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
123
+ Napi::TypeError::New(env, "findInfo(namespace: string, name: string)")
124
+ .ThrowAsJavaScriptException();
125
+ return env.Null();
126
+ }
127
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
128
+ std::string name = info[1].As<Napi::String>().Utf8Value();
129
+ GIRepository* repo = DupDefaultRepository();
130
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
131
+ if (base == nullptr) {
132
+ g_object_unref(repo);
133
+ return env.Null();
134
+ }
135
+ // Order matters: GIFlagsInfo derives from GIEnumInfo, so test flags first.
136
+ const char* kind;
137
+ if (GI_IS_FUNCTION_INFO(base)) {
138
+ kind = "function";
139
+ } else if (GI_IS_FLAGS_INFO(base)) {
140
+ kind = "flags";
141
+ } else if (GI_IS_ENUM_INFO(base)) {
142
+ kind = "enum";
143
+ } else if (GI_IS_OBJECT_INFO(base)) {
144
+ kind = "object";
145
+ } else if (GI_IS_INTERFACE_INFO(base)) {
146
+ kind = "interface";
147
+ } else if (GI_IS_STRUCT_INFO(base)) {
148
+ kind = "struct";
149
+ } else if (GI_IS_UNION_INFO(base)) {
150
+ kind = "union";
151
+ } else if (GI_IS_CONSTANT_INFO(base)) {
152
+ kind = "constant";
153
+ } else if (GI_IS_CALLBACK_INFO(base)) {
154
+ kind = "callback";
155
+ } else {
156
+ kind = "other";
157
+ }
158
+ gi_base_info_unref(base);
159
+ g_object_unref(repo);
160
+ Napi::Object result = Napi::Object::New(env);
161
+ result.Set("kind", Napi::String::New(env, kind));
162
+ return result;
163
+ }
164
+
165
+ // getConstantValue(namespace, name) -> unknown
166
+ // Read a namespace-level GI constant (e.g. GLib.PRIORITY_DEFAULT) and marshal it
167
+ // to JS. The constant owns its storage, so we copy out (transfer NOTHING) then
168
+ // free it.
169
+ Napi::Value GetConstantValue(const Napi::CallbackInfo& info) {
170
+ Napi::Env env = info.Env();
171
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
172
+ Napi::TypeError::New(env, "getConstantValue(namespace: string, name: string)")
173
+ .ThrowAsJavaScriptException();
174
+ return env.Null();
175
+ }
176
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
177
+ std::string name = info[1].As<Napi::String>().Utf8Value();
178
+ GIRepository* repo = DupDefaultRepository();
179
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
180
+ if (base == nullptr || !GI_IS_CONSTANT_INFO(base)) {
181
+ if (base != nullptr) gi_base_info_unref(base);
182
+ g_object_unref(repo);
183
+ Napi::TypeError::New(env, ns + "." + name + " is not a constant")
184
+ .ThrowAsJavaScriptException();
185
+ return env.Null();
186
+ }
187
+ GIConstantInfo* ci = reinterpret_cast<GIConstantInfo*>(base);
188
+ GIArgument arg;
189
+ gi_constant_info_get_value(ci, &arg);
190
+ GITypeInfo* ti = gi_constant_info_get_type_info(ci);
191
+ // transfer NOTHING: GIArgumentToJs copies strings (Napi::String::New) rather
192
+ // than g_free'ing them; gi_constant_info_free_value owns the release.
193
+ Napi::Value result = GIArgumentToJs(env, ti, &arg, GI_TRANSFER_NOTHING);
194
+ gi_constant_info_free_value(ci, &arg);
195
+ gi_base_info_unref(ti);
196
+ gi_base_info_unref(base);
197
+ g_object_unref(repo);
198
+ return result;
199
+ }
200
+
201
+ // getEnumValues(namespace, name) -> Record<string, number>
202
+ // Enumerate an enum/flags type's members as { rawGiName: int }. The L1 wrapper
203
+ // re-keys them GJS-style (UPPER_CASE, '-' -> '_').
204
+ Napi::Value GetEnumValues(const Napi::CallbackInfo& info) {
205
+ Napi::Env env = info.Env();
206
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
207
+ Napi::TypeError::New(env, "getEnumValues(namespace: string, name: string)")
208
+ .ThrowAsJavaScriptException();
209
+ return env.Null();
210
+ }
211
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
212
+ std::string name = info[1].As<Napi::String>().Utf8Value();
213
+ GIRepository* repo = DupDefaultRepository();
214
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
215
+ if (base == nullptr || !GI_IS_ENUM_INFO(base)) {
216
+ if (base != nullptr) gi_base_info_unref(base);
217
+ g_object_unref(repo);
218
+ Napi::TypeError::New(env, ns + "." + name + " is not an enum or flags type")
219
+ .ThrowAsJavaScriptException();
220
+ return env.Null();
221
+ }
222
+ GIEnumInfo* ei = reinterpret_cast<GIEnumInfo*>(base);
223
+ unsigned int n = gi_enum_info_get_n_values(ei);
224
+ Napi::Object result = Napi::Object::New(env);
225
+ for (unsigned int i = 0; i < n; i++) {
226
+ GIValueInfo* vi = gi_enum_info_get_value(ei, i);
227
+ const char* vname = gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(vi));
228
+ int64_t val = gi_value_info_get_value(vi);
229
+ result.Set(vname != nullptr ? vname : "", Napi::Number::New(env, static_cast<double>(val)));
230
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(vi));
231
+ }
232
+ gi_base_info_unref(base);
233
+ g_object_unref(repo);
234
+ return result;
235
+ }
236
+
237
+ // prependSearchPath(path: string) -> void
238
+ // Lets a caller add a typelib search directory before requiring (the Node twin
239
+ // of GIRepository.prepend_search_path(); needed for non-system typelibs).
240
+ Napi::Value PrependSearchPath(const Napi::CallbackInfo& info) {
241
+ Napi::Env env = info.Env();
242
+ if (info.Length() < 1 || !info[0].IsString()) {
243
+ Napi::TypeError::New(env, "prependSearchPath(path: string)")
244
+ .ThrowAsJavaScriptException();
245
+ return env.Undefined();
246
+ }
247
+ std::string path = info[0].As<Napi::String>().Utf8Value();
248
+ GIRepository* repo = DupDefaultRepository();
249
+ gi_repository_prepend_search_path(repo, path.c_str());
250
+ g_object_unref(repo);
251
+ return env.Undefined();
252
+ }
253
+
254
+ // ---- value marshalling (milestone 1: primitives + strings) ----
255
+ //
256
+ // The minimal GIArgument <-> JS boundary. This is the seam node-gtk's value.cc
257
+ // fills out exhaustively; here it covers the numeric + string + boolean tags so
258
+ // real GI function calls work end to end. Compound tags (ARRAY/INTERFACE/GLIST/
259
+ // GHASH/…) and OUT/INOUT directions land with the GObject + full-marshalling
260
+ // drops.
261
+
262
+ // Marshal a JS value into a GIArgument for an IN argument of `type`.
263
+ // `heldString` keeps UTF-8 storage alive for the duration of the call.
264
+ // ---- boxed / struct handles (milestone: mainloop) ----
265
+ //
266
+ // Boxed/struct instances (e.g. GMainLoop) are wrapped as type-tagged Externals
267
+ // over a small heap record carrying the pointer + its boxed GType, so the
268
+ // finalizer can g_boxed_free a fully-owned boxed and method resolution can find
269
+ // the struct's GIStructInfo by GType. Distinct tag from the GObject handle so
270
+ // the two never cross-dereference. Full general struct support (field access,
271
+ // copy semantics for non-registered C structs) lands with the broader
272
+ // structs/boxed drop; this is the slice the GLib main loop needs.
273
+ struct BoxedHandle {
274
+ gpointer ptr;
275
+ GType gtype; // boxed GType, or G_TYPE_INVALID when unknown/non-registered
276
+ bool owns; // g_boxed_free(gtype, ptr) on finalize when true
277
+ };
278
+
279
+ static const napi_type_tag kBoxedHandleTag = {0x6d2f8c4b1a9e7350ULL,
280
+ 0xb7e1d3a5c9f08264ULL};
281
+
282
+ static Napi::Value MakeBoxedHandle(Napi::Env env, gpointer ptr, GType gtype, bool owns) {
283
+ BoxedHandle* bh = new BoxedHandle{ptr, gtype, owns};
284
+ Napi::External<BoxedHandle> ext =
285
+ Napi::External<BoxedHandle>::New(env, bh, [](Napi::Env, BoxedHandle* h) {
286
+ if (h->owns && h->ptr != nullptr && h->gtype != G_TYPE_INVALID &&
287
+ G_TYPE_IS_BOXED(h->gtype)) {
288
+ g_boxed_free(h->gtype, h->ptr);
289
+ }
290
+ delete h;
291
+ });
292
+ ext.TypeTag(&kBoxedHandleTag);
293
+ return ext;
294
+ }
295
+
296
+ // Wrap a returned struct/boxed pointer. `structInfo` is the GIStructInfo (or
297
+ // union info) for the static type; the runtime GType (if registered + boxed)
298
+ // drives ownership + later method resolution.
299
+ static Napi::Value WrapBoxed(Napi::Env env, gpointer ptr, GIBaseInfo* structInfo,
300
+ GITransfer transfer) {
301
+ if (ptr == nullptr) return env.Null();
302
+ GType gt = G_TYPE_INVALID;
303
+ if (structInfo != nullptr && (GI_IS_STRUCT_INFO(structInfo) || GI_IS_UNION_INFO(structInfo))) {
304
+ gt = gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(structInfo));
305
+ }
306
+ bool boxed = gt != G_TYPE_INVALID && gt != G_TYPE_NONE && G_TYPE_IS_BOXED(gt);
307
+ bool owns = boxed && transfer == GI_TRANSFER_EVERYTHING;
308
+ return MakeBoxedHandle(env, ptr, boxed ? gt : G_TYPE_INVALID, owns);
309
+ }
310
+
311
+ // Read a boxed handle's pointer if `v` is one (tag-checked; no deref of ptr).
312
+ static bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
313
+ if (!v.IsExternal()) return false;
314
+ Napi::External<BoxedHandle> ext = v.As<Napi::External<BoxedHandle>>();
315
+ if (!ext.CheckTypeTag(&kBoxedHandleTag)) return false;
316
+ BoxedHandle* h = ext.Data();
317
+ *out = h != nullptr ? h->ptr : nullptr;
318
+ return true;
319
+ }
320
+
321
+ static bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument* out,
322
+ std::string* heldString) {
323
+ GITypeTag tag = gi_type_info_get_tag(type);
324
+ switch (tag) {
325
+ case GI_TYPE_TAG_BOOLEAN: out->v_boolean = v.ToBoolean().Value(); return true;
326
+ case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(v.ToNumber().Int32Value()); return true;
327
+ case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(v.ToNumber().Uint32Value()); return true;
328
+ case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(v.ToNumber().Int32Value()); return true;
329
+ case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(v.ToNumber().Uint32Value()); return true;
330
+ case GI_TYPE_TAG_INT32: out->v_int32 = v.ToNumber().Int32Value(); return true;
331
+ case GI_TYPE_TAG_UINT32: out->v_uint32 = v.ToNumber().Uint32Value(); return true;
332
+ case GI_TYPE_TAG_INT64: out->v_int64 = v.ToNumber().Int64Value(); return true;
333
+ case GI_TYPE_TAG_UINT64: out->v_uint64 = static_cast<uint64_t>(v.ToNumber().Int64Value()); return true;
334
+ case GI_TYPE_TAG_FLOAT: out->v_float = static_cast<float>(v.ToNumber().DoubleValue()); return true;
335
+ case GI_TYPE_TAG_DOUBLE: out->v_double = v.ToNumber().DoubleValue(); return true;
336
+ case GI_TYPE_TAG_UTF8:
337
+ case GI_TYPE_TAG_FILENAME:
338
+ if (v.IsNull() || v.IsUndefined()) {
339
+ out->v_string = nullptr;
340
+ return true;
341
+ }
342
+ *heldString = v.ToString().Utf8Value();
343
+ out->v_string = const_cast<char*>(heldString->c_str());
344
+ return true;
345
+ case GI_TYPE_TAG_INTERFACE: {
346
+ // Object/interface instances arrive as opaque External<GObject> handles;
347
+ // enums/flags as plain numbers. Other interface kinds (structs/unions/
348
+ // callbacks) follow with the full-marshalling drop.
349
+ GIBaseInfo* iface = gi_type_info_get_interface(type);
350
+ bool handled = false;
351
+ if (iface != nullptr) {
352
+ if (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface)) {
353
+ if (v.IsNull() || v.IsUndefined()) {
354
+ out->v_pointer = nullptr;
355
+ handled = true;
356
+ } else if (v.IsExternal()) {
357
+ out->v_pointer = v.As<Napi::External<GObject>>().Data();
358
+ handled = true;
359
+ }
360
+ } else if (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface)) {
361
+ out->v_int = v.ToNumber().Int32Value();
362
+ handled = true;
363
+ } else if (GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface)) {
364
+ // Boxed/struct IN args arrive as boxed handles; null/undefined maps to
365
+ // a NULL pointer (e.g. GLib.MainLoop.new(null, false)).
366
+ if (v.IsNull() || v.IsUndefined()) {
367
+ out->v_pointer = nullptr;
368
+ handled = true;
369
+ } else {
370
+ gpointer p = nullptr;
371
+ if (TryGetBoxedPtr(v, &p)) {
372
+ out->v_pointer = p;
373
+ handled = true;
374
+ }
375
+ }
376
+ }
377
+ gi_base_info_unref(iface);
378
+ }
379
+ if (!handled) {
380
+ Napi::TypeError::New(
381
+ env,
382
+ "Unsupported interface IN argument (expected a GObject/boxed handle, enum or flags number)")
383
+ .ThrowAsJavaScriptException();
384
+ return false;
385
+ }
386
+ return true;
387
+ }
388
+ default:
389
+ Napi::TypeError::New(
390
+ env, "Unsupported IN argument type tag " + std::to_string(static_cast<int>(tag)) +
391
+ " (milestone 1 supports numbers, booleans and strings)")
392
+ .ThrowAsJavaScriptException();
393
+ return false;
394
+ }
395
+ }
396
+
397
+ // A process-unique type tag distinguishing node-gi's GObject-instance Externals
398
+ // from other Externals (notably registerClass's GType handle). isGObjectHandle /
399
+ // UnwrapGObject validate against it WITHOUT dereferencing the pointer — a GType
400
+ // handle holds a small integer, not a valid GObject*, so a blind G_IS_OBJECT on
401
+ // it would segfault.
402
+ static const napi_type_tag kGObjectHandleTag = {0x9f3c1a7b5e2d4068ULL,
403
+ 0xa1b2c3d4e5f60718ULL};
404
+
405
+ // Create the owned, type-tagged External that represents a live GObject. The
406
+ // finalizer drops the single ref we hold; N-API finalizers run outside GC, so
407
+ // the unref (and any resulting GObject finalize) is safe here.
408
+ static Napi::Value MakeGObjectHandle(Napi::Env env, GObject* obj) {
409
+ Napi::External<GObject> ext = Napi::External<GObject>::New(
410
+ env, obj, [](Napi::Env, GObject* ptr) { g_object_unref(ptr); });
411
+ ext.TypeTag(&kGObjectHandleTag);
412
+ return ext;
413
+ }
414
+
415
+ // Wrap a borrowed/owned GObject pointer as a node-gi handle. Floating refs are
416
+ // sunk; a transfer-none borrow is ref'd so the finalizer has a ref to drop.
417
+ static Napi::Value WrapGObject(Napi::Env env, GObject* obj, GITransfer transfer) {
418
+ if (obj == nullptr) return env.Null();
419
+ if (g_object_is_floating(obj)) {
420
+ g_object_ref_sink(obj);
421
+ } else if (transfer == GI_TRANSFER_NOTHING) {
422
+ g_object_ref(obj);
423
+ }
424
+ return MakeGObjectHandle(env, obj);
425
+ }
426
+
427
+ // Marshal a return-value GIArgument into a JS value, honouring transfer.
428
+ static Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
429
+ GITransfer transfer) {
430
+ GITypeTag tag = gi_type_info_get_tag(type);
431
+ switch (tag) {
432
+ case GI_TYPE_TAG_VOID: return env.Undefined();
433
+ case GI_TYPE_TAG_BOOLEAN: return Napi::Boolean::New(env, arg->v_boolean);
434
+ case GI_TYPE_TAG_INT8: return Napi::Number::New(env, arg->v_int8);
435
+ case GI_TYPE_TAG_UINT8: return Napi::Number::New(env, arg->v_uint8);
436
+ case GI_TYPE_TAG_INT16: return Napi::Number::New(env, arg->v_int16);
437
+ case GI_TYPE_TAG_UINT16: return Napi::Number::New(env, arg->v_uint16);
438
+ case GI_TYPE_TAG_INT32: return Napi::Number::New(env, arg->v_int32);
439
+ case GI_TYPE_TAG_UINT32: return Napi::Number::New(env, arg->v_uint32);
440
+ case GI_TYPE_TAG_INT64: return Napi::Number::New(env, static_cast<double>(arg->v_int64));
441
+ case GI_TYPE_TAG_UINT64: return Napi::Number::New(env, static_cast<double>(arg->v_uint64));
442
+ case GI_TYPE_TAG_FLOAT: return Napi::Number::New(env, arg->v_float);
443
+ case GI_TYPE_TAG_DOUBLE: return Napi::Number::New(env, arg->v_double);
444
+ case GI_TYPE_TAG_UTF8:
445
+ case GI_TYPE_TAG_FILENAME: {
446
+ if (arg->v_string == nullptr) return env.Null();
447
+ Napi::Value str = Napi::String::New(env, arg->v_string);
448
+ if (transfer == GI_TRANSFER_EVERYTHING) g_free(arg->v_string);
449
+ return str;
450
+ }
451
+ case GI_TYPE_TAG_INTERFACE: {
452
+ GIBaseInfo* iface = gi_type_info_get_interface(type);
453
+ Napi::Value result;
454
+ if (iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface))) {
455
+ result = WrapGObject(env, static_cast<GObject*>(arg->v_pointer), transfer);
456
+ } else if (iface != nullptr && (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface))) {
457
+ result = Napi::Number::New(env, arg->v_int);
458
+ } else if (iface != nullptr && (GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface))) {
459
+ result = WrapBoxed(env, arg->v_pointer, iface, transfer);
460
+ } else {
461
+ Napi::TypeError::New(
462
+ env,
463
+ "Unsupported interface return type (milestone: objects, interfaces, enums, flags, boxed)")
464
+ .ThrowAsJavaScriptException();
465
+ result = env.Undefined();
466
+ }
467
+ if (iface != nullptr) gi_base_info_unref(iface);
468
+ return result;
469
+ }
470
+ default:
471
+ Napi::TypeError::New(env, "Unsupported return type tag " +
472
+ std::to_string(static_cast<int>(tag)) + " (milestone 1)")
473
+ .ThrowAsJavaScriptException();
474
+ return env.Undefined();
475
+ }
476
+ }
477
+
478
+ // Shared invocation core: marshal the JS args into a GIArgument vector (the
479
+ // instance prepended for methods), call gi_function_info_invoke, marshal the
480
+ // return. IN-only primitives/strings/objects/enums today; OUT/INOUT follow.
481
+ // ---- callbacks (JS function -> GI callback via an ffi closure) ----
482
+ //
483
+ // A JS function passed where a GI callback is expected (e.g. GLib.timeout_add's
484
+ // GSourceFunc) is wrapped in an ffi closure created by girepository
485
+ // (gi_callable_info_create_closure). When the C library invokes it, the
486
+ // trampoline marshals the C args to JS, calls the function, and marshals the
487
+ // return back. The function's associated user_data slot carries the wrapper
488
+ // pointer and the destroy-notify slot frees it (scope = notified); call-scope
489
+ // closures are freed by the caller after the invoke; async closures free
490
+ // themselves after the first call. Reference: refs/node-gtk src/callback.cc.
491
+ struct NodeGiCallback {
492
+ napi_env env;
493
+ napi_ref jsFn;
494
+ GICallableInfo* info; // the callback type (owned)
495
+ ffi_cif cif;
496
+ ffi_closure* closure; // from gi_callable_info_create_closure
497
+ gpointer native; // executable trampoline address
498
+ GIScopeType scope;
499
+ };
500
+
501
+ static void NodeGiCallbackFree(NodeGiCallback* cb) {
502
+ if (cb == nullptr) return;
503
+ if (cb->jsFn != nullptr) napi_delete_reference(cb->env, cb->jsFn);
504
+ if (cb->closure != nullptr) gi_callable_info_destroy_closure(cb->info, cb->closure);
505
+ if (cb->info != nullptr) gi_base_info_unref(cb->info);
506
+ delete cb;
507
+ }
508
+
509
+ // GDestroyNotify for scope=notified callbacks; user_data is the NodeGiCallback*.
510
+ static void NodeGiCallbackDestroyNotify(gpointer user_data) {
511
+ NodeGiCallbackFree(static_cast<NodeGiCallback*>(user_data));
512
+ }
513
+
514
+ // girepository-2.0's closure/destroy index getters are out-param + gboolean;
515
+ // return the index or -1 when the arg has no associated slot.
516
+ static int ArgClosureIndex(GIArgInfo* ai) {
517
+ unsigned int idx = 0;
518
+ return gi_arg_info_get_closure_index(ai, &idx) ? static_cast<int>(idx) : -1;
519
+ }
520
+ static int ArgDestroyIndex(GIArgInfo* ai) {
521
+ unsigned int idx = 0;
522
+ return gi_arg_info_get_destroy_index(ai, &idx) ? static_cast<int>(idx) : -1;
523
+ }
524
+
525
+ // The ffi closure entry point: marshal C args -> JS, call the JS fn, marshal the
526
+ // JS return -> C. Runs on the main thread (the GLib loop the bridge pumps).
527
+ static void NodeGiCallbackTrampoline(ffi_cif* /*cif*/, void* result, void** args,
528
+ gpointer user_data) {
529
+ NodeGiCallback* cb = static_cast<NodeGiCallback*>(user_data);
530
+ napi_env env = cb->env;
531
+ Napi::Env napiEnv(env);
532
+ Napi::HandleScope scope(napiEnv);
533
+
534
+ GICallableInfo* ci = cb->info;
535
+ unsigned int n = gi_callable_info_get_n_args(ci);
536
+ std::vector<napi_value> jsArgs;
537
+ jsArgs.reserve(n);
538
+ bool ok = true;
539
+ for (unsigned int i = 0; i < n; i++) {
540
+ GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
541
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
542
+ // ffi hands each argument's storage as args[i]; reinterpret as a GIArgument
543
+ // union (a user_data/void arg marshals to undefined, which JS ignores).
544
+ Napi::Value v = GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i]), GI_TRANSFER_NOTHING);
545
+ gi_base_info_unref(ti);
546
+ gi_base_info_unref(ai);
547
+ if (napiEnv.IsExceptionPending()) {
548
+ ok = false;
549
+ break;
550
+ }
551
+ jsArgs.push_back(v);
552
+ }
553
+
554
+ // Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
555
+ // upper bytes indeterminate otherwise).
556
+ if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
557
+
558
+ GITypeInfo* retType = gi_callable_info_get_return_type(ci);
559
+ if (ok) {
560
+ napi_value fn = nullptr;
561
+ if (napi_get_reference_value(env, cb->jsFn, &fn) == napi_ok && fn != nullptr) {
562
+ napi_value global = nullptr;
563
+ napi_get_global(env, &global);
564
+ napi_value ret = nullptr;
565
+ // napi_make_callback drains nextTick/microtasks around the call.
566
+ napi_status st = napi_make_callback(env, nullptr, global, fn, jsArgs.size(), jsArgs.data(), &ret);
567
+ if (st == napi_ok && result != nullptr) {
568
+ GITypeTag rtag = gi_type_info_get_tag(retType);
569
+ if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
570
+ // Hand the caller an owned copy — a JsToGIArgument string would point
571
+ // into a std::string that dies with this frame.
572
+ Napi::Value rv(env, ret);
573
+ static_cast<GIArgument*>(result)->v_string =
574
+ rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
575
+ } else if (rtag != GI_TYPE_TAG_VOID) {
576
+ std::string held;
577
+ JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
578
+ &held);
579
+ }
580
+ }
581
+ }
582
+ }
583
+ gi_base_info_unref(retType);
584
+ // A pending JS exception surfaces at the next N-API boundary (e.g. when the
585
+ // blocking run() that pumped this callback returns).
586
+ }
587
+
588
+ // Create an ffi closure wrapping a JS function for a GI callback-typed arg.
589
+ static NodeGiCallback* CreateCallback(Napi::Env env, Napi::Function fn, GICallableInfo* callbackInfo,
590
+ GIScopeType scope) {
591
+ NodeGiCallback* cb = new NodeGiCallback();
592
+ cb->env = env;
593
+ cb->info = reinterpret_cast<GICallableInfo*>(gi_base_info_ref(callbackInfo));
594
+ cb->scope = scope;
595
+ napi_create_reference(env, fn, 1, &cb->jsFn);
596
+ cb->closure = gi_callable_info_create_closure(callbackInfo, &cb->cif, NodeGiCallbackTrampoline, cb);
597
+ cb->native = gi_callable_info_get_closure_native_address(callbackInfo, cb->closure);
598
+ if (cb->native == nullptr) cb->native = cb->closure;
599
+ return cb;
600
+ }
601
+
602
+ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpointer instance,
603
+ Napi::Array args, const std::string& displayName) {
604
+ GICallableInfo* callable = reinterpret_cast<GICallableInfo*>(func);
605
+ unsigned int n_args = gi_callable_info_get_n_args(callable);
606
+ bool isMethod = instance != nullptr;
607
+ size_t offset = isMethod ? 1 : 0;
608
+ std::vector<GIArgument> in_args(n_args + offset);
609
+ std::vector<std::string> held(n_args);
610
+ if (isMethod) in_args[0].v_pointer = instance;
611
+
612
+ // Pre-scan: the user_data + destroy-notify slots associated with a callback
613
+ // arg are filled from the callback, not consumed from the JS argument list.
614
+ std::vector<bool> skip(n_args, false);
615
+ for (unsigned int i = 0; i < n_args; i++) {
616
+ GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
617
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
618
+ if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE) {
619
+ GIBaseInfo* iface = gi_type_info_get_interface(ti);
620
+ if (iface != nullptr && GI_IS_CALLBACK_INFO(iface)) {
621
+ int ci = ArgClosureIndex(ai);
622
+ int di = ArgDestroyIndex(ai);
623
+ if (ci >= 0 && static_cast<unsigned int>(ci) < n_args) skip[ci] = true;
624
+ if (di >= 0 && static_cast<unsigned int>(di) < n_args) skip[di] = true;
625
+ }
626
+ if (iface != nullptr) gi_base_info_unref(iface);
627
+ }
628
+ gi_base_info_unref(ti);
629
+ gi_base_info_unref(ai);
630
+ }
631
+
632
+ std::vector<NodeGiCallback*> created; // all callbacks made this call
633
+ std::vector<NodeGiCallback*> callScope; // scope=call → freed after the invoke
634
+ bool ok = true;
635
+ size_t jsCursor = 0;
636
+ for (unsigned int i = 0; i < n_args && ok; i++) {
637
+ if (skip[i]) continue; // a user_data / destroy slot — set via its callback
638
+ GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
639
+ GIDirection dir = gi_arg_info_get_direction(ai);
640
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
641
+ Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
642
+ jsCursor++;
643
+
644
+ GIBaseInfo* iface = gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE
645
+ ? gi_type_info_get_interface(ti)
646
+ : nullptr;
647
+ bool isCallback = iface != nullptr && GI_IS_CALLBACK_INFO(iface);
648
+
649
+ if (dir != GI_DIRECTION_IN && !isCallback) {
650
+ if (iface != nullptr) gi_base_info_unref(iface);
651
+ gi_base_info_unref(ti);
652
+ gi_base_info_unref(ai);
653
+ Napi::TypeError::New(env, displayName + ": OUT/INOUT parameters are not yet supported")
654
+ .ThrowAsJavaScriptException();
655
+ ok = false;
656
+ break;
657
+ }
658
+
659
+ if (isCallback) {
660
+ int ci = ArgClosureIndex(ai);
661
+ int di = ArgDestroyIndex(ai);
662
+ if (v.IsFunction()) {
663
+ GIScopeType scopeType = gi_arg_info_get_scope(ai);
664
+ NodeGiCallback* cb =
665
+ CreateCallback(env, v.As<Napi::Function>(), reinterpret_cast<GICallableInfo*>(iface),
666
+ scopeType);
667
+ created.push_back(cb);
668
+ if (scopeType == GI_SCOPE_TYPE_CALL) callScope.push_back(cb);
669
+ in_args[offset + i].v_pointer = cb->native;
670
+ if (ci >= 0 && static_cast<unsigned int>(ci) < n_args)
671
+ in_args[offset + ci].v_pointer = cb;
672
+ if (di >= 0 && static_cast<unsigned int>(di) < n_args)
673
+ in_args[offset + di].v_pointer = reinterpret_cast<gpointer>(NodeGiCallbackDestroyNotify);
674
+ } else if (v.IsNull() || v.IsUndefined()) {
675
+ in_args[offset + i].v_pointer = nullptr;
676
+ } else {
677
+ gi_base_info_unref(iface);
678
+ gi_base_info_unref(ti);
679
+ gi_base_info_unref(ai);
680
+ Napi::TypeError::New(env, displayName + ": expected a function for the callback argument")
681
+ .ThrowAsJavaScriptException();
682
+ ok = false;
683
+ break;
684
+ }
685
+ } else {
686
+ ok = JsToGIArgument(env, v, ti, &in_args[offset + i], &held[i]);
687
+ }
688
+
689
+ if (iface != nullptr) gi_base_info_unref(iface);
690
+ gi_base_info_unref(ti);
691
+ gi_base_info_unref(ai);
692
+ }
693
+ if (!ok) {
694
+ for (NodeGiCallback* cb : created) NodeGiCallbackFree(cb);
695
+ return env.Null();
696
+ }
697
+
698
+ GIArgument retval;
699
+ GError* error = nullptr;
700
+ gboolean success =
701
+ gi_function_info_invoke(func, in_args.data(), in_args.size(), nullptr, 0, &retval, &error);
702
+ // Call-scope closures are only valid for the duration of the invoke; free them
703
+ // now (notified/async closures are owned by the callee / self-freeing).
704
+ for (NodeGiCallback* cb : callScope) NodeGiCallbackFree(cb);
705
+ if (!success) {
706
+ std::string message = error != nullptr ? error->message : "invocation failed";
707
+ if (error != nullptr) g_error_free(error);
708
+ Napi::Error::New(env, "Calling " + displayName + ": " + message).ThrowAsJavaScriptException();
709
+ return env.Null();
710
+ }
711
+
712
+ GITypeInfo* return_type = gi_callable_info_get_return_type(callable);
713
+ GITransfer return_transfer = gi_callable_info_get_caller_owns(callable);
714
+ Napi::Value result = GIArgumentToJs(env, return_type, &retval, return_transfer);
715
+ gi_base_info_unref(return_type);
716
+ return result;
717
+ }
718
+
719
+ // callFunction(namespace, functionName, args?: unknown[]) -> unknown
720
+ // Invokes a namespace-level GI function (not an instance method) with IN-only
721
+ // primitive/string/object args. Instance methods go through callMethod.
722
+ Napi::Value CallFunction(const Napi::CallbackInfo& info) {
723
+ Napi::Env env = info.Env();
724
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
725
+ Napi::TypeError::New(env, "callFunction(namespace: string, functionName: string, args?: unknown[])")
726
+ .ThrowAsJavaScriptException();
727
+ return env.Null();
728
+ }
729
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
730
+ std::string fn = info[1].As<Napi::String>().Utf8Value();
731
+ Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
732
+ : Napi::Array::New(env, 0);
733
+
734
+ GIRepository* repo = DupDefaultRepository();
735
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), fn.c_str());
736
+ if (base == nullptr) {
737
+ g_object_unref(repo);
738
+ Napi::Error::New(env, "No such symbol: " + ns + "." + fn).ThrowAsJavaScriptException();
739
+ return env.Null();
740
+ }
741
+ if (!GI_IS_FUNCTION_INFO(base)) {
742
+ gi_base_info_unref(base);
743
+ g_object_unref(repo);
744
+ Napi::TypeError::New(env, ns + "." + fn + " is not a function").ThrowAsJavaScriptException();
745
+ return env.Null();
746
+ }
747
+ GIFunctionInfo* func = reinterpret_cast<GIFunctionInfo*>(base);
748
+ if (gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(base))) {
749
+ gi_base_info_unref(base);
750
+ g_object_unref(repo);
751
+ Napi::TypeError::New(
752
+ env, ns + "." + fn + " is an instance method — use callMethod(handle, name, args)")
753
+ .ThrowAsJavaScriptException();
754
+ return env.Null();
755
+ }
756
+
757
+ Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + fn);
758
+ gi_base_info_unref(base);
759
+ g_object_unref(repo);
760
+ return result;
761
+ }
762
+
763
+ // ---- GObject lifecycle + properties (milestone 1) ----
764
+ //
765
+ // Construct GObjects, read/write their properties, and own them with an N-API
766
+ // finalizer. Unlike node-gtk's NAN/V8-weak-callback model (which must defer the
767
+ // unref to a GLib idle because V8 forbids calling into GObject during GC),
768
+ // N-API finalizers run at a safe point OUTSIDE garbage collection, so a plain
769
+ // g_object_unref in the finalizer is correct here. The toggle-ref dance is only
770
+ // needed once JS-subclassed objects / JS-connected signal closures enter the
771
+ // picture (the signals + registerClass drops); a plain owned ref suffices for
772
+ // the headless-core object lifecycle.
773
+ //
774
+ // Instances are handed back as opaque Napi::External<GObject> handles; the
775
+ // ergonomic class/prototype surface is layered in the GJS-compat runtime.
776
+
777
+ // Marshal a GValue into a JS value (fundamental types).
778
+ static Napi::Value GValueToJs(Napi::Env env, const GValue* v) {
779
+ GType ft = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(v));
780
+ switch (ft) {
781
+ case G_TYPE_BOOLEAN: return Napi::Boolean::New(env, g_value_get_boolean(v));
782
+ case G_TYPE_CHAR: return Napi::Number::New(env, g_value_get_schar(v));
783
+ case G_TYPE_UCHAR: return Napi::Number::New(env, g_value_get_uchar(v));
784
+ case G_TYPE_INT: return Napi::Number::New(env, g_value_get_int(v));
785
+ case G_TYPE_UINT: return Napi::Number::New(env, g_value_get_uint(v));
786
+ case G_TYPE_LONG: return Napi::Number::New(env, static_cast<double>(g_value_get_long(v)));
787
+ case G_TYPE_ULONG: return Napi::Number::New(env, static_cast<double>(g_value_get_ulong(v)));
788
+ case G_TYPE_INT64: return Napi::Number::New(env, static_cast<double>(g_value_get_int64(v)));
789
+ case G_TYPE_UINT64: return Napi::Number::New(env, static_cast<double>(g_value_get_uint64(v)));
790
+ case G_TYPE_FLOAT: return Napi::Number::New(env, g_value_get_float(v));
791
+ case G_TYPE_DOUBLE: return Napi::Number::New(env, g_value_get_double(v));
792
+ case G_TYPE_ENUM: return Napi::Number::New(env, g_value_get_enum(v));
793
+ case G_TYPE_FLAGS: return Napi::Number::New(env, g_value_get_flags(v));
794
+ case G_TYPE_STRING: {
795
+ const char* s = g_value_get_string(v);
796
+ return s != nullptr ? Napi::Value(Napi::String::New(env, s)) : env.Null();
797
+ }
798
+ case G_TYPE_OBJECT:
799
+ // Signal/property object values are transfer-none borrows; WrapGObject refs.
800
+ return WrapGObject(env, static_cast<GObject*>(g_value_get_object(v)), GI_TRANSFER_NOTHING);
801
+ case G_TYPE_PARAM: {
802
+ // GParamSpec (e.g. the `notify` signal argument) — surface the changed
803
+ // property's name so a `notify` handler can read it.
804
+ GParamSpec* p = g_value_get_param(v);
805
+ if (p == nullptr) return env.Null();
806
+ Napi::Object o = Napi::Object::New(env);
807
+ o.Set("name", Napi::String::New(env, p->name));
808
+ o.Set("valueType", Napi::String::New(env, g_type_name(p->value_type)));
809
+ return o;
810
+ }
811
+ default:
812
+ Napi::TypeError::New(env, std::string("Unsupported property GType ") +
813
+ g_type_name(G_VALUE_TYPE(v)) + " (milestone 1: fundamentals only)")
814
+ .ThrowAsJavaScriptException();
815
+ return env.Undefined();
816
+ }
817
+ }
818
+
819
+ // Marshal a JS value into an already-g_value_init'd GValue.
820
+ static bool JsToGValue(Napi::Env env, Napi::Value js, GValue* v) {
821
+ GType ft = G_TYPE_FUNDAMENTAL(G_VALUE_TYPE(v));
822
+ switch (ft) {
823
+ case G_TYPE_BOOLEAN: g_value_set_boolean(v, js.ToBoolean().Value()); return true;
824
+ case G_TYPE_CHAR: g_value_set_schar(v, static_cast<gint8>(js.ToNumber().Int32Value())); return true;
825
+ case G_TYPE_UCHAR: g_value_set_uchar(v, static_cast<guchar>(js.ToNumber().Uint32Value())); return true;
826
+ case G_TYPE_INT: g_value_set_int(v, js.ToNumber().Int32Value()); return true;
827
+ case G_TYPE_UINT: g_value_set_uint(v, js.ToNumber().Uint32Value()); return true;
828
+ case G_TYPE_LONG: g_value_set_long(v, static_cast<glong>(js.ToNumber().Int64Value())); return true;
829
+ case G_TYPE_ULONG: g_value_set_ulong(v, static_cast<gulong>(js.ToNumber().Int64Value())); return true;
830
+ case G_TYPE_INT64: g_value_set_int64(v, js.ToNumber().Int64Value()); return true;
831
+ case G_TYPE_UINT64: g_value_set_uint64(v, static_cast<guint64>(js.ToNumber().Int64Value())); return true;
832
+ case G_TYPE_FLOAT: g_value_set_float(v, static_cast<float>(js.ToNumber().DoubleValue())); return true;
833
+ case G_TYPE_DOUBLE: g_value_set_double(v, js.ToNumber().DoubleValue()); return true;
834
+ case G_TYPE_ENUM: g_value_set_enum(v, js.ToNumber().Int32Value()); return true;
835
+ case G_TYPE_FLAGS: g_value_set_flags(v, js.ToNumber().Uint32Value()); return true;
836
+ case G_TYPE_STRING:
837
+ if (js.IsNull() || js.IsUndefined()) {
838
+ g_value_set_string(v, nullptr);
839
+ } else {
840
+ std::string s = js.ToString().Utf8Value();
841
+ g_value_set_string(v, s.c_str()); // g_value_set_string copies
842
+ }
843
+ return true;
844
+ default:
845
+ Napi::TypeError::New(env, std::string("Unsupported property GType ") +
846
+ g_type_name(G_VALUE_TYPE(v)))
847
+ .ThrowAsJavaScriptException();
848
+ return false;
849
+ }
850
+ }
851
+
852
+ static GObject* UnwrapGObject(Napi::Env env, Napi::Value handle) {
853
+ // Tag-check before touching Data() — a GType handle (registerClass) is also an
854
+ // External but holds a non-dereferenceable integer; G_IS_OBJECT on it crashes.
855
+ if (!handle.IsExternal() ||
856
+ !handle.As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag)) {
857
+ Napi::TypeError::New(env, "expected a node-gi GObject handle").ThrowAsJavaScriptException();
858
+ return nullptr;
859
+ }
860
+ GObject* obj = handle.As<Napi::External<GObject>>().Data();
861
+ if (obj == nullptr || !G_IS_OBJECT(obj)) {
862
+ Napi::TypeError::New(env, "invalid GObject handle").ThrowAsJavaScriptException();
863
+ return nullptr;
864
+ }
865
+ return obj;
866
+ }
867
+
868
+ // Shared constructor core: realise the class, marshal `props` into a GValue
869
+ // vector against the type's (inherited) GParamSpecs, g_object_new_with_properties,
870
+ // and hand back an owned External<GObject>. Used by both newObject (resolve by
871
+ // namespace.typeName) and constructType (resolve by a registered GType handle).
872
+ static Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
873
+ const std::string& displayName) {
874
+ Napi::Array names = props.GetPropertyNames();
875
+ guint n = names.Length();
876
+ std::vector<GValue> values(n); // zero-initialised == G_VALUE_INIT
877
+ std::vector<std::string> nameStorage(n);
878
+ std::vector<const char*> cnames(n);
879
+
880
+ gpointer klass = g_type_class_ref(gtype); // realises the class so pspecs exist
881
+ guint initialised = 0;
882
+ bool ok = true;
883
+ for (guint i = 0; i < n; i++) {
884
+ nameStorage[i] = names.Get(i).ToString().Utf8Value();
885
+ GParamSpec* pspec =
886
+ g_object_class_find_property(reinterpret_cast<GObjectClass*>(klass), nameStorage[i].c_str());
887
+ if (pspec == nullptr) {
888
+ Napi::TypeError::New(env, displayName + " has no property '" + nameStorage[i] + "'")
889
+ .ThrowAsJavaScriptException();
890
+ ok = false;
891
+ break;
892
+ }
893
+ g_value_init(&values[i], pspec->value_type);
894
+ initialised = i + 1;
895
+ if (!JsToGValue(env, props.Get(nameStorage[i]), &values[i])) {
896
+ ok = false;
897
+ break;
898
+ }
899
+ cnames[i] = nameStorage[i].c_str();
900
+ }
901
+
902
+ GObject* obj = nullptr;
903
+ if (ok) {
904
+ obj = g_object_new_with_properties(gtype, n, cnames.data(), values.data());
905
+ }
906
+ for (guint j = 0; j < initialised; j++) g_value_unset(&values[j]);
907
+ g_type_class_unref(klass);
908
+
909
+ if (!ok) return env.Null();
910
+ if (obj == nullptr) {
911
+ Napi::Error::New(env, "Failed to construct " + displayName).ThrowAsJavaScriptException();
912
+ return env.Null();
913
+ }
914
+ // Take a single strong, non-floating ref; the finalizer releases it.
915
+ if (g_object_is_floating(obj)) {
916
+ g_object_ref_sink(obj);
917
+ }
918
+ return MakeGObjectHandle(env, obj);
919
+ }
920
+
921
+ // newObject(namespace, typeName, props?: Record<string, unknown>) -> External<GObject>
922
+ Napi::Value NewObject(const Napi::CallbackInfo& info) {
923
+ Napi::Env env = info.Env();
924
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
925
+ Napi::TypeError::New(env, "newObject(namespace: string, typeName: string, props?: object)")
926
+ .ThrowAsJavaScriptException();
927
+ return env.Null();
928
+ }
929
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
930
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
931
+ Napi::Object props =
932
+ (info.Length() >= 3 && info[2].IsObject()) ? info[2].As<Napi::Object>() : Napi::Object::New(env);
933
+
934
+ GIRepository* repo = DupDefaultRepository();
935
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
936
+ bool isObject = base != nullptr && GI_IS_OBJECT_INFO(base);
937
+ GType gtype = isObject
938
+ ? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base))
939
+ : G_TYPE_INVALID;
940
+ if (base != nullptr) gi_base_info_unref(base);
941
+ g_object_unref(repo);
942
+ if (!isObject || gtype == G_TYPE_INVALID || gtype == G_TYPE_NONE) {
943
+ Napi::TypeError::New(env, ns + "." + tn + " is not a constructible GObject type")
944
+ .ThrowAsJavaScriptException();
945
+ return env.Null();
946
+ }
947
+
948
+ return ConstructGObject(env, gtype, props, ns + "." + tn);
949
+ }
950
+
951
+ // ---- subclassing (registerClass — minimal: subtype + construct) ----
952
+ //
953
+ // Register a new GObject subclass of `parentNamespace.parentTypeName` named
954
+ // `name`, inheriting the parent's class/instance layout. Supports custom
955
+ // properties + signals (installed in class_init — see below); vfunc overrides
956
+ // and the toggle-ref GC bridge a JS vfunc/closure requires land in the next
957
+ // drop. A plain dynamic subtype's instances are ordinary GObjects, so the
958
+ // existing finalizer-unref ownership is correct. Returns an opaque type handle
959
+ // (the GType) for constructType().
960
+
961
+ // GTypes are process-stable and never freed (static registration), so the
962
+ // handle carries no finalizer.
963
+ static GType UnwrapGType(Napi::Env env, Napi::Value v) {
964
+ if (!v.IsExternal()) {
965
+ Napi::TypeError::New(env, "expected a node-gi type handle from registerClass()")
966
+ .ThrowAsJavaScriptException();
967
+ return 0;
968
+ }
969
+ return reinterpret_cast<GType>(v.As<Napi::External<void>>().Data());
970
+ }
971
+
972
+ // ---- registerClass custom properties + signals (class_init) ----
973
+ //
974
+ // A registered subclass can declare custom GObject properties and signals.
975
+ // In class_init we install the GParamSpecs + override get/set_property (routing
976
+ // custom props to a per-instance value store) and g_signal_newv each signal.
977
+ // Inherited (introspected-parent) properties still flow through the parent's
978
+ // vfuncs — a property is "ours" iff its owner GType carries node-gi class-data.
979
+ // Backing the values with a per-instance store (not C struct fields) keeps a
980
+ // plain dynamic subtype's instances ordinary GObjects (the existing
981
+ // finalizer-unref ownership stays correct). vfunc overrides + the toggle-ref GC
982
+ // bridge a JS method/closure needs land in the next drop.
983
+
984
+ // Map a JS type-name to a GType (shared by property + signal specs).
985
+ static GType TypeNameToGType(const std::string& t) {
986
+ if (t == "string" || t == "utf8") return G_TYPE_STRING;
987
+ if (t == "boolean" || t == "bool") return G_TYPE_BOOLEAN;
988
+ if (t == "int") return G_TYPE_INT;
989
+ if (t == "uint") return G_TYPE_UINT;
990
+ if (t == "int64") return G_TYPE_INT64;
991
+ if (t == "uint64") return G_TYPE_UINT64;
992
+ if (t == "double") return G_TYPE_DOUBLE;
993
+ if (t == "float") return G_TYPE_FLOAT;
994
+ if (t == "object") return G_TYPE_OBJECT;
995
+ if (t == "void" || t == "none") return G_TYPE_NONE;
996
+ return G_TYPE_INVALID;
997
+ }
998
+
999
+ // Build a floating GParamSpec from a JS spec `{ name, type, flags?, default?,
1000
+ // minimum?, maximum? }`. Returns nullptr + sets *err on an unsupported type.
1001
+ static GParamSpec* BuildParamSpec(Napi::Env env, Napi::Object spec, std::string* err) {
1002
+ if (!spec.Has("name") || !spec.Get("name").IsString()) {
1003
+ *err = "property requires a string 'name'";
1004
+ return nullptr;
1005
+ }
1006
+ std::string name = spec.Get("name").As<Napi::String>().Utf8Value();
1007
+ std::string type = (spec.Has("type") && spec.Get("type").IsString())
1008
+ ? spec.Get("type").As<Napi::String>().Utf8Value()
1009
+ : std::string("string");
1010
+ GParamFlags flags = (spec.Has("flags") && spec.Get("flags").IsNumber())
1011
+ ? static_cast<GParamFlags>(spec.Get("flags").As<Napi::Number>().Int32Value())
1012
+ : G_PARAM_READWRITE;
1013
+ Napi::Value def = spec.Get("default");
1014
+ bool hasMin = spec.Has("minimum") && spec.Get("minimum").IsNumber();
1015
+ bool hasMax = spec.Has("maximum") && spec.Get("maximum").IsNumber();
1016
+ double mn = hasMin ? spec.Get("minimum").As<Napi::Number>().DoubleValue() : 0;
1017
+ double mx = hasMax ? spec.Get("maximum").As<Napi::Number>().DoubleValue() : 0;
1018
+ const char* nm = name.c_str();
1019
+
1020
+ if (type == "string" || type == "utf8") {
1021
+ std::string d = def.IsString() ? def.As<Napi::String>().Utf8Value() : std::string();
1022
+ return g_param_spec_string(nm, nm, nm, def.IsString() ? d.c_str() : nullptr, flags);
1023
+ }
1024
+ if (type == "boolean" || type == "bool") {
1025
+ gboolean d = def.IsBoolean() ? def.As<Napi::Boolean>().Value()
1026
+ : (def.IsNumber() ? def.ToBoolean().Value() : FALSE);
1027
+ return g_param_spec_boolean(nm, nm, nm, d, flags);
1028
+ }
1029
+ if (type == "int") {
1030
+ return g_param_spec_int(nm, nm, nm, hasMin ? static_cast<gint>(mn) : G_MININT,
1031
+ hasMax ? static_cast<gint>(mx) : G_MAXINT,
1032
+ def.IsNumber() ? def.As<Napi::Number>().Int32Value() : 0, flags);
1033
+ }
1034
+ if (type == "uint") {
1035
+ return g_param_spec_uint(nm, nm, nm, hasMin ? static_cast<guint>(mn) : 0,
1036
+ hasMax ? static_cast<guint>(mx) : G_MAXUINT,
1037
+ def.IsNumber() ? def.As<Napi::Number>().Uint32Value() : 0, flags);
1038
+ }
1039
+ if (type == "int64") {
1040
+ return g_param_spec_int64(nm, nm, nm, hasMin ? static_cast<gint64>(mn) : G_MININT64,
1041
+ hasMax ? static_cast<gint64>(mx) : G_MAXINT64,
1042
+ def.IsNumber() ? def.As<Napi::Number>().Int64Value() : 0, flags);
1043
+ }
1044
+ if (type == "uint64") {
1045
+ return g_param_spec_uint64(nm, nm, nm, hasMin ? static_cast<guint64>(mn) : 0,
1046
+ hasMax ? static_cast<guint64>(mx) : G_MAXUINT64,
1047
+ def.IsNumber() ? static_cast<guint64>(def.As<Napi::Number>().Int64Value())
1048
+ : 0,
1049
+ flags);
1050
+ }
1051
+ if (type == "double") {
1052
+ return g_param_spec_double(nm, nm, nm, hasMin ? mn : -G_MAXDOUBLE, hasMax ? mx : G_MAXDOUBLE,
1053
+ def.IsNumber() ? def.As<Napi::Number>().DoubleValue() : 0, flags);
1054
+ }
1055
+ if (type == "float") {
1056
+ return g_param_spec_float(nm, nm, nm, hasMin ? static_cast<gfloat>(mn) : -G_MAXFLOAT,
1057
+ hasMax ? static_cast<gfloat>(mx) : G_MAXFLOAT,
1058
+ def.IsNumber() ? static_cast<gfloat>(def.As<Napi::Number>().DoubleValue())
1059
+ : 0,
1060
+ flags);
1061
+ }
1062
+ *err = "unsupported property type '" + type + "'";
1063
+ return nullptr;
1064
+ }
1065
+
1066
+ struct NodeGiSignalDef {
1067
+ std::string name;
1068
+ std::vector<GType> paramTypes;
1069
+ GType returnType;
1070
+ GSignalFlags flags;
1071
+ };
1072
+
1073
+ // ---- registerClass vfunc overrides (class-level refs; no toggle-ref) ----
1074
+ //
1075
+ // A registered subclass can override a parent GObject vfunc with a JS function.
1076
+ // Each override is held by a per-CLASS record carrying a STRONG napi_ref to the
1077
+ // JS impl plus the ffi closure written into the class vtable. Both live for the
1078
+ // class lifetime and are NEVER freed — a GType is process-permanent, so this is
1079
+ // the same ownership model as the signal class-handler, NOT a per-instance cycle:
1080
+ // no toggle-ref / instance-GC bridge is needed (that lands in a later drop).
1081
+ //
1082
+ // In class_init the vfunc info is resolved by walking the parent object-info
1083
+ // chain (gi_object_info_find_vfunc), the vtable slot is located via the matching
1084
+ // class-struct FIELD offset (gi_vfunc_info_get_offset is GI_UNKNOWN/0xFFFF for
1085
+ // GObject's own vfuncs, so the GJS field-offset approach is authoritative), and
1086
+ // the closure's native address is written into the class struct at that offset.
1087
+ struct NodeGiVFunc {
1088
+ napi_env env;
1089
+ std::string name;
1090
+ napi_ref fn; // strong ref to the JS impl (class lifetime; never freed)
1091
+ GIVFuncInfo* info; // resolved vfunc info (owned; kept alive for the closure)
1092
+ ffi_cif cif; // stable storage for the closure's cif
1093
+ ffi_closure* closure; // ffi closure (class lifetime; never freed)
1094
+ };
1095
+
1096
+ // The ffi closure entry point invoked when C calls the overridden vfunc. For a
1097
+ // method vfunc the ffi args are [instance, declared-arg-0, declared-arg-1, ...];
1098
+ // the instance is passed as the JS receiver (`this`, GJS-faithful: vfunc impls
1099
+ // are methods on the instance) and the declared args become the JS arguments.
1100
+ // The return is marshalled into `result` exactly like NodeGiCallbackTrampoline.
1101
+ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
1102
+ gpointer user_data) {
1103
+ NodeGiVFunc* vf = static_cast<NodeGiVFunc*>(user_data);
1104
+ napi_env env = vf->env;
1105
+ Napi::Env napiEnv(env);
1106
+ Napi::HandleScope scope(napiEnv);
1107
+
1108
+ GICallableInfo* ci = reinterpret_cast<GICallableInfo*>(vf->info);
1109
+ // args[0] is the instance; declared args follow at args[1..].
1110
+ Napi::Value recv = WrapGObject(
1111
+ napiEnv, static_cast<GObject*>(static_cast<GIArgument*>(args[0])->v_pointer),
1112
+ GI_TRANSFER_NOTHING);
1113
+
1114
+ unsigned int n = gi_callable_info_get_n_args(ci);
1115
+ std::vector<napi_value> jsArgs;
1116
+ jsArgs.reserve(n);
1117
+ bool ok = true;
1118
+ for (unsigned int i = 0; i < n; i++) {
1119
+ GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
1120
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
1121
+ Napi::Value v =
1122
+ GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i + 1]), GI_TRANSFER_NOTHING);
1123
+ gi_base_info_unref(ti);
1124
+ gi_base_info_unref(ai);
1125
+ if (napiEnv.IsExceptionPending()) {
1126
+ ok = false;
1127
+ break;
1128
+ }
1129
+ jsArgs.push_back(v);
1130
+ }
1131
+
1132
+ // Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
1133
+ // upper bytes indeterminate otherwise).
1134
+ if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
1135
+
1136
+ GITypeInfo* retType = gi_callable_info_get_return_type(ci);
1137
+ if (ok) {
1138
+ napi_value fn = nullptr;
1139
+ if (napi_get_reference_value(env, vf->fn, &fn) == napi_ok && fn != nullptr) {
1140
+ napi_value ret = nullptr;
1141
+ // napi_make_callback drains nextTick/microtasks around the call; the
1142
+ // wrapped instance is the receiver (`this`).
1143
+ napi_status st =
1144
+ napi_make_callback(env, nullptr, recv, fn, jsArgs.size(), jsArgs.data(), &ret);
1145
+ if (st == napi_ok && result != nullptr) {
1146
+ GITypeTag rtag = gi_type_info_get_tag(retType);
1147
+ if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
1148
+ // Hand the caller an owned copy — a JsToGIArgument string would point
1149
+ // into a std::string that dies with this frame.
1150
+ Napi::Value rv(env, ret);
1151
+ static_cast<GIArgument*>(result)->v_string =
1152
+ rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
1153
+ } else if (rtag != GI_TYPE_TAG_VOID) {
1154
+ std::string held;
1155
+ JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
1156
+ &held);
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+ gi_base_info_unref(retType);
1162
+ // A pending JS exception surfaces at the next N-API boundary (e.g. when the
1163
+ // constructType / method call that triggered this vfunc returns).
1164
+ }
1165
+
1166
+ // Per-registered-type metadata, passed as GTypeInfo.class_data → class_init.
1167
+ // Heap-allocated and intentionally never freed (a GType is process-permanent).
1168
+ struct NodeGiClassData {
1169
+ std::vector<GParamSpec*> properties; // ownership transfers to the class on install
1170
+ std::vector<NodeGiSignalDef> signals;
1171
+ std::vector<NodeGiVFunc*> vfuncs; // class-lifetime vfunc overrides (never freed)
1172
+ void (*parentGet)(GObject*, guint, GValue*, GParamSpec*);
1173
+ void (*parentSet)(GObject*, guint, const GValue*, GParamSpec*);
1174
+ };
1175
+
1176
+ static GQuark NodeGiClassDataQuark() {
1177
+ static GQuark q = g_quark_from_static_string("node-gi-class-data");
1178
+ return q;
1179
+ }
1180
+ static GQuark NodeGiInstancePropsQuark() {
1181
+ static GQuark q = g_quark_from_static_string("node-gi-instance-props");
1182
+ return q;
1183
+ }
1184
+
1185
+ static void FreeStoredGValue(gpointer p) {
1186
+ GValue* v = static_cast<GValue*>(p);
1187
+ g_value_unset(v);
1188
+ g_free(v);
1189
+ }
1190
+
1191
+ // Nearest ancestor (incl. self) carrying node-gi class-data.
1192
+ static NodeGiClassData* FindClassData(GType type) {
1193
+ for (GType t = type; t != 0; t = g_type_parent(t)) {
1194
+ NodeGiClassData* cd = static_cast<NodeGiClassData*>(g_type_get_qdata(t, NodeGiClassDataQuark()));
1195
+ if (cd != nullptr) return cd;
1196
+ }
1197
+ return nullptr;
1198
+ }
1199
+
1200
+ // A property is custom iff its owner GType carries node-gi class-data; otherwise
1201
+ // it is inherited from the introspected parent and chains to the parent vfunc.
1202
+ static void NodeGiGetProperty(GObject* obj, guint prop_id, GValue* value, GParamSpec* pspec) {
1203
+ NodeGiClassData* ownerCd =
1204
+ static_cast<NodeGiClassData*>(g_type_get_qdata(pspec->owner_type, NodeGiClassDataQuark()));
1205
+ if (ownerCd != nullptr) {
1206
+ GHashTable* store = static_cast<GHashTable*>(g_object_get_qdata(obj, NodeGiInstancePropsQuark()));
1207
+ GValue* stored = store ? static_cast<GValue*>(g_hash_table_lookup(store, pspec->name)) : nullptr;
1208
+ if (stored != nullptr && G_IS_VALUE(stored)) {
1209
+ g_value_copy(stored, value);
1210
+ } else {
1211
+ g_param_value_set_default(pspec, value);
1212
+ }
1213
+ return;
1214
+ }
1215
+ NodeGiClassData* cd = FindClassData(G_OBJECT_TYPE(obj));
1216
+ if (cd != nullptr && cd->parentGet != nullptr) cd->parentGet(obj, prop_id, value, pspec);
1217
+ }
1218
+
1219
+ static void NodeGiSetProperty(GObject* obj, guint prop_id, const GValue* value, GParamSpec* pspec) {
1220
+ NodeGiClassData* ownerCd =
1221
+ static_cast<NodeGiClassData*>(g_type_get_qdata(pspec->owner_type, NodeGiClassDataQuark()));
1222
+ if (ownerCd != nullptr) {
1223
+ GHashTable* store = static_cast<GHashTable*>(g_object_get_qdata(obj, NodeGiInstancePropsQuark()));
1224
+ if (store == nullptr) {
1225
+ store = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, FreeStoredGValue);
1226
+ g_object_set_qdata_full(obj, NodeGiInstancePropsQuark(), store,
1227
+ reinterpret_cast<GDestroyNotify>(g_hash_table_destroy));
1228
+ }
1229
+ GValue* copy = g_new0(GValue, 1);
1230
+ g_value_init(copy, G_VALUE_TYPE(value));
1231
+ g_value_copy(value, copy);
1232
+ g_hash_table_replace(store, g_strdup(pspec->name), copy);
1233
+ g_object_notify_by_pspec(obj, pspec);
1234
+ return;
1235
+ }
1236
+ NodeGiClassData* cd = FindClassData(G_OBJECT_TYPE(obj));
1237
+ if (cd != nullptr && cd->parentSet != nullptr) cd->parentSet(obj, prop_id, value, pspec);
1238
+ }
1239
+
1240
+ static void NodeGiClassInit(gpointer g_class, gpointer class_data) {
1241
+ NodeGiClassData* cd = static_cast<NodeGiClassData*>(class_data);
1242
+ GObjectClass* oc = G_OBJECT_CLASS(g_class);
1243
+ g_type_set_qdata(G_TYPE_FROM_CLASS(g_class), NodeGiClassDataQuark(), cd);
1244
+
1245
+ if (!cd->properties.empty()) {
1246
+ cd->parentGet = oc->get_property; // capture before override (chain target)
1247
+ cd->parentSet = oc->set_property;
1248
+ oc->get_property = NodeGiGetProperty;
1249
+ oc->set_property = NodeGiSetProperty;
1250
+ guint id = 1;
1251
+ for (GParamSpec* p : cd->properties) {
1252
+ g_object_class_install_property(oc, id++, p);
1253
+ }
1254
+ }
1255
+ for (const NodeGiSignalDef& s : cd->signals) {
1256
+ g_signal_newv(s.name.c_str(), G_TYPE_FROM_CLASS(g_class), s.flags, nullptr, nullptr, nullptr,
1257
+ nullptr, s.returnType, static_cast<guint>(s.paramTypes.size()),
1258
+ s.paramTypes.empty() ? nullptr : const_cast<GType*>(s.paramTypes.data()));
1259
+ }
1260
+
1261
+ if (!cd->vfuncs.empty()) {
1262
+ GType newType = G_TYPE_FROM_CLASS(g_class);
1263
+ GType parentType = g_type_parent(newType);
1264
+ GIRepository* repo = gi_repository_dup_default();
1265
+ for (NodeGiVFunc* vf : cd->vfuncs) {
1266
+ // Resolve the vfunc info by walking the parent object-info chain; the
1267
+ // declarer's class struct holds the vtable slot we write into.
1268
+ GIVFuncInfo* vi = nullptr;
1269
+ GIObjectInfo* declarer = nullptr;
1270
+ for (GType t = parentType; t != 0 && vi == nullptr; t = g_type_parent(t)) {
1271
+ GIBaseInfo* bi = gi_repository_find_by_gtype(repo, t);
1272
+ if (bi != nullptr) {
1273
+ if (GI_IS_OBJECT_INFO(bi)) {
1274
+ vi = gi_object_info_find_vfunc(reinterpret_cast<GIObjectInfo*>(bi), vf->name.c_str());
1275
+ if (vi != nullptr)
1276
+ declarer = reinterpret_cast<GIObjectInfo*>(gi_base_info_ref(bi));
1277
+ }
1278
+ gi_base_info_unref(bi);
1279
+ }
1280
+ }
1281
+ if (vi == nullptr) {
1282
+ g_warning("node-gi: registerClass vfunc '%s' not found on any ancestor of %s",
1283
+ vf->name.c_str(), g_type_name(newType));
1284
+ continue;
1285
+ }
1286
+
1287
+ // Locate the vtable slot. gi_vfunc_info_get_offset is GI_UNKNOWN (0xFFFF)
1288
+ // for GObject's own vfuncs, so match the vfunc name to a class-struct field
1289
+ // and use that field's offset (the GJS approach); fall back to the recorded
1290
+ // offset only when the field lookup fails.
1291
+ int offset = -1;
1292
+ GIStructInfo* cs = gi_object_info_get_class_struct(declarer);
1293
+ if (cs != nullptr) {
1294
+ unsigned int nf = gi_struct_info_get_n_fields(cs);
1295
+ for (unsigned int fi = 0; fi < nf && offset < 0; fi++) {
1296
+ GIFieldInfo* f = gi_struct_info_get_field(cs, fi);
1297
+ const char* fn = gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(f));
1298
+ if (fn != nullptr && vf->name == fn) offset = gi_field_info_get_offset(f);
1299
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(f));
1300
+ }
1301
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(cs));
1302
+ }
1303
+ if (offset < 0) {
1304
+ size_t off = gi_vfunc_info_get_offset(vi);
1305
+ if (off != 0 && off != 0xFFFF) offset = static_cast<int>(off);
1306
+ }
1307
+ if (offset < 0) {
1308
+ g_warning("node-gi: could not resolve a vtable slot for vfunc '%s' on %s",
1309
+ vf->name.c_str(), g_type_name(newType));
1310
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(vi));
1311
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(declarer));
1312
+ continue;
1313
+ }
1314
+
1315
+ // Create the ffi closure and write its native address into the class vtable.
1316
+ // The vfunc info + closure + cif are kept alive for the class lifetime.
1317
+ vf->info = vi; // retained (kept alive); never unref'd — class is permanent
1318
+ vf->closure = gi_callable_info_create_closure(reinterpret_cast<GICallableInfo*>(vi), &vf->cif,
1319
+ NodeGiVFuncTrampoline, vf);
1320
+ gpointer native =
1321
+ gi_callable_info_get_closure_native_address(reinterpret_cast<GICallableInfo*>(vi),
1322
+ vf->closure);
1323
+ if (native == nullptr) native = vf->closure;
1324
+ *reinterpret_cast<gpointer*>(reinterpret_cast<guint8*>(g_class) + offset) = native;
1325
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(declarer));
1326
+ }
1327
+ g_object_unref(repo);
1328
+ }
1329
+ }
1330
+
1331
+ // registerClass(name, parentNamespace, parentTypeName, options?) -> typeHandle
1332
+ Napi::Value RegisterClass(const Napi::CallbackInfo& info) {
1333
+ Napi::Env env = info.Env();
1334
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1335
+ Napi::TypeError::New(
1336
+ env,
1337
+ "registerClass(name: string, parentNamespace: string, parentTypeName: string, options?: "
1338
+ "{ properties?, signals?, vfuncs? })")
1339
+ .ThrowAsJavaScriptException();
1340
+ return env.Null();
1341
+ }
1342
+ std::string name = info[0].As<Napi::String>().Utf8Value();
1343
+ std::string pns = info[1].As<Napi::String>().Utf8Value();
1344
+ std::string ptn = info[2].As<Napi::String>().Utf8Value();
1345
+
1346
+ if (g_type_from_name(name.c_str()) != 0) {
1347
+ Napi::Error::New(env, "a GType named '" + name + "' is already registered")
1348
+ .ThrowAsJavaScriptException();
1349
+ return env.Null();
1350
+ }
1351
+
1352
+ // Resolve the parent GType from its introspection info.
1353
+ GIRepository* repo = DupDefaultRepository();
1354
+ GIBaseInfo* base = gi_repository_find_by_name(repo, pns.c_str(), ptn.c_str());
1355
+ bool isObject = base != nullptr && GI_IS_OBJECT_INFO(base);
1356
+ GType parentType =
1357
+ isObject ? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base))
1358
+ : G_TYPE_INVALID;
1359
+ if (base != nullptr) gi_base_info_unref(base);
1360
+ g_object_unref(repo);
1361
+ if (!isObject || !G_TYPE_IS_OBJECT(parentType)) {
1362
+ Napi::TypeError::New(env, pns + "." + ptn + " is not a subclassable GObject type")
1363
+ .ThrowAsJavaScriptException();
1364
+ return env.Null();
1365
+ }
1366
+
1367
+ GTypeQuery query;
1368
+ g_type_query(parentType, &query);
1369
+ if (query.type == 0) {
1370
+ Napi::Error::New(env, "failed to query parent type " + pns + "." + ptn)
1371
+ .ThrowAsJavaScriptException();
1372
+ return env.Null();
1373
+ }
1374
+
1375
+ // Parse the optional { properties, signals } and build the class metadata.
1376
+ NodeGiClassData* cd = new NodeGiClassData();
1377
+ cd->parentGet = nullptr;
1378
+ cd->parentSet = nullptr;
1379
+ if (info.Length() >= 4 && info[3].IsObject()) {
1380
+ Napi::Object opts = info[3].As<Napi::Object>();
1381
+ if (opts.Has("properties") && opts.Get("properties").IsArray()) {
1382
+ Napi::Array props = opts.Get("properties").As<Napi::Array>();
1383
+ for (uint32_t i = 0; i < props.Length(); i++) {
1384
+ Napi::Value pv = props.Get(i);
1385
+ if (!pv.IsObject()) continue;
1386
+ std::string perr;
1387
+ GParamSpec* ps = BuildParamSpec(env, pv.As<Napi::Object>(), &perr);
1388
+ if (ps == nullptr) {
1389
+ for (GParamSpec* done : cd->properties) {
1390
+ g_param_spec_ref_sink(done);
1391
+ g_param_spec_unref(done);
1392
+ }
1393
+ delete cd;
1394
+ Napi::TypeError::New(env, "registerClass property: " + perr).ThrowAsJavaScriptException();
1395
+ return env.Null();
1396
+ }
1397
+ cd->properties.push_back(ps);
1398
+ }
1399
+ }
1400
+ if (opts.Has("signals") && opts.Get("signals").IsArray()) {
1401
+ Napi::Array sigs = opts.Get("signals").As<Napi::Array>();
1402
+ for (uint32_t i = 0; i < sigs.Length(); i++) {
1403
+ Napi::Value sv = sigs.Get(i);
1404
+ if (!sv.IsObject()) continue;
1405
+ Napi::Object so = sv.As<Napi::Object>();
1406
+ if (!so.Has("name") || !so.Get("name").IsString()) continue;
1407
+ NodeGiSignalDef sd;
1408
+ sd.name = so.Get("name").As<Napi::String>().Utf8Value();
1409
+ sd.returnType = G_TYPE_NONE;
1410
+ if (so.Has("returnType") && so.Get("returnType").IsString()) {
1411
+ GType rt = TypeNameToGType(so.Get("returnType").As<Napi::String>().Utf8Value());
1412
+ if (rt != G_TYPE_INVALID) sd.returnType = rt;
1413
+ }
1414
+ sd.flags = (so.Has("flags") && so.Get("flags").IsNumber())
1415
+ ? static_cast<GSignalFlags>(so.Get("flags").As<Napi::Number>().Int32Value())
1416
+ : G_SIGNAL_RUN_LAST;
1417
+ if (so.Has("paramTypes") && so.Get("paramTypes").IsArray()) {
1418
+ Napi::Array pts = so.Get("paramTypes").As<Napi::Array>();
1419
+ for (uint32_t j = 0; j < pts.Length(); j++) {
1420
+ GType t = TypeNameToGType(pts.Get(j).ToString().Utf8Value());
1421
+ if (t != G_TYPE_INVALID && t != G_TYPE_NONE) sd.paramTypes.push_back(t);
1422
+ }
1423
+ }
1424
+ cd->signals.push_back(sd);
1425
+ }
1426
+ }
1427
+ // vfuncs: an object { "<vfunc-name>": <jsFunction>, ... }. Each holds a strong
1428
+ // napi_ref for the class lifetime (resolved + hooked up in class_init).
1429
+ if (opts.Has("vfuncs") && opts.Get("vfuncs").IsObject()) {
1430
+ Napi::Object vf = opts.Get("vfuncs").As<Napi::Object>();
1431
+ Napi::Array keys = vf.GetPropertyNames();
1432
+ for (uint32_t i = 0; i < keys.Length(); i++) {
1433
+ std::string vname = keys.Get(i).ToString().Utf8Value();
1434
+ Napi::Value fnv = vf.Get(vname);
1435
+ if (!fnv.IsFunction()) continue;
1436
+ NodeGiVFunc* rec = new NodeGiVFunc();
1437
+ rec->env = env;
1438
+ rec->name = vname;
1439
+ rec->info = nullptr;
1440
+ rec->closure = nullptr;
1441
+ rec->fn = nullptr;
1442
+ napi_create_reference(env, fnv, 1, &rec->fn);
1443
+ cd->vfuncs.push_back(rec);
1444
+ }
1445
+ }
1446
+ }
1447
+
1448
+ GTypeInfo typeInfo = {};
1449
+ typeInfo.class_size = static_cast<guint16>(query.class_size);
1450
+ typeInfo.instance_size = static_cast<guint16>(query.instance_size);
1451
+ // class_init installs the custom properties + signals (and records the class
1452
+ // data even when there are none, so the property vfuncs can find it).
1453
+ typeInfo.class_init = NodeGiClassInit;
1454
+ typeInfo.class_data = cd;
1455
+
1456
+ GType newType = g_type_register_static(parentType, name.c_str(), &typeInfo, (GTypeFlags)0);
1457
+ if (newType == 0) {
1458
+ for (NodeGiVFunc* vf : cd->vfuncs) {
1459
+ if (vf->fn != nullptr) napi_delete_reference(env, vf->fn);
1460
+ delete vf;
1461
+ }
1462
+ delete cd;
1463
+ Napi::Error::New(env, "g_type_register_static failed for '" + name + "'")
1464
+ .ThrowAsJavaScriptException();
1465
+ return env.Null();
1466
+ }
1467
+ return Napi::External<void>::New(env, reinterpret_cast<void*>(newType));
1468
+ }
1469
+
1470
+ // constructType(typeHandle, props?: Record<string, unknown>) -> External<GObject>
1471
+ Napi::Value ConstructType(const Napi::CallbackInfo& info) {
1472
+ Napi::Env env = info.Env();
1473
+ if (info.Length() < 1) {
1474
+ Napi::TypeError::New(env, "constructType(typeHandle, props?: object)")
1475
+ .ThrowAsJavaScriptException();
1476
+ return env.Null();
1477
+ }
1478
+ GType gtype = UnwrapGType(env, info[0]);
1479
+ if (gtype == 0) return env.Null();
1480
+ if (!G_TYPE_IS_OBJECT(gtype)) {
1481
+ Napi::TypeError::New(env, "type handle is not a constructible GObject type")
1482
+ .ThrowAsJavaScriptException();
1483
+ return env.Null();
1484
+ }
1485
+ Napi::Object props = (info.Length() >= 2 && info[1].IsObject()) ? info[1].As<Napi::Object>()
1486
+ : Napi::Object::New(env);
1487
+ return ConstructGObject(env, gtype, props, std::string(g_type_name(gtype)));
1488
+ }
1489
+
1490
+ // getProperty(handle, name) -> unknown
1491
+ Napi::Value GetProperty(const Napi::CallbackInfo& info) {
1492
+ Napi::Env env = info.Env();
1493
+ if (info.Length() < 2 || !info[1].IsString()) {
1494
+ Napi::TypeError::New(env, "getProperty(handle, name: string)").ThrowAsJavaScriptException();
1495
+ return env.Null();
1496
+ }
1497
+ GObject* obj = UnwrapGObject(env, info[0]);
1498
+ if (obj == nullptr) return env.Null();
1499
+ std::string name = info[1].As<Napi::String>().Utf8Value();
1500
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
1501
+ if (pspec == nullptr) {
1502
+ Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
1503
+ return env.Null();
1504
+ }
1505
+ GValue v = G_VALUE_INIT;
1506
+ g_value_init(&v, pspec->value_type);
1507
+ g_object_get_property(obj, name.c_str(), &v);
1508
+ Napi::Value result = GValueToJs(env, &v);
1509
+ g_value_unset(&v);
1510
+ return result;
1511
+ }
1512
+
1513
+ // setProperty(handle, name, value) -> void
1514
+ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
1515
+ Napi::Env env = info.Env();
1516
+ if (info.Length() < 3 || !info[1].IsString()) {
1517
+ Napi::TypeError::New(env, "setProperty(handle, name: string, value)").ThrowAsJavaScriptException();
1518
+ return env.Undefined();
1519
+ }
1520
+ GObject* obj = UnwrapGObject(env, info[0]);
1521
+ if (obj == nullptr) return env.Undefined();
1522
+ std::string name = info[1].As<Napi::String>().Utf8Value();
1523
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
1524
+ if (pspec == nullptr) {
1525
+ Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
1526
+ return env.Undefined();
1527
+ }
1528
+ GValue v = G_VALUE_INIT;
1529
+ g_value_init(&v, pspec->value_type);
1530
+ if (JsToGValue(env, info[2], &v)) {
1531
+ g_object_set_property(obj, name.c_str(), &v);
1532
+ }
1533
+ g_value_unset(&v);
1534
+ return env.Undefined();
1535
+ }
1536
+
1537
+ // getTypeName(handle) -> string (the runtime GType name of an instance)
1538
+ Napi::Value GetTypeName(const Napi::CallbackInfo& info) {
1539
+ Napi::Env env = info.Env();
1540
+ GObject* obj = UnwrapGObject(env, info[0]);
1541
+ if (obj == nullptr) return env.Null();
1542
+ return Napi::String::New(env, G_OBJECT_TYPE_NAME(obj));
1543
+ }
1544
+
1545
+ // hasProperty(handle, name) -> boolean
1546
+ // Whether the instance's type has a GObject property by this name. The L1
1547
+ // wrapper uses it to route `obj.foo` to a property read vs an `obj.foo()` method.
1548
+ Napi::Value HasProperty(const Napi::CallbackInfo& info) {
1549
+ Napi::Env env = info.Env();
1550
+ if (info.Length() < 2 || !info[1].IsString()) {
1551
+ Napi::TypeError::New(env, "hasProperty(handle, name: string)").ThrowAsJavaScriptException();
1552
+ return env.Null();
1553
+ }
1554
+ GObject* obj = UnwrapGObject(env, info[0]);
1555
+ if (obj == nullptr) return env.Null();
1556
+ std::string name = info[1].As<Napi::String>().Utf8Value();
1557
+ GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name.c_str());
1558
+ return Napi::Boolean::New(env, pspec != nullptr);
1559
+ }
1560
+
1561
+ // isGObjectHandle(value) -> boolean
1562
+ // Whether `value` is one of node-gi's GObject-instance handles (tag-checked, no
1563
+ // dereference). Lets the L1 wrapper detect object-typed return values and wrap
1564
+ // them as instances for chaining, without misclassifying a GType handle.
1565
+ Napi::Value IsGObjectHandle(const Napi::CallbackInfo& info) {
1566
+ Napi::Env env = info.Env();
1567
+ bool is = info.Length() >= 1 && info[0].IsExternal() &&
1568
+ info[0].As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag);
1569
+ return Napi::Boolean::New(env, is);
1570
+ }
1571
+
1572
+ // callMethod(handle, methodName, args?: unknown[]) -> unknown
1573
+ // Resolve an instance method by walking the instance's GIObjectInfo (own +
1574
+ // implemented-interface methods at each level, then up the parent chain), then
1575
+ // invoke it with the instance prepended. The Node twin of `obj.method(...)`.
1576
+ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1577
+ Napi::Env env = info.Env();
1578
+ if (info.Length() < 2 || !info[1].IsString()) {
1579
+ Napi::TypeError::New(env, "callMethod(handle, methodName: string, args?: unknown[])")
1580
+ .ThrowAsJavaScriptException();
1581
+ return env.Null();
1582
+ }
1583
+ GObject* obj = UnwrapGObject(env, info[0]);
1584
+ if (obj == nullptr) return env.Null();
1585
+ std::string method = info[1].As<Napi::String>().Utf8Value();
1586
+ Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
1587
+ : Napi::Array::New(env, 0);
1588
+
1589
+ GType gtype = G_OBJECT_TYPE(obj);
1590
+ GIRepository* repo = DupDefaultRepository();
1591
+
1592
+ // The instance's concrete GType may lack introspection info (e.g. a private
1593
+ // GLocalFile); walk up to the nearest ancestor GType that has an object info.
1594
+ GIObjectInfo* objInfo = nullptr;
1595
+ for (GType t = gtype; t != 0; t = g_type_parent(t)) {
1596
+ GIBaseInfo* bi = gi_repository_find_by_gtype(repo, t);
1597
+ if (bi != nullptr) {
1598
+ if (GI_IS_OBJECT_INFO(bi)) {
1599
+ objInfo = reinterpret_cast<GIObjectInfo*>(bi);
1600
+ break;
1601
+ }
1602
+ gi_base_info_unref(bi);
1603
+ }
1604
+ }
1605
+ if (objInfo == nullptr) {
1606
+ g_object_unref(repo);
1607
+ Napi::Error::New(env, std::string("no introspection info for ") + G_OBJECT_TYPE_NAME(obj))
1608
+ .ThrowAsJavaScriptException();
1609
+ return env.Null();
1610
+ }
1611
+
1612
+ // Search own + implemented-interface methods at each level, then the parent.
1613
+ GIFunctionInfo* func = nullptr;
1614
+ while (objInfo != nullptr) {
1615
+ GIBaseInfo* declarer = nullptr;
1616
+ func = gi_object_info_find_method_using_interfaces(objInfo, method.c_str(), &declarer);
1617
+ if (declarer != nullptr) gi_base_info_unref(declarer);
1618
+ if (func != nullptr) break;
1619
+ GIObjectInfo* parent = gi_object_info_get_parent(objInfo);
1620
+ gi_base_info_unref(objInfo);
1621
+ objInfo = parent;
1622
+ }
1623
+ if (objInfo != nullptr) gi_base_info_unref(objInfo);
1624
+
1625
+ // The concrete GType may implement introspectable interfaces that its nearest
1626
+ // introspectable ANCESTOR's info does not list — e.g. a private GLocalFile
1627
+ // (no info; ancestor = GObject) implementing GFile. Scan the live GType's
1628
+ // interface list directly so interface methods (g_file_get_path) resolve.
1629
+ if (func == nullptr) {
1630
+ unsigned int n_ifaces = 0;
1631
+ GType* ifaces = g_type_interfaces(gtype, &n_ifaces);
1632
+ for (unsigned int i = 0; i < n_ifaces && func == nullptr; i++) {
1633
+ GIBaseInfo* ii = gi_repository_find_by_gtype(repo, ifaces[i]);
1634
+ if (ii != nullptr) {
1635
+ if (GI_IS_INTERFACE_INFO(ii)) {
1636
+ func = gi_interface_info_find_method(reinterpret_cast<GIInterfaceInfo*>(ii),
1637
+ method.c_str());
1638
+ }
1639
+ gi_base_info_unref(ii);
1640
+ }
1641
+ }
1642
+ g_free(ifaces);
1643
+ }
1644
+ g_object_unref(repo);
1645
+
1646
+ if (func == nullptr) {
1647
+ Napi::Error::New(env, std::string("no method '") + method + "' on " + G_OBJECT_TYPE_NAME(obj))
1648
+ .ThrowAsJavaScriptException();
1649
+ return env.Null();
1650
+ }
1651
+ if (!gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1652
+ gi_base_info_unref(func);
1653
+ Napi::TypeError::New(env, method + " is not an instance method").ThrowAsJavaScriptException();
1654
+ return env.Null();
1655
+ }
1656
+
1657
+ Napi::Value result =
1658
+ InvokeFunctionInfo(env, func, obj, args, std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1659
+ gi_base_info_unref(func);
1660
+ return result;
1661
+ }
1662
+
1663
+ // callStaticMethod(namespace, typeName, methodName, args?) -> unknown
1664
+ // Invoke a type-level constructor/static function (e.g. Gio.File.new_for_path,
1665
+ // Gtk.Label.new) — a function found ON a type but taking no instance. The Node
1666
+ // twin of `Ns.Class.method(...)`.
1667
+ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1668
+ Napi::Env env = info.Env();
1669
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1670
+ Napi::TypeError::New(
1671
+ env, "callStaticMethod(namespace: string, typeName: string, methodName: string, args?: unknown[])")
1672
+ .ThrowAsJavaScriptException();
1673
+ return env.Null();
1674
+ }
1675
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
1676
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
1677
+ std::string method = info[2].As<Napi::String>().Utf8Value();
1678
+ Napi::Array args = (info.Length() >= 4 && info[3].IsArray()) ? info[3].As<Napi::Array>()
1679
+ : Napi::Array::New(env, 0);
1680
+
1681
+ GIRepository* repo = DupDefaultRepository();
1682
+ GIBaseInfo* typeInfo = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
1683
+ if (typeInfo == nullptr) {
1684
+ g_object_unref(repo);
1685
+ Napi::Error::New(env, "no such type: " + ns + "." + tn).ThrowAsJavaScriptException();
1686
+ return env.Null();
1687
+ }
1688
+ GIFunctionInfo* func = nullptr;
1689
+ if (GI_IS_OBJECT_INFO(typeInfo)) {
1690
+ func = gi_object_info_find_method(reinterpret_cast<GIObjectInfo*>(typeInfo), method.c_str());
1691
+ } else if (GI_IS_INTERFACE_INFO(typeInfo)) {
1692
+ func = gi_interface_info_find_method(reinterpret_cast<GIInterfaceInfo*>(typeInfo), method.c_str());
1693
+ } else if (GI_IS_STRUCT_INFO(typeInfo)) {
1694
+ func = gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(typeInfo), method.c_str());
1695
+ }
1696
+ gi_base_info_unref(typeInfo);
1697
+ if (func == nullptr) {
1698
+ g_object_unref(repo);
1699
+ Napi::Error::New(env, "no static method '" + method + "' on " + ns + "." + tn)
1700
+ .ThrowAsJavaScriptException();
1701
+ return env.Null();
1702
+ }
1703
+ if (gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1704
+ gi_base_info_unref(func);
1705
+ g_object_unref(repo);
1706
+ Napi::TypeError::New(env, ns + "." + tn + "." + method +
1707
+ " is an instance method — call it on an instance")
1708
+ .ThrowAsJavaScriptException();
1709
+ return env.Null();
1710
+ }
1711
+ Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + tn + "." + method);
1712
+ gi_base_info_unref(func);
1713
+ g_object_unref(repo);
1714
+ return result;
1715
+ }
1716
+
1717
+ // callBoxedMethod(handle, methodName, args?) -> unknown
1718
+ // Invoke an instance method on a boxed/struct handle (e.g. mainLoop.run() /
1719
+ // mainLoop.quit()). Resolves the method against the boxed GType's GIStructInfo
1720
+ // (or union info) and invokes it with the boxed pointer prepended as instance.
1721
+ Napi::Value CallBoxedMethod(const Napi::CallbackInfo& info) {
1722
+ Napi::Env env = info.Env();
1723
+ if (info.Length() < 2 || !info[1].IsString()) {
1724
+ Napi::TypeError::New(env, "callBoxedMethod(handle, methodName: string, args?: unknown[])")
1725
+ .ThrowAsJavaScriptException();
1726
+ return env.Null();
1727
+ }
1728
+ if (!info[0].IsExternal() ||
1729
+ !info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag)) {
1730
+ Napi::TypeError::New(env, "expected a node-gi boxed handle").ThrowAsJavaScriptException();
1731
+ return env.Null();
1732
+ }
1733
+ BoxedHandle* bh = info[0].As<Napi::External<BoxedHandle>>().Data();
1734
+ if (bh == nullptr || bh->ptr == nullptr) {
1735
+ Napi::TypeError::New(env, "invalid boxed handle").ThrowAsJavaScriptException();
1736
+ return env.Null();
1737
+ }
1738
+ std::string method = info[1].As<Napi::String>().Utf8Value();
1739
+ Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
1740
+ : Napi::Array::New(env, 0);
1741
+ if (bh->gtype == G_TYPE_INVALID) {
1742
+ Napi::Error::New(env, "boxed handle has no introspection GType for method resolution")
1743
+ .ThrowAsJavaScriptException();
1744
+ return env.Null();
1745
+ }
1746
+
1747
+ GIRepository* repo = DupDefaultRepository();
1748
+ GIBaseInfo* bi = gi_repository_find_by_gtype(repo, bh->gtype);
1749
+ GIFunctionInfo* func = nullptr;
1750
+ if (bi != nullptr && GI_IS_STRUCT_INFO(bi)) {
1751
+ func = gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(bi), method.c_str());
1752
+ } else if (bi != nullptr && GI_IS_UNION_INFO(bi)) {
1753
+ func = gi_union_info_find_method(reinterpret_cast<GIUnionInfo*>(bi), method.c_str());
1754
+ }
1755
+ if (bi != nullptr) gi_base_info_unref(bi);
1756
+ if (func == nullptr) {
1757
+ g_object_unref(repo);
1758
+ Napi::Error::New(env, std::string("no method '") + method + "' on " + g_type_name(bh->gtype))
1759
+ .ThrowAsJavaScriptException();
1760
+ return env.Null();
1761
+ }
1762
+
1763
+ Napi::Value result = InvokeFunctionInfo(env, func, bh->ptr, args,
1764
+ std::string(g_type_name(bh->gtype)) + "." + method);
1765
+ gi_base_info_unref(func);
1766
+ g_object_unref(repo);
1767
+ return result;
1768
+ }
1769
+
1770
+ // isBoxedHandle(value) -> boolean (tag-checked; no dereference)
1771
+ Napi::Value IsBoxedHandle(const Napi::CallbackInfo& info) {
1772
+ Napi::Env env = info.Env();
1773
+ bool is = info.Length() >= 1 && info[0].IsExternal() &&
1774
+ info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag);
1775
+ return Napi::Boolean::New(env, is);
1776
+ }
1777
+
1778
+ // ---- signals (milestone 1) ----
1779
+ //
1780
+ // A GClosure that wraps a JS callback. The callback is held by a strong
1781
+ // napi_ref; the closure's finalize notifier drops it. The generic marshal
1782
+ // converts the signal's GValue params to JS (skipping the emitter instance at
1783
+ // index 0) and the JS return into the signal return GValue.
1784
+ //
1785
+ // Caveat (documented): if the JS callback closes over the object's handle, that
1786
+ // forms a handle -> GObject -> closure -> napi_ref -> callback -> handle cycle
1787
+ // that V8's GC cannot break across the C boundary, so such a handler keeps the
1788
+ // object alive until explicitly disconnected. The toggle-ref refinement (with
1789
+ // the subclassing/registerClass drop) addresses this; for now disconnect
1790
+ // long-lived handlers explicitly.
1791
+
1792
+ struct JsClosureData {
1793
+ napi_env env;
1794
+ napi_ref callback;
1795
+ };
1796
+
1797
+ static void JsClosureFinalize(gpointer data, GClosure* /*closure*/) {
1798
+ JsClosureData* jc = static_cast<JsClosureData*>(data);
1799
+ if (jc == nullptr) return;
1800
+ if (jc->callback != nullptr) napi_delete_reference(jc->env, jc->callback);
1801
+ g_free(jc);
1802
+ }
1803
+
1804
+ static void JsClosureMarshal(GClosure* closure, GValue* return_value, guint n_param_values,
1805
+ const GValue* param_values, gpointer /*invocation_hint*/,
1806
+ gpointer /*marshal_data*/) {
1807
+ JsClosureData* jc = static_cast<JsClosureData*>(closure->data);
1808
+ if (jc == nullptr || jc->callback == nullptr) return;
1809
+ Napi::Env env(jc->env);
1810
+ Napi::HandleScope scope(env);
1811
+
1812
+ napi_value cbv = nullptr;
1813
+ if (napi_get_reference_value(jc->env, jc->callback, &cbv) != napi_ok || cbv == nullptr) return;
1814
+
1815
+ // Signal args excluding the emitter instance (param_values[0]).
1816
+ std::vector<napi_value> args;
1817
+ args.reserve(n_param_values > 0 ? n_param_values - 1 : 0);
1818
+ for (guint i = 1; i < n_param_values; i++) {
1819
+ Napi::Value v = GValueToJs(env, &param_values[i]);
1820
+ if (env.IsExceptionPending()) return;
1821
+ args.push_back(v);
1822
+ }
1823
+
1824
+ napi_value result = nullptr;
1825
+ napi_status st = napi_call_function(jc->env, env.Undefined(), cbv, args.size(), args.data(), &result);
1826
+ if (st != napi_ok) return; // JS threw — leave the pending exception to surface
1827
+
1828
+ if (return_value != nullptr && (G_VALUE_TYPE(return_value) != G_TYPE_INVALID)) {
1829
+ JsToGValue(env, Napi::Value(jc->env, result), return_value);
1830
+ }
1831
+ }
1832
+
1833
+ // connectSignal(handle, signalName, callback, after?) -> handlerId
1834
+ Napi::Value ConnectSignal(const Napi::CallbackInfo& info) {
1835
+ Napi::Env env = info.Env();
1836
+ if (info.Length() < 3 || !info[1].IsString() || !info[2].IsFunction()) {
1837
+ Napi::TypeError::New(env, "connectSignal(handle, signalName: string, callback: function, after?: boolean)")
1838
+ .ThrowAsJavaScriptException();
1839
+ return env.Null();
1840
+ }
1841
+ GObject* obj = UnwrapGObject(env, info[0]);
1842
+ if (obj == nullptr) return env.Null();
1843
+ std::string name = info[1].As<Napi::String>().Utf8Value();
1844
+ bool after = info.Length() >= 4 && info[3].ToBoolean().Value();
1845
+
1846
+ // Parse a possibly-detailed signal name ("notify::prop") into its signal id +
1847
+ // detail quark, so GJS-style detailed connects work (common for notify::).
1848
+ guint sigid = 0;
1849
+ GQuark detail = 0;
1850
+ if (!g_signal_parse_name(name.c_str(), G_OBJECT_TYPE(obj), &sigid, &detail, TRUE)) {
1851
+ Napi::Error::New(env, std::string("no signal '") + name + "' on " + G_OBJECT_TYPE_NAME(obj))
1852
+ .ThrowAsJavaScriptException();
1853
+ return env.Null();
1854
+ }
1855
+
1856
+ JsClosureData* jc = g_new0(JsClosureData, 1);
1857
+ jc->env = env;
1858
+ napi_create_reference(env, info[2], 1, &jc->callback);
1859
+
1860
+ GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
1861
+ g_closure_set_marshal(closure, JsClosureMarshal);
1862
+ g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);
1863
+
1864
+ // g_signal_connect_closure_by_id sinks the floating closure ref + owns it,
1865
+ // and honours the detail quark (the by-name variant cannot take a detail).
1866
+ gulong id = g_signal_connect_closure_by_id(obj, sigid, detail, closure, after);
1867
+ return Napi::Number::New(env, static_cast<double>(id));
1868
+ }
1869
+
1870
+ // emitSignal(handle, signalName, args?) -> returnValue
1871
+ Napi::Value EmitSignal(const Napi::CallbackInfo& info) {
1872
+ Napi::Env env = info.Env();
1873
+ if (info.Length() < 2 || !info[1].IsString()) {
1874
+ Napi::TypeError::New(env, "emitSignal(handle, signalName: string, args?: unknown[])")
1875
+ .ThrowAsJavaScriptException();
1876
+ return env.Null();
1877
+ }
1878
+ GObject* obj = UnwrapGObject(env, info[0]);
1879
+ if (obj == nullptr) return env.Null();
1880
+ std::string name = info[1].As<Napi::String>().Utf8Value();
1881
+ Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
1882
+ : Napi::Array::New(env, 0);
1883
+
1884
+ GType gtype = G_OBJECT_TYPE(obj);
1885
+ guint sigid = g_signal_lookup(name.c_str(), gtype);
1886
+ if (sigid == 0) {
1887
+ Napi::Error::New(env, std::string("no signal '") + name + "' on " + G_OBJECT_TYPE_NAME(obj))
1888
+ .ThrowAsJavaScriptException();
1889
+ return env.Null();
1890
+ }
1891
+ GSignalQuery query;
1892
+ g_signal_query(sigid, &query);
1893
+
1894
+ guint n = query.n_params;
1895
+ std::vector<GValue> params(n + 1); // [0] = instance
1896
+ g_value_init(&params[0], gtype);
1897
+ g_value_set_object(&params[0], obj);
1898
+ guint initialised = 1;
1899
+ bool ok = true;
1900
+ for (guint i = 0; i < n; i++) {
1901
+ GType pt = query.param_types[i] & ~G_SIGNAL_TYPE_STATIC_SCOPE;
1902
+ g_value_init(&params[i + 1], pt);
1903
+ initialised = i + 2;
1904
+ Napi::Value v = i < args.Length() ? args.Get(i) : env.Undefined();
1905
+ if (!JsToGValue(env, v, &params[i + 1])) {
1906
+ ok = false;
1907
+ break;
1908
+ }
1909
+ }
1910
+
1911
+ GType rt = query.return_type & ~G_SIGNAL_TYPE_STATIC_SCOPE;
1912
+ bool hasReturn = rt != G_TYPE_NONE && rt != G_TYPE_INVALID;
1913
+ GValue ret = G_VALUE_INIT;
1914
+ Napi::Value result = env.Undefined();
1915
+ if (ok) {
1916
+ if (hasReturn) g_value_init(&ret, rt);
1917
+ g_signal_emitv(params.data(), sigid, 0, hasReturn ? &ret : nullptr);
1918
+ if (hasReturn && !env.IsExceptionPending()) {
1919
+ result = GValueToJs(env, &ret);
1920
+ g_value_unset(&ret);
1921
+ } else if (hasReturn) {
1922
+ g_value_unset(&ret);
1923
+ }
1924
+ }
1925
+ for (guint j = 0; j < initialised; j++) g_value_unset(&params[j]);
1926
+ return ok ? result : env.Null();
1927
+ }
1928
+
1929
+ // disconnectSignal(handle, handlerId) -> void
1930
+ Napi::Value DisconnectSignal(const Napi::CallbackInfo& info) {
1931
+ Napi::Env env = info.Env();
1932
+ if (info.Length() < 2 || !info[1].IsNumber()) {
1933
+ Napi::TypeError::New(env, "disconnectSignal(handle, handlerId: number)").ThrowAsJavaScriptException();
1934
+ return env.Undefined();
1935
+ }
1936
+ GObject* obj = UnwrapGObject(env, info[0]);
1937
+ if (obj == nullptr) return env.Undefined();
1938
+ gulong id = static_cast<gulong>(info[1].As<Napi::Number>().Int64Value());
1939
+ if (id != 0 && g_signal_handler_is_connected(obj, id)) {
1940
+ g_signal_handler_disconnect(obj, id);
1941
+ }
1942
+ return env.Undefined();
1943
+ }
1944
+
1945
+ // ---- libuv <-> GLib main loop bridge (milestone: mainloop) ----
1946
+ //
1947
+ // Port of node-gtk's src/loop.cc (romgrk and contributors, MIT) to N-API. Nests
1948
+ // Node's libuv loop inside GLib's main loop: a GSource polls libuv's backend fd
1949
+ // and runs uv_run(UV_RUN_NOWAIT) on dispatch, so a blocking GLib main loop
1950
+ // (GLib.MainLoop.run / GApplication.run) keeps Node timers/promises/IO alive —
1951
+ // matching GJS, where the GLib loop IS the process loop. Nesting GLib inside uv
1952
+ // is impractical (uv exposes no external prepare/check hook), so we nest the
1953
+ // other way, exactly as node-gtk does.
1954
+ //
1955
+ // Main-thread only (worker_threads would need a per-context source); the GLib
1956
+ // default context is iterated on the same thread Node runs on.
1957
+ struct UvLoopSource {
1958
+ GSource source;
1959
+ uv_loop_t* loop;
1960
+ gpointer fd_tag;
1961
+ gboolean fd_polled;
1962
+ };
1963
+
1964
+ static napi_env g_loop_env = nullptr; // captured at startMainLoop (main thread)
1965
+ static gboolean g_loop_started = FALSE;
1966
+ static napi_ref g_process_ref = nullptr; // process
1967
+ static napi_ref g_tick_callback_ref = nullptr; // process._tickCallback
1968
+
1969
+ // Drain Node's nextTick queue + run a microtask checkpoint. process._tickCallback
1970
+ // invoked through napi_make_callback runs the tick queue, and the surrounding
1971
+ // callback scope's close performs the microtask checkpoint — the N-API analogue
1972
+ // of node-gtk's CallMicrotaskHandlers (process._tickCallback +
1973
+ // Isolate::PerformMicrotaskCheckpoint). Best-effort: skipped if a JS exception is
1974
+ // already pending (it will surface when the blocking run() returns).
1975
+ //
1976
+ // Limitation (node-gtk #442/#121): when the blocking run() is nested inside an
1977
+ // outer async callback scope (node:test, an await, a signal handler), V8 defers
1978
+ // the checkpoint to that outer scope, so promise continuations queued before the
1979
+ // run() do not drain until run() returns. nextTick still drains; timers/I/O the
1980
+ // loop dispatches are unaffected. The robust fix lives in L1 (defer the run() to
1981
+ // a macrotask when a microtask checkpoint is in progress).
1982
+ static void DrainMicrotasks() {
1983
+ if (g_loop_env == nullptr || g_tick_callback_ref == nullptr || g_process_ref == nullptr) return;
1984
+ napi_env env = g_loop_env;
1985
+ bool pending = false;
1986
+ if (napi_is_exception_pending(env, &pending) != napi_ok || pending) return;
1987
+
1988
+ napi_handle_scope scope;
1989
+ if (napi_open_handle_scope(env, &scope) != napi_ok) return;
1990
+ napi_value process_v = nullptr, tick = nullptr, result = nullptr;
1991
+ if (napi_get_reference_value(env, g_process_ref, &process_v) == napi_ok &&
1992
+ napi_get_reference_value(env, g_tick_callback_ref, &tick) == napi_ok &&
1993
+ process_v != nullptr && tick != nullptr) {
1994
+ napi_make_callback(env, nullptr, process_v, tick, 0, nullptr, &result);
1995
+ }
1996
+ napi_close_handle_scope(env, scope);
1997
+ }
1998
+
1999
+ static gboolean uv_source_prepare(GSource* base, gint* timeout) {
2000
+ UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
2001
+ uv_update_time(s->loop);
2002
+ DrainMicrotasks();
2003
+
2004
+ gboolean alive = uv_loop_alive(s->loop);
2005
+ // Toggle whether GLib polls uv's backend fd: an unref'd-but-active uv handle
2006
+ // keeps the backend fd perpetually ready, which would busy-spin GLib at 100%
2007
+ // CPU when the loop is otherwise dead. Mask the fd while dead so GLib actually
2008
+ // blocks until a GLib source wakes us; restore it the moment uv is alive again.
2009
+ if (s->fd_tag != nullptr && alive != s->fd_polled) {
2010
+ g_source_modify_unix_fd(
2011
+ &s->source, s->fd_tag,
2012
+ alive ? static_cast<GIOCondition>(G_IO_IN | G_IO_OUT | G_IO_ERR) : static_cast<GIOCondition>(0));
2013
+ s->fd_polled = alive;
2014
+ }
2015
+
2016
+ if (!alive) {
2017
+ *timeout = -1; // sleep until a GLib source wakes us
2018
+ return FALSE;
2019
+ }
2020
+ int t = uv_backend_timeout(s->loop);
2021
+ *timeout = t;
2022
+ return t == 0; // ready immediately when uv has work due now
2023
+ }
2024
+
2025
+ static gboolean uv_source_dispatch(GSource* base, GSourceFunc /*callback*/, gpointer /*user_data*/) {
2026
+ UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
2027
+ uv_run(s->loop, UV_RUN_NOWAIT);
2028
+ DrainMicrotasks();
2029
+ return G_SOURCE_CONTINUE;
2030
+ }
2031
+
2032
+ static GSourceFuncs uv_source_funcs = {
2033
+ uv_source_prepare, nullptr, uv_source_dispatch, nullptr, nullptr, nullptr,
2034
+ };
2035
+
2036
+ // startMainLoop() -> void
2037
+ // Attach the libuv-backed GSource to the default GLib main context (idempotent).
2038
+ // Harmless until a GLib main loop actually runs — it adds no uv handle, so it
2039
+ // neither keeps Node alive nor runs uv on its own; it only pumps uv while a GLib
2040
+ // loop is iterating. The L1 layer calls this once when a namespace is required.
2041
+ Napi::Value StartMainLoop(const Napi::CallbackInfo& info) {
2042
+ Napi::Env env = info.Env();
2043
+ if (g_loop_started) return env.Undefined();
2044
+
2045
+ uv_loop_t* loop = nullptr;
2046
+ if (napi_get_uv_event_loop(env, &loop) != napi_ok || loop == nullptr) {
2047
+ Napi::Error::New(env, "failed to obtain the libuv event loop").ThrowAsJavaScriptException();
2048
+ return env.Undefined();
2049
+ }
2050
+
2051
+ // Capture env + process._tickCallback for nextTick/microtask draining.
2052
+ g_loop_env = env;
2053
+ napi_value global = nullptr, process_v = nullptr, tick = nullptr;
2054
+ if (napi_get_global(env, &global) == napi_ok &&
2055
+ napi_get_named_property(env, global, "process", &process_v) == napi_ok &&
2056
+ process_v != nullptr) {
2057
+ napi_create_reference(env, process_v, 1, &g_process_ref);
2058
+ if (napi_get_named_property(env, process_v, "_tickCallback", &tick) == napi_ok) {
2059
+ napi_valuetype vt;
2060
+ if (napi_typeof(env, tick, &vt) == napi_ok && vt == napi_function) {
2061
+ napi_create_reference(env, tick, 1, &g_tick_callback_ref);
2062
+ }
2063
+ }
2064
+ }
2065
+
2066
+ GSource* source = g_source_new(&uv_source_funcs, sizeof(UvLoopSource));
2067
+ UvLoopSource* s = reinterpret_cast<UvLoopSource*>(source);
2068
+ s->loop = loop;
2069
+ s->fd_polled = TRUE;
2070
+ // uv_backend_fd is the epoll/kqueue fd on POSIX. (Windows uses a different
2071
+ // wake mechanism — node-gtk guards it; this milestone targets Linux/Fedora.)
2072
+ s->fd_tag = g_source_add_unix_fd(source, uv_backend_fd(loop),
2073
+ static_cast<GIOCondition>(G_IO_IN | G_IO_OUT | G_IO_ERR));
2074
+ g_source_attach(source, nullptr); // default GLib main context
2075
+ g_source_unref(source); // the context holds the surviving ref
2076
+
2077
+ g_loop_started = TRUE;
2078
+ return env.Undefined();
2079
+ }
2080
+
2081
+ } // namespace
2082
+
2083
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
2084
+ exports.Set("requireNamespace", Napi::Function::New(env, RequireNamespace));
2085
+ exports.Set("listInfoNames", Napi::Function::New(env, ListInfoNames));
2086
+ exports.Set("findInfo", Napi::Function::New(env, FindInfo));
2087
+ exports.Set("getConstantValue", Napi::Function::New(env, GetConstantValue));
2088
+ exports.Set("getEnumValues", Napi::Function::New(env, GetEnumValues));
2089
+ exports.Set("prependSearchPath", Napi::Function::New(env, PrependSearchPath));
2090
+ exports.Set("callFunction", Napi::Function::New(env, CallFunction));
2091
+ exports.Set("callMethod", Napi::Function::New(env, CallMethod));
2092
+ exports.Set("callStaticMethod", Napi::Function::New(env, CallStaticMethod));
2093
+ exports.Set("newObject", Napi::Function::New(env, NewObject));
2094
+ exports.Set("registerClass", Napi::Function::New(env, RegisterClass));
2095
+ exports.Set("constructType", Napi::Function::New(env, ConstructType));
2096
+ exports.Set("getProperty", Napi::Function::New(env, GetProperty));
2097
+ exports.Set("setProperty", Napi::Function::New(env, SetProperty));
2098
+ exports.Set("hasProperty", Napi::Function::New(env, HasProperty));
2099
+ exports.Set("getTypeName", Napi::Function::New(env, GetTypeName));
2100
+ exports.Set("isGObjectHandle", Napi::Function::New(env, IsGObjectHandle));
2101
+ exports.Set("callBoxedMethod", Napi::Function::New(env, CallBoxedMethod));
2102
+ exports.Set("isBoxedHandle", Napi::Function::New(env, IsBoxedHandle));
2103
+ exports.Set("startMainLoop", Napi::Function::New(env, StartMainLoop));
2104
+ exports.Set("connectSignal", Napi::Function::New(env, ConnectSignal));
2105
+ exports.Set("emitSignal", Napi::Function::New(env, EmitSignal));
2106
+ exports.Set("disconnectSignal", Napi::Function::New(env, DisconnectSignal));
2107
+ return exports;
2108
+ }
2109
+
2110
+ NODE_API_MODULE(node_gi, Init)