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