@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/marshal.cc
ADDED
|
@@ -0,0 +1,1738 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Value marshalling: GIArgument <-> JS, boxed/GType handles, array/GList/GSList/GHashTable containers.
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
#include <unordered_map>
|
|
7
|
+
#include <utility>
|
|
8
|
+
|
|
9
|
+
namespace nodegi {
|
|
10
|
+
|
|
11
|
+
// ---- foreign-struct registry (cairo seam) -----------------------------------
|
|
12
|
+
//
|
|
13
|
+
// Port of GJS's gi/foreign.cpp table (keyed by (namespace, type_name)). A module
|
|
14
|
+
// (cairo.cc) registers converters for its foreign structs; the INTERFACE
|
|
15
|
+
// marshalling branches below route a foreign-typed arg/return to them instead of
|
|
16
|
+
// the generic boxed path. Populated at addon init (before any GI call marshals a
|
|
17
|
+
// foreign type), then read-only — no lock needed.
|
|
18
|
+
using ForeignKey = std::pair<std::string, std::string>;
|
|
19
|
+
struct ForeignKeyHash {
|
|
20
|
+
size_t operator()(const ForeignKey& k) const {
|
|
21
|
+
return std::hash<std::string>()(k.first) ^ (std::hash<std::string>()(k.second) << 1);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
static std::unordered_map<ForeignKey, const ForeignStructOps*, ForeignKeyHash> g_foreign_structs;
|
|
25
|
+
|
|
26
|
+
void RegisterForeignStruct(const char* ns, const char* type_name, const ForeignStructOps* ops) {
|
|
27
|
+
g_foreign_structs[{ns != nullptr ? ns : "", type_name != nullptr ? type_name : ""}] = ops;
|
|
28
|
+
}
|
|
29
|
+
const ForeignStructOps* LookupForeignStruct(const char* ns, const char* type_name) {
|
|
30
|
+
auto it = g_foreign_structs.find({ns != nullptr ? ns : "", type_name != nullptr ? type_name : ""});
|
|
31
|
+
return it == g_foreign_structs.end() ? nullptr : it->second;
|
|
32
|
+
}
|
|
33
|
+
const ForeignStructOps* ForeignOpsForInfo(GIBaseInfo* iface) {
|
|
34
|
+
if (iface == nullptr || !GI_IS_STRUCT_INFO(iface)) return nullptr;
|
|
35
|
+
if (!gi_struct_info_is_foreign(reinterpret_cast<GIStructInfo*>(iface))) return nullptr;
|
|
36
|
+
return LookupForeignStruct(gi_base_info_get_namespace(iface), gi_base_info_get_name(iface));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---- value marshalling (milestone 1: primitives + strings) ----
|
|
40
|
+
//
|
|
41
|
+
// The minimal GIArgument <-> JS boundary. This is the seam node-gtk's value.cc
|
|
42
|
+
// fills out exhaustively; here it covers the numeric + string + boolean tags so
|
|
43
|
+
// real GI function calls work end to end. Compound tags (ARRAY/INTERFACE/GLIST/
|
|
44
|
+
// GHASH/…) and OUT/INOUT directions land with the GObject + full-marshalling
|
|
45
|
+
// drops.
|
|
46
|
+
|
|
47
|
+
// Marshal a JS value into a GIArgument for an IN argument of `type`.
|
|
48
|
+
// `heldString` keeps UTF-8 storage alive for the duration of the call.
|
|
49
|
+
|
|
50
|
+
extern const napi_type_tag kBoxedHandleTag = {0x6d2f8c4b1a9e7350ULL,
|
|
51
|
+
0xb7e1d3a5c9f08264ULL};
|
|
52
|
+
|
|
53
|
+
// Release a BoxedHandle record + what it owns. Shared by the External finalizer
|
|
54
|
+
// and the swallowed-failure bail below (which must free exactly what the never-
|
|
55
|
+
// registered finalizer would have).
|
|
56
|
+
static void FreeBoxedHandleRecord(BoxedHandle* h) {
|
|
57
|
+
if (h->owns && h->ptr != nullptr && h->gtype != G_TYPE_INVALID) {
|
|
58
|
+
// GVariant is a fundamental (NOT a boxed) GType, so it cannot go
|
|
59
|
+
// through g_boxed_free — it is reference-counted via g_variant_unref.
|
|
60
|
+
// (GLib.Variant flows through this handle so it reuses the boxed
|
|
61
|
+
// method-resolution + IN-arg-unwrap path; only the free differs.)
|
|
62
|
+
if (h->gtype == G_TYPE_VARIANT) {
|
|
63
|
+
g_variant_unref(static_cast<GVariant*>(h->ptr));
|
|
64
|
+
} else if (G_TYPE_IS_BOXED(h->gtype)) {
|
|
65
|
+
g_boxed_free(h->gtype, h->ptr);
|
|
66
|
+
}
|
|
67
|
+
} else if (h->rawOwned && h->ptr != nullptr) {
|
|
68
|
+
// A plain (non-boxed) C struct whose storage the engine g_malloc0'd itself —
|
|
69
|
+
// the caller-allocates OUT case. No boxed free-func exists; the handle owns
|
|
70
|
+
// the block (matches gjs CallerAllocatesOut::release, a plain g_free).
|
|
71
|
+
g_free(h->ptr);
|
|
72
|
+
}
|
|
73
|
+
if (h->info != nullptr) gi_base_info_unref(h->info);
|
|
74
|
+
delete h;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Napi::Value MakeBoxedHandle(Napi::Env env, gpointer ptr, GType gtype, bool owns,
|
|
78
|
+
GIBaseInfo* info, bool rawOwned) {
|
|
79
|
+
GIBaseInfo* heldInfo = info != nullptr ? gi_base_info_ref(info) : nullptr;
|
|
80
|
+
BoxedHandle* bh = new BoxedHandle{ptr, gtype, owns, heldInfo, rawOwned};
|
|
81
|
+
Napi::External<BoxedHandle> ext = Napi::External<BoxedHandle>::New(
|
|
82
|
+
env, bh, [](Napi::Env, BoxedHandle* h) { FreeBoxedHandleRecord(h); });
|
|
83
|
+
if (ext.IsEmpty()) {
|
|
84
|
+
// napi_create_external failed with the throw swallowed (terminating env /
|
|
85
|
+
// pending exception). The finalizer was never registered — free the record
|
|
86
|
+
// here and bail instead of chaining TypeTag on the empty External, which
|
|
87
|
+
// would abort via Error::New(nullptr)'s fatal sites (see common.h helpers).
|
|
88
|
+
FreeBoxedHandleRecord(bh);
|
|
89
|
+
return ext;
|
|
90
|
+
}
|
|
91
|
+
ext.TypeTag(&kBoxedHandleTag);
|
|
92
|
+
return ext;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Wrap a GVariant pointer as a node-gi boxed handle tagged with G_TYPE_VARIANT.
|
|
96
|
+
// GVariant is a fundamental ref-counted (de)floating type, so ownership differs
|
|
97
|
+
// from g_boxed types: TAKE the handed ref on transfer-full (sinking a floating
|
|
98
|
+
// one), else add our own ref on a borrow. The finalizer (MakeBoxedHandle) drops
|
|
99
|
+
// exactly the one ref we end up owning via g_variant_unref. The L1 layer turns
|
|
100
|
+
// this handle into a GLib.Variant wrapper (deepUnpack/unpack/recursiveUnpack/…).
|
|
101
|
+
Napi::Value WrapVariant(Napi::Env env, GVariant* var, GITransfer transfer) {
|
|
102
|
+
if (var == nullptr) return env.Null();
|
|
103
|
+
if (transfer == GI_TRANSFER_EVERYTHING)
|
|
104
|
+
g_variant_take_ref(var);
|
|
105
|
+
else
|
|
106
|
+
g_variant_ref_sink(var);
|
|
107
|
+
return MakeBoxedHandle(env, var, G_TYPE_VARIANT, true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Wrap a returned struct/boxed pointer. `structInfo` is the GIStructInfo (or
|
|
111
|
+
// union info) for the static type; the runtime GType (if registered + boxed)
|
|
112
|
+
// drives ownership + later method resolution.
|
|
113
|
+
static Napi::Value WrapBoxed(Napi::Env env, gpointer ptr, GIBaseInfo* structInfo,
|
|
114
|
+
GITransfer transfer) {
|
|
115
|
+
if (ptr == nullptr) return env.Null();
|
|
116
|
+
GIBaseInfo* info = nullptr;
|
|
117
|
+
GType gt = G_TYPE_INVALID;
|
|
118
|
+
if (structInfo != nullptr && (GI_IS_STRUCT_INFO(structInfo) || GI_IS_UNION_INFO(structInfo))) {
|
|
119
|
+
info = structInfo; // stored (re-ref'd) so field/method resolution has the static info
|
|
120
|
+
gt = gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(structInfo));
|
|
121
|
+
}
|
|
122
|
+
if (gt == G_TYPE_VARIANT) return WrapVariant(env, static_cast<GVariant*>(ptr), transfer);
|
|
123
|
+
bool boxed = gt != G_TYPE_INVALID && gt != G_TYPE_NONE && G_TYPE_IS_BOXED(gt);
|
|
124
|
+
// COPY a transfer-none BOXED return so the JS handle owns an independent copy —
|
|
125
|
+
// matching gjs (gi/boxed.cpp: a transfer-none boxed is g_boxed_copy'd). Without
|
|
126
|
+
// this the handle shares the callee's (often static) instance, so a field WRITE
|
|
127
|
+
// (union.long_ = …) would corrupt that shared instance across calls. A
|
|
128
|
+
// transfer-everything boxed is already owned; a non-boxed struct has no copy
|
|
129
|
+
// function, so it keeps sharing (reads only — its fields aren't mutated here).
|
|
130
|
+
gpointer handlePtr = ptr;
|
|
131
|
+
bool owns = false;
|
|
132
|
+
if (boxed) {
|
|
133
|
+
handlePtr = transfer == GI_TRANSFER_EVERYTHING ? ptr : g_boxed_copy(gt, ptr);
|
|
134
|
+
owns = true;
|
|
135
|
+
}
|
|
136
|
+
// Keep a valid registered GType on the handle even for a NON-boxed struct (e.g.
|
|
137
|
+
// GIMarshalling PointerStruct): it drives method + field resolution via
|
|
138
|
+
// find_by_gtype. `owns` stays gated on boxed-ness, so the finalizer only ever
|
|
139
|
+
// g_boxed_free's a real boxed type — never a plain registered struct.
|
|
140
|
+
GType handleGType = (gt != G_TYPE_INVALID && gt != G_TYPE_NONE) ? gt : G_TYPE_INVALID;
|
|
141
|
+
return MakeBoxedHandle(env, handlePtr, handleGType, owns, info);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Read a boxed handle (ptr + boxed GType) if `v` is one (tag-checked; no deref of
|
|
145
|
+
// ptr). Returns nullptr when `v` is not a node-gi boxed handle. The returned handle
|
|
146
|
+
// is owned by the External — never free it. Callers that need the GType (boxed
|
|
147
|
+
// type-safety checks before g_value_set_boxed / a struct IN arg) use this; the
|
|
148
|
+
// pointer-only TryGetBoxedPtr below is the thin wrapper for everyone else.
|
|
149
|
+
BoxedHandle* TryGetBoxedHandle(Napi::Value v) {
|
|
150
|
+
if (!v.IsExternal()) return nullptr;
|
|
151
|
+
Napi::External<BoxedHandle> ext = v.As<Napi::External<BoxedHandle>>();
|
|
152
|
+
if (!ext.CheckTypeTag(&kBoxedHandleTag)) return nullptr;
|
|
153
|
+
return ext.Data();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Read a boxed handle's pointer if `v` is one (tag-checked; no deref of ptr).
|
|
157
|
+
bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
|
|
158
|
+
if (!v.IsExternal()) return false;
|
|
159
|
+
Napi::External<BoxedHandle> ext = v.As<Napi::External<BoxedHandle>>();
|
|
160
|
+
if (!ext.CheckTypeTag(&kBoxedHandleTag)) return false;
|
|
161
|
+
BoxedHandle* h = ext.Data();
|
|
162
|
+
*out = h != nullptr ? h->ptr : nullptr;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// boxedTypeName(value) -> the boxed handle's GType name (e.g. "GBytes"), or null
|
|
167
|
+
// when the handle carries no registered GType. Lets the L1 layer attach a
|
|
168
|
+
// type-specific convenience (GLib.Bytes.toArray) without a per-type wrapper.
|
|
169
|
+
Napi::Value BoxedTypeName(const Napi::CallbackInfo& info) {
|
|
170
|
+
Napi::Env env = info.Env();
|
|
171
|
+
if (info.Length() < 1 || !info[0].IsExternal() ||
|
|
172
|
+
!info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag)) {
|
|
173
|
+
return env.Null();
|
|
174
|
+
}
|
|
175
|
+
BoxedHandle* bh = info[0].As<Napi::External<BoxedHandle>>().Data();
|
|
176
|
+
if (bh == nullptr || bh->gtype == G_TYPE_INVALID) return env.Null();
|
|
177
|
+
const char* name = g_type_name(bh->gtype);
|
|
178
|
+
return name != nullptr ? Napi::Value(Napi::String::New(env, name)) : env.Null();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- GType value handle ----
|
|
182
|
+
//
|
|
183
|
+
// A GType is represented in JS as a dedicated, type-tagged External holding the
|
|
184
|
+
// `GType` (a `gsize`) directly in its Data() pointer slot — NOT a plain number
|
|
185
|
+
// (a number is indistinguishable from an enum value at a GI_TYPE_TAG_GTYPE arg)
|
|
186
|
+
// and NOT a GObject/boxed handle (a GType is a small integer, dereferencing it as
|
|
187
|
+
// a pointer would crash). The tag lets the marshaller recognise it structurally
|
|
188
|
+
// without touching the value. GJS's richer GType *object* (refs/gjs gi/gtype.cpp)
|
|
189
|
+
// carries a name slot too; node-gi's tagged External is the minimal equivalent —
|
|
190
|
+
// the L1 layer can layer `.name` ergonomics on top if needed. GTypes are
|
|
191
|
+
// process-stable (static registration, never freed), so the handle has no
|
|
192
|
+
// finalizer. This is also the handle registerClass returns (so a registered
|
|
193
|
+
// class's `$gtype` and the constructType type-handle are one and the same).
|
|
194
|
+
static const napi_type_tag kGTypeHandleTag = {0x3a7f1c9e5b2d4860ULL,
|
|
195
|
+
0xc4e8a1b3d5f72096ULL};
|
|
196
|
+
|
|
197
|
+
Napi::Value MakeGTypeHandle(Napi::Env env, GType gtype) {
|
|
198
|
+
Napi::External<void> ext = Napi::External<void>::New(env, reinterpret_cast<void*>(gtype));
|
|
199
|
+
// Swallowed napi failure (terminating env): TypeTag on the empty External
|
|
200
|
+
// would abort via Error::New(nullptr) — bail with the empty value instead.
|
|
201
|
+
if (ext.IsEmpty()) return ext;
|
|
202
|
+
ext.TypeTag(&kGTypeHandleTag);
|
|
203
|
+
return ext;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Read a GType from a kGTypeHandleTag External (the GType representation above),
|
|
207
|
+
// without dereferencing. Returns 0 (G_TYPE_INVALID) when `v` is not a GType handle.
|
|
208
|
+
GType ReadGTypeHandle(Napi::Value v) {
|
|
209
|
+
if (!v.IsExternal()) return 0;
|
|
210
|
+
Napi::External<void> ext = v.As<Napi::External<void>>();
|
|
211
|
+
if (!ext.CheckTypeTag(&kGTypeHandleTag)) return 0;
|
|
212
|
+
return reinterpret_cast<GType>(ext.Data());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Marshal a JS GType argument (a GType handle, or null → 0) into a GType, throwing
|
|
216
|
+
// a clean TypeError otherwise. Shared by the GI_TYPE_TAG_GTYPE / G_TYPE_GTYPE
|
|
217
|
+
// marshalling paths.
|
|
218
|
+
bool UnwrapGTypeArg(Napi::Env env, Napi::Value v, GType* out) {
|
|
219
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
220
|
+
*out = 0;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (v.IsExternal() && v.As<Napi::External<void>>().CheckTypeTag(&kGTypeHandleTag)) {
|
|
224
|
+
*out = reinterpret_cast<GType>(v.As<Napi::External<void>>().Data());
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
Napi::TypeError::New(env, "expected a GType handle (e.g. Class.$gtype)")
|
|
228
|
+
.ThrowAsJavaScriptException();
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Byte width of one element of a napi typed array.
|
|
233
|
+
static size_t TypedArrayElementSize(napi_typedarray_type t) {
|
|
234
|
+
switch (t) {
|
|
235
|
+
case napi_int8_array:
|
|
236
|
+
case napi_uint8_array:
|
|
237
|
+
case napi_uint8_clamped_array: return 1;
|
|
238
|
+
case napi_int16_array:
|
|
239
|
+
case napi_uint16_array: return 2;
|
|
240
|
+
case napi_int32_array:
|
|
241
|
+
case napi_uint32_array:
|
|
242
|
+
case napi_float32_array: return 4;
|
|
243
|
+
case napi_float64_array:
|
|
244
|
+
case napi_bigint64_array:
|
|
245
|
+
case napi_biguint64_array: return 8;
|
|
246
|
+
default: return 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Extract the byte slice of a JS binary value — a TypedArray (incl. a Node
|
|
251
|
+
// Buffer and Uint8ClampedArray), a DataView, or a bare ArrayBuffer. Returns
|
|
252
|
+
// false when `v` is none of those. The slice borrows the JS backing store; it
|
|
253
|
+
// is only valid until control returns to JS (copy before storing).
|
|
254
|
+
//
|
|
255
|
+
// CROSS-RUNTIME: read the `data` out-param of napi_get_typedarray_info /
|
|
256
|
+
// napi_get_dataview_info — it already points at the VIEW's first byte on every
|
|
257
|
+
// runtime. Do NOT recompute `arraybuffer.Data() + byte_offset`: Bun reports a
|
|
258
|
+
// byte_offset inconsistent with its arraybuffer data pointer for subarray
|
|
259
|
+
// views, which silently marshals the wrong slice (caught by the bytes-in
|
|
260
|
+
// conformance program's `subarray` case on bun).
|
|
261
|
+
static bool JsBinaryData(Napi::Env env, Napi::Value v, const uint8_t** data, size_t* len) {
|
|
262
|
+
napi_value val = v;
|
|
263
|
+
bool is = false;
|
|
264
|
+
if (napi_is_typedarray(env, val, &is) == napi_ok && is) {
|
|
265
|
+
napi_typedarray_type type = napi_uint8_array;
|
|
266
|
+
size_t length = 0;
|
|
267
|
+
void* d = nullptr;
|
|
268
|
+
if (napi_get_typedarray_info(env, val, &type, &length, &d, nullptr, nullptr) != napi_ok)
|
|
269
|
+
return false;
|
|
270
|
+
*data = static_cast<const uint8_t*>(d);
|
|
271
|
+
*len = length * TypedArrayElementSize(type);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (napi_is_dataview(env, val, &is) == napi_ok && is) {
|
|
275
|
+
size_t byteLength = 0;
|
|
276
|
+
void* d = nullptr;
|
|
277
|
+
if (napi_get_dataview_info(env, val, &byteLength, &d, nullptr, nullptr) != napi_ok)
|
|
278
|
+
return false;
|
|
279
|
+
*data = static_cast<const uint8_t*>(d);
|
|
280
|
+
*len = byteLength;
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (napi_is_arraybuffer(env, val, &is) == napi_ok && is) {
|
|
284
|
+
void* d = nullptr;
|
|
285
|
+
size_t byteLength = 0;
|
|
286
|
+
if (napi_get_arraybuffer_info(env, val, &d, &byteLength) != napi_ok) return false;
|
|
287
|
+
*data = static_cast<const uint8_t*>(d);
|
|
288
|
+
*len = byteLength;
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// `ownedStrings` (optional): when a transfer-full string IN/INOUT arg is g_strdup'd
|
|
295
|
+
// here, the freshly-allocated pointer is appended so the caller can g_free it if the
|
|
296
|
+
// invoke never adopts it (an arg-marshal error before the call, or a failed invoke).
|
|
297
|
+
// nullptr (the default) → no tracking, for the vfunc-return / signal-arg callers.
|
|
298
|
+
bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument* out,
|
|
299
|
+
std::string* heldString,
|
|
300
|
+
GITransfer transfer,
|
|
301
|
+
std::vector<gpointer>* ownedStrings,
|
|
302
|
+
CreatedClosures* closures,
|
|
303
|
+
CreatedBytes* bytes) {
|
|
304
|
+
if (v.IsEmpty()) {
|
|
305
|
+
// Residue of a swallowed napi failure (a fallible Get()/coercion upstream
|
|
306
|
+
// failed on a terminating env, or a throwing getter left the exception
|
|
307
|
+
// pending). Nothing can be marshalled from it, and coercing it would abort
|
|
308
|
+
// via Error::New(nullptr)'s fatal sites — fail the marshal cleanly. Gate the
|
|
309
|
+
// diagnostic THROW on NodeGiJsAvailable: it is false both on a dying env
|
|
310
|
+
// (worker.terminate mid-call — constructing a Napi::Error there runs fallible
|
|
311
|
+
// napi calls that abort via the SAME funnel) AND on a live env with an
|
|
312
|
+
// exception already pending (propagate that one). Only throw when the env can
|
|
313
|
+
// safely build the error.
|
|
314
|
+
if (NodeGiJsAvailable(env)) {
|
|
315
|
+
Napi::Error::New(env, "argument value unavailable (env is terminating)")
|
|
316
|
+
.ThrowAsJavaScriptException();
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
GITypeTag tag = gi_type_info_get_tag(type);
|
|
321
|
+
switch (tag) {
|
|
322
|
+
case GI_TYPE_TAG_BOOLEAN: out->v_boolean = NodeGiToBool(v); return true;
|
|
323
|
+
case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(NodeGiToInt32(v)); return true;
|
|
324
|
+
case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(NodeGiToUint32(v)); return true;
|
|
325
|
+
case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(NodeGiToInt32(v)); return true;
|
|
326
|
+
case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(NodeGiToUint32(v)); return true;
|
|
327
|
+
case GI_TYPE_TAG_INT32: out->v_int32 = NodeGiToInt32(v); return true;
|
|
328
|
+
case GI_TYPE_TAG_UINT32: out->v_uint32 = NodeGiToUint32(v); return true;
|
|
329
|
+
// 64-bit: accept a BigInt losslessly (else a Number, truncated) — never let a
|
|
330
|
+
// BigInt reach ToNumber() (fatal abort). See JsValueTo{Int,Uint}64 in common.h.
|
|
331
|
+
case GI_TYPE_TAG_INT64: out->v_int64 = JsValueToInt64(v); return true;
|
|
332
|
+
case GI_TYPE_TAG_UINT64: out->v_uint64 = JsValueToUint64(v); return true;
|
|
333
|
+
case GI_TYPE_TAG_FLOAT: out->v_float = static_cast<float>(NodeGiToDouble(v)); return true;
|
|
334
|
+
case GI_TYPE_TAG_DOUBLE: out->v_double = NodeGiToDouble(v); return true;
|
|
335
|
+
case GI_TYPE_TAG_UTF8:
|
|
336
|
+
case GI_TYPE_TAG_FILENAME:
|
|
337
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
338
|
+
out->v_string = nullptr;
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (transfer == GI_TRANSFER_EVERYTHING) {
|
|
342
|
+
// The callee adopts the string and g_free's it. heldString points into a
|
|
343
|
+
// std::string buffer (NOT g_malloc'd) → an invalid free. Hand over a
|
|
344
|
+
// g_strdup'd copy the callee can legally free; we keep no reference. Track
|
|
345
|
+
// it so a NON-adopting caller (error before / failed invoke) can free it.
|
|
346
|
+
out->v_string = g_strdup(NodeGiToUtf8(v).c_str());
|
|
347
|
+
if (ownedStrings != nullptr) ownedStrings->push_back(out->v_string);
|
|
348
|
+
} else {
|
|
349
|
+
*heldString = NodeGiToUtf8(v);
|
|
350
|
+
out->v_string = const_cast<char*>(heldString->c_str());
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
case GI_TYPE_TAG_INTERFACE: {
|
|
354
|
+
// Object/interface instances arrive as opaque External<GObject> handles;
|
|
355
|
+
// enums/flags as plain numbers. Other interface kinds (structs/unions/
|
|
356
|
+
// callbacks) follow with the full-marshalling drop.
|
|
357
|
+
GIBaseInfo* iface = gi_type_info_get_interface(type);
|
|
358
|
+
bool handled = false;
|
|
359
|
+
if (iface != nullptr) {
|
|
360
|
+
if (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface)) {
|
|
361
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
362
|
+
out->v_pointer = nullptr;
|
|
363
|
+
handled = true;
|
|
364
|
+
} else if (v.IsExternal()) {
|
|
365
|
+
out->v_pointer = v.As<Napi::External<GObject>>().Data();
|
|
366
|
+
handled = true;
|
|
367
|
+
}
|
|
368
|
+
} else if (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface)) {
|
|
369
|
+
out->v_int = NodeGiToInt32(v);
|
|
370
|
+
handled = true;
|
|
371
|
+
} else if (GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface)) {
|
|
372
|
+
// A foreign struct (cairo Context/Surface/Pattern) marshals through its
|
|
373
|
+
// module's converter, not the boxed path.
|
|
374
|
+
const ForeignStructOps* fops = ForeignOpsForInfo(iface);
|
|
375
|
+
if (fops != nullptr) {
|
|
376
|
+
if (!fops->to(env, v, transfer, out)) {
|
|
377
|
+
gi_base_info_unref(iface);
|
|
378
|
+
return false; // to() already threw
|
|
379
|
+
}
|
|
380
|
+
handled = true;
|
|
381
|
+
} else if (v.IsFunction() && closures != nullptr &&
|
|
382
|
+
gi_registered_type_info_get_g_type(
|
|
383
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(iface)) == G_TYPE_CLOSURE) {
|
|
384
|
+
// A JS function for a `GObject.Closure` IN-arg → a real marshaled
|
|
385
|
+
// GClosure, exactly as gjs (refs/gjs/gi/arg-cache.cpp
|
|
386
|
+
// GClosureInTransferNone::in: create_marshaled + g_closure_ref +
|
|
387
|
+
// g_closure_sink). The ref taken here is released by calls.cc after
|
|
388
|
+
// the invoke for transfer-none (gjs BoxedInTransferNone::release —
|
|
389
|
+
// the callee keeps its own ref if it stored the closure), and left
|
|
390
|
+
// with the callee for transfer-full (gjs GClosureIn::release skips).
|
|
391
|
+
// An existing GObject.Closure boxed handle still routes through the
|
|
392
|
+
// boxed-handle branch below.
|
|
393
|
+
GClosure* closure = NodeGiMakeGenericJsClosure(env, v);
|
|
394
|
+
g_closure_ref(closure);
|
|
395
|
+
g_closure_sink(closure);
|
|
396
|
+
out->v_pointer = closure;
|
|
397
|
+
if (transfer == GI_TRANSFER_NOTHING) closures->transferNone.push_back(closure);
|
|
398
|
+
else closures->transferFull.push_back(closure);
|
|
399
|
+
handled = true;
|
|
400
|
+
} else if (bytes != nullptr &&
|
|
401
|
+
(v.IsTypedArray() || v.IsDataView() || v.IsArrayBuffer()) &&
|
|
402
|
+
g_type_is_a(gi_registered_type_info_get_g_type(
|
|
403
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(iface)),
|
|
404
|
+
G_TYPE_BYTES)) {
|
|
405
|
+
// JS bytes for a `GLib.Bytes` IN-arg → a fresh GBytes COPY, exactly
|
|
406
|
+
// as gjs (refs/gjs/gi/arg-cache.cpp GBytesIn::in Uint8Array path →
|
|
407
|
+
// gjs_byte_array_get_bytes → g_bytes_new). Recorded in `bytes` so
|
|
408
|
+
// calls.cc releases the fresh ref per transfer after the invoke
|
|
409
|
+
// (transfer-none: always unref — the callee ref'd it if it kept it;
|
|
410
|
+
// transfer-full: the callee adopts it). An existing GLib.Bytes boxed
|
|
411
|
+
// handle still routes through the boxed-handle branch below.
|
|
412
|
+
const uint8_t* data = nullptr;
|
|
413
|
+
size_t len = 0;
|
|
414
|
+
JsBinaryData(env, v, &data, &len);
|
|
415
|
+
GBytes* b = g_bytes_new(data, len);
|
|
416
|
+
out->v_pointer = b;
|
|
417
|
+
if (transfer == GI_TRANSFER_NOTHING) bytes->transferNone.push_back(b);
|
|
418
|
+
else bytes->transferFull.push_back(b);
|
|
419
|
+
handled = true;
|
|
420
|
+
} else if (v.IsNull() || v.IsUndefined()) {
|
|
421
|
+
// Boxed/struct IN args arrive as boxed handles; null/undefined maps to
|
|
422
|
+
// a NULL pointer (e.g. GLib.MainLoop.new(null, false)).
|
|
423
|
+
out->v_pointer = nullptr;
|
|
424
|
+
handled = true;
|
|
425
|
+
} else {
|
|
426
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
427
|
+
if (h != nullptr) {
|
|
428
|
+
// Type-safety: handing a wrong boxed handle's raw pointer to the
|
|
429
|
+
// callee is undefined behaviour. Reject a mismatch with a clean
|
|
430
|
+
// TypeError. Only checkable when BOTH the handle and the expected
|
|
431
|
+
// struct carry a known registered GType (non-registered C structs are
|
|
432
|
+
// G_TYPE_INVALID → no GType to compare, fall through as before).
|
|
433
|
+
GType expected = gi_registered_type_info_get_g_type(
|
|
434
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(iface));
|
|
435
|
+
if (h->gtype != G_TYPE_INVALID && expected != G_TYPE_INVALID &&
|
|
436
|
+
expected != G_TYPE_NONE && !g_type_is_a(h->gtype, expected)) {
|
|
437
|
+
gi_base_info_unref(iface);
|
|
438
|
+
Napi::TypeError::New(env, std::string("expected a ") + g_type_name(expected) +
|
|
439
|
+
" boxed handle, got " + g_type_name(h->gtype))
|
|
440
|
+
.ThrowAsJavaScriptException();
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
out->v_pointer = h->ptr;
|
|
444
|
+
handled = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
gi_base_info_unref(iface);
|
|
449
|
+
}
|
|
450
|
+
if (!handled) {
|
|
451
|
+
Napi::TypeError::New(
|
|
452
|
+
env,
|
|
453
|
+
"Unsupported interface IN argument (expected a GObject/boxed handle, enum or flags number)")
|
|
454
|
+
.ThrowAsJavaScriptException();
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
case GI_TYPE_TAG_GTYPE: {
|
|
460
|
+
// A GType argument (e.g. GObject.type_ensure, g_type_name): read the GType
|
|
461
|
+
// from a node-gi GType handle and write it into the GType slot (gsize-wide).
|
|
462
|
+
GType gt = 0;
|
|
463
|
+
if (!UnwrapGTypeArg(env, v, >)) return false;
|
|
464
|
+
out->v_size = static_cast<gsize>(gt);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
default:
|
|
468
|
+
Napi::TypeError::New(
|
|
469
|
+
env, "Unsupported IN argument type tag " + std::to_string(static_cast<int>(tag)) +
|
|
470
|
+
" (milestone 1 supports numbers, booleans and strings)")
|
|
471
|
+
.ThrowAsJavaScriptException();
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// A process-unique type tag distinguishing node-gi's GObject-instance Externals
|
|
477
|
+
// from other Externals (notably registerClass's GType handle). isGObjectHandle /
|
|
478
|
+
// UnwrapGObject validate against it WITHOUT dereferencing the pointer — a GType
|
|
479
|
+
// handle holds a small integer, not a valid GObject*, so a blind G_IS_OBJECT on
|
|
480
|
+
// it would segfault.
|
|
481
|
+
extern const napi_type_tag kGObjectHandleTag = {0x9f3c1a7b5e2d4068ULL,
|
|
482
|
+
0xa1b2c3d4e5f60718ULL};
|
|
483
|
+
|
|
484
|
+
// Marshal a return-value GIArgument into a JS value, honouring transfer.
|
|
485
|
+
Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
486
|
+
GITransfer transfer) {
|
|
487
|
+
GITypeTag tag = gi_type_info_get_tag(type);
|
|
488
|
+
switch (tag) {
|
|
489
|
+
case GI_TYPE_TAG_VOID: return env.Undefined();
|
|
490
|
+
case GI_TYPE_TAG_BOOLEAN: return Napi::Boolean::New(env, arg->v_boolean);
|
|
491
|
+
case GI_TYPE_TAG_INT8: return Napi::Number::New(env, arg->v_int8);
|
|
492
|
+
case GI_TYPE_TAG_UINT8: return Napi::Number::New(env, arg->v_uint8);
|
|
493
|
+
case GI_TYPE_TAG_INT16: return Napi::Number::New(env, arg->v_int16);
|
|
494
|
+
case GI_TYPE_TAG_UINT16: return Napi::Number::New(env, arg->v_uint16);
|
|
495
|
+
case GI_TYPE_TAG_INT32: return Napi::Number::New(env, arg->v_int32);
|
|
496
|
+
case GI_TYPE_TAG_UINT32: return Napi::Number::New(env, arg->v_uint32);
|
|
497
|
+
// 64-bit: GJS always returns a Number, warning when the value is not exactly
|
|
498
|
+
// representable (|v| > 2^53-1). See WarnIfUnsafe{Int,Uint}64 in common.h.
|
|
499
|
+
case GI_TYPE_TAG_INT64:
|
|
500
|
+
WarnIfUnsafeInt64(arg->v_int64);
|
|
501
|
+
return Napi::Number::New(env, static_cast<double>(arg->v_int64));
|
|
502
|
+
case GI_TYPE_TAG_UINT64:
|
|
503
|
+
WarnIfUnsafeUint64(arg->v_uint64);
|
|
504
|
+
return Napi::Number::New(env, static_cast<double>(arg->v_uint64));
|
|
505
|
+
case GI_TYPE_TAG_FLOAT: return Napi::Number::New(env, arg->v_float);
|
|
506
|
+
case GI_TYPE_TAG_DOUBLE: return Napi::Number::New(env, arg->v_double);
|
|
507
|
+
case GI_TYPE_TAG_UTF8:
|
|
508
|
+
case GI_TYPE_TAG_FILENAME: {
|
|
509
|
+
if (arg->v_string == nullptr) return env.Null();
|
|
510
|
+
Napi::Value str = Napi::String::New(env, arg->v_string);
|
|
511
|
+
if (transfer == GI_TRANSFER_EVERYTHING) g_free(arg->v_string);
|
|
512
|
+
return str;
|
|
513
|
+
}
|
|
514
|
+
case GI_TYPE_TAG_INTERFACE: {
|
|
515
|
+
GIBaseInfo* iface = gi_type_info_get_interface(type);
|
|
516
|
+
Napi::Value result;
|
|
517
|
+
// A GParamSpec return (e.g. GObject.ParamSpec factory / *_returnv) is a
|
|
518
|
+
// GObject FUNDAMENTAL, not a GObject — introspected as an object info but
|
|
519
|
+
// ref-counted via g_param_spec_ref, so it must NOT go through WrapGObject
|
|
520
|
+
// (g_object_ref would be wrong). Route it to the paramspec handle.
|
|
521
|
+
GType ifaceGType =
|
|
522
|
+
iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_STRUCT_INFO(iface))
|
|
523
|
+
? gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(iface))
|
|
524
|
+
: G_TYPE_INVALID;
|
|
525
|
+
if (ifaceGType != G_TYPE_INVALID && g_type_is_a(ifaceGType, G_TYPE_VALUE)) {
|
|
526
|
+
// A GValue return/OUT (GType G_TYPE_VALUE, or derived): GJS AUTO-UNBOXES it
|
|
527
|
+
// to the contained JS value (int/uint/int64/string/boolean/double/enum/
|
|
528
|
+
// object/boxed/GVariant/GParamSpec/GType/null), NOT a boxed handle. Verified
|
|
529
|
+
// against gjs 1.88: GIMarshallingTests.gvalue_return() → 42 (a number), not a
|
|
530
|
+
// GObject.Value box; gvalue_copy(<string GValue>) → "hi"; an object-holding
|
|
531
|
+
// GValue → the wrapped GObject (same identity); a NULL/unset string/object
|
|
532
|
+
// GValue → null. Unbox via GValueToJs — the SAME converter property/signal
|
|
533
|
+
// GValues use (BigInt/lossy 64-bit warning + all contained-type logic). The
|
|
534
|
+
// pointer slot holds the GValue* (a GValue return/OUT is always a pointer).
|
|
535
|
+
// Reference: refs/gjs/gi/arg.cpp gjs_value_from_gi_argument (INTERFACE branch
|
|
536
|
+
// tests `g_type_is_a(gtype, G_TYPE_VALUE)` BEFORE Struct) → value.cpp
|
|
537
|
+
// gjs_value_from_g_value. NB an EXPLICIT GObject.Value instance a user
|
|
538
|
+
// constructs stays a box — this only fires on a function's GValue return/OUT.
|
|
539
|
+
GValue* gvalue = static_cast<GValue*>(arg->v_pointer);
|
|
540
|
+
if (gvalue == nullptr) {
|
|
541
|
+
result = env.Null();
|
|
542
|
+
} else {
|
|
543
|
+
result = GValueToJs(env, gvalue);
|
|
544
|
+
// Transfer/free: GValueToJs COPIES/REFS every contained value into JS
|
|
545
|
+
// (strings are copied into V8, objects/boxeds ref'd/copied), so freeing the
|
|
546
|
+
// GValue container after the read is always safe. A transfer-EVERYTHING
|
|
547
|
+
// GValue* return (caller owns) is g_boxed_free'd — g_value_unset + free the
|
|
548
|
+
// GValue slice — exactly as GJS does (refs/gjs/gi/arg.cpp
|
|
549
|
+
// gjs_gi_argument_release G_TYPE_VALUE: pointer → g_boxed_free(gtype, …)).
|
|
550
|
+
// Transfer-NONE (the common case: gvalue_return/out, Gda.get_value_at) is a
|
|
551
|
+
// borrow → no free.
|
|
552
|
+
if (transfer == GI_TRANSFER_EVERYTHING) g_boxed_free(G_TYPE_VALUE, gvalue);
|
|
553
|
+
}
|
|
554
|
+
} else if (ifaceGType != G_TYPE_INVALID && g_type_is_a(ifaceGType, G_TYPE_PARAM)) {
|
|
555
|
+
result = MakeParamSpecHandle(env, static_cast<GParamSpec*>(arg->v_pointer), transfer);
|
|
556
|
+
} else if (iface != nullptr && GI_IS_OBJECT_INFO(iface) &&
|
|
557
|
+
gi_object_info_get_fundamental(reinterpret_cast<GIObjectInfo*>(iface))) {
|
|
558
|
+
// A non-GObject GObject-fundamental (GskRenderNode from Gtk.Snapshot.to_node,
|
|
559
|
+
// GdkEvent, …): introspected as object info but ref-counted via its OWN
|
|
560
|
+
// ref/unref funcs — G_IS_OBJECT is FALSE. WrapGObject would run the
|
|
561
|
+
// toggle-ref/qdata dance on a non-GObject → a G_IS_OBJECT critical cascade +
|
|
562
|
+
// a leaked ref. Wrap with the introspected ref/unref instead. (GParamSpec +
|
|
563
|
+
// GValue are handled by their dedicated branches above, so this is the rest.)
|
|
564
|
+
result = MakeFundamentalHandle(env, arg->v_pointer,
|
|
565
|
+
reinterpret_cast<GIObjectInfo*>(iface), transfer);
|
|
566
|
+
} else if (iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface))) {
|
|
567
|
+
result = WrapGObject(env, static_cast<GObject*>(arg->v_pointer), transfer);
|
|
568
|
+
} else if (iface != nullptr && (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface))) {
|
|
569
|
+
result = Napi::Number::New(env, arg->v_int);
|
|
570
|
+
} else if (iface != nullptr && (GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface))) {
|
|
571
|
+
// A foreign struct (cairo) returned/handed to a callback (e.g. a draw-func's
|
|
572
|
+
// cairo_t) wraps via its module's converter, not the boxed path.
|
|
573
|
+
const ForeignStructOps* fops = ForeignOpsForInfo(iface);
|
|
574
|
+
result = fops != nullptr ? fops->from(env, arg->v_pointer, transfer)
|
|
575
|
+
: WrapBoxed(env, arg->v_pointer, iface, transfer);
|
|
576
|
+
} else {
|
|
577
|
+
Napi::TypeError::New(
|
|
578
|
+
env,
|
|
579
|
+
"Unsupported interface return type (milestone: objects, interfaces, enums, flags, boxed)")
|
|
580
|
+
.ThrowAsJavaScriptException();
|
|
581
|
+
result = env.Undefined();
|
|
582
|
+
}
|
|
583
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
case GI_TYPE_TAG_GTYPE: {
|
|
587
|
+
// A GType return value (e.g. g_type_from_name): wrap the GType slot
|
|
588
|
+
// (gsize-wide) as a node-gi GType handle so it round-trips back into the
|
|
589
|
+
// engine. A 0 GType (not found) becomes null.
|
|
590
|
+
GType gt = static_cast<GType>(arg->v_size);
|
|
591
|
+
return gt != 0 ? MakeGTypeHandle(env, gt) : env.Null();
|
|
592
|
+
}
|
|
593
|
+
case GI_TYPE_TAG_ERROR: {
|
|
594
|
+
// A GError-typed value (a `GLib.Error` return like Gtk.GLArea.get_error(),
|
|
595
|
+
// or a GLib.Error.new_literal construct): GJS surfaces it as a GLib.Error
|
|
596
|
+
// boxed (G_TYPE_ERROR) with `.domain`/`.code`/`.message` field access +
|
|
597
|
+
// `.matches()`/`.copy()` methods. Wrap through the GLib.Error struct info
|
|
598
|
+
// so the boxed handle resolves fields and methods; a NULL GError (the
|
|
599
|
+
// no-error case) stays null. WrapBoxed handles transfer: a transfer-none
|
|
600
|
+
// borrow is g_boxed_copy'd into an owned copy, transfer-full is adopted.
|
|
601
|
+
GError* gerr = static_cast<GError*>(arg->v_pointer);
|
|
602
|
+
if (gerr == nullptr) return env.Null();
|
|
603
|
+
GIRepository* repo = DupDefaultRepository();
|
|
604
|
+
// GLib is loaded by any namespace require; require defensively anyway so a
|
|
605
|
+
// bare engine call still resolves the struct info (idempotent, cheap).
|
|
606
|
+
gi_repository_require(repo, "GLib", "2.0", static_cast<GIRepositoryLoadFlags>(0), nullptr);
|
|
607
|
+
GIBaseInfo* errInfo = gi_repository_find_by_name(repo, "GLib", "Error");
|
|
608
|
+
Napi::Value result = WrapBoxed(env, gerr, errInfo, transfer);
|
|
609
|
+
if (errInfo != nullptr) gi_base_info_unref(errInfo);
|
|
610
|
+
g_object_unref(repo);
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
default:
|
|
614
|
+
Napi::TypeError::New(env, "Unsupported return type tag " +
|
|
615
|
+
std::to_string(static_cast<int>(tag)) + " (milestone 1)")
|
|
616
|
+
.ThrowAsJavaScriptException();
|
|
617
|
+
return env.Undefined();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ====================================================================
|
|
622
|
+
// ---- array / GList / GSList / GHashTable / GStrv marshalling --------
|
|
623
|
+
// ====================================================================
|
|
624
|
+
//
|
|
625
|
+
// Compound container marshalling for IN, return, and OUT directions. Reference:
|
|
626
|
+
// refs/node-gtk src/{value,function}.cc (romgrk, MIT), retargeted to the
|
|
627
|
+
// girepository-2.0 API and the dense in_args/out_args layout this addon uses.
|
|
628
|
+
//
|
|
629
|
+
// SCOPE (the common cases real GJS code hits): C arrays + GStrv, GByteArray,
|
|
630
|
+
// GArray, GPtrArray (read), GList/GSList, GHashTable with string keys. Element
|
|
631
|
+
// types: utf8/filename (strings), the numeric fundamentals + boolean, and
|
|
632
|
+
// GObject/interface instances. EXOTIC combos (struct/union/enum/flags/nested-
|
|
633
|
+
// container elements, non-string hash keys, GArray/GPtrArray as IN, INOUT
|
|
634
|
+
// containers) are DEFERRED with a clear "<type> not yet supported" thrown BEFORE
|
|
635
|
+
// the invoke — mirroring the #652 IsSupportedOutType pattern.
|
|
636
|
+
//
|
|
637
|
+
// OWNERSHIP (the leak/UAF surface — scrutinise here):
|
|
638
|
+
// * Returns / OUT (caller-owns per gi_arg_info_get_ownership_transfer /
|
|
639
|
+
// gi_callable_info_get_caller_owns): TRANSFER_EVERYTHING frees both the
|
|
640
|
+
// container AND its elements after the read (strings g_free'd, object refs
|
|
641
|
+
// adopted by the JS wrapper); TRANSFER_CONTAINER frees only the container
|
|
642
|
+
// (elements stay callee-owned); TRANSFER_NOTHING frees nothing.
|
|
643
|
+
// * IN: TRANSFER_NOTHING means the callee borrows — we build the container,
|
|
644
|
+
// pass it, and free it (container + any g_strdup'd strings) AFTER the invoke.
|
|
645
|
+
// TRANSFER_EVERYTHING / TRANSFER_CONTAINER means the callee adopts what we
|
|
646
|
+
// built — we must NOT free it (that would UAF / double-free).
|
|
647
|
+
|
|
648
|
+
// Element-type support shared by every container kind. *why receives a short
|
|
649
|
+
// label on refusal (so the caller can throw a precise deferral message).
|
|
650
|
+
static bool IsSupportedElementType(GITypeInfo* elem, std::string* why) {
|
|
651
|
+
switch (gi_type_info_get_tag(elem)) {
|
|
652
|
+
case GI_TYPE_TAG_BOOLEAN:
|
|
653
|
+
case GI_TYPE_TAG_INT8:
|
|
654
|
+
case GI_TYPE_TAG_UINT8:
|
|
655
|
+
case GI_TYPE_TAG_INT16:
|
|
656
|
+
case GI_TYPE_TAG_UINT16:
|
|
657
|
+
case GI_TYPE_TAG_INT32:
|
|
658
|
+
case GI_TYPE_TAG_UINT32:
|
|
659
|
+
case GI_TYPE_TAG_INT64:
|
|
660
|
+
case GI_TYPE_TAG_UINT64:
|
|
661
|
+
case GI_TYPE_TAG_FLOAT:
|
|
662
|
+
case GI_TYPE_TAG_DOUBLE:
|
|
663
|
+
case GI_TYPE_TAG_UTF8:
|
|
664
|
+
case GI_TYPE_TAG_FILENAME:
|
|
665
|
+
return true;
|
|
666
|
+
case GI_TYPE_TAG_INTERFACE: {
|
|
667
|
+
GIBaseInfo* iface = gi_type_info_get_interface(elem);
|
|
668
|
+
bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface));
|
|
669
|
+
if (!ok && why != nullptr) *why = "struct/union/enum element";
|
|
670
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
671
|
+
return ok;
|
|
672
|
+
}
|
|
673
|
+
default:
|
|
674
|
+
if (why != nullptr) *why = "nested-container element";
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Whether a container type (ARRAY/GLIST/GSLIST/GHASH) is marshallable. Used by
|
|
680
|
+
// IsSupportedOutType (OUT/return) and the IN path to defer the unsupported cases
|
|
681
|
+
// up front. Array-type kind is intentionally NOT restricted here — the read side
|
|
682
|
+
// (GIArrayToJs) handles C/BYTE_ARRAY/GArray/GPtrArray; the IN side rejects
|
|
683
|
+
// GArray/GPtrArray separately (they're rare as IN in headless GLib).
|
|
684
|
+
bool IsSupportedContainerType(GITypeInfo* type, std::string* why) {
|
|
685
|
+
switch (gi_type_info_get_tag(type)) {
|
|
686
|
+
case GI_TYPE_TAG_ARRAY:
|
|
687
|
+
case GI_TYPE_TAG_GLIST:
|
|
688
|
+
case GI_TYPE_TAG_GSLIST: {
|
|
689
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
690
|
+
if (elem == nullptr) {
|
|
691
|
+
if (why != nullptr) *why = "untyped container";
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
bool ok = IsSupportedElementType(elem, why);
|
|
695
|
+
gi_base_info_unref(elem);
|
|
696
|
+
return ok;
|
|
697
|
+
}
|
|
698
|
+
case GI_TYPE_TAG_GHASH: {
|
|
699
|
+
GITypeInfo* kt = gi_type_info_get_param_type(type, 0);
|
|
700
|
+
GITypeInfo* vt = gi_type_info_get_param_type(type, 1);
|
|
701
|
+
GITypeTag ktag = kt != nullptr ? gi_type_info_get_tag(kt) : GI_TYPE_TAG_VOID;
|
|
702
|
+
// Supported hash key tags — a subset of GJS's is_supported_ghash_key_type
|
|
703
|
+
// (refs/gjs/gi/arg.cpp): strings + the pointer-fitting integers. 64-bit keys
|
|
704
|
+
// (which don't fit a pointer) + unichar keys are out of scope.
|
|
705
|
+
bool kok = ktag == GI_TYPE_TAG_UTF8 || ktag == GI_TYPE_TAG_FILENAME ||
|
|
706
|
+
ktag == GI_TYPE_TAG_INT8 || ktag == GI_TYPE_TAG_UINT8 ||
|
|
707
|
+
ktag == GI_TYPE_TAG_INT16 || ktag == GI_TYPE_TAG_UINT16 ||
|
|
708
|
+
ktag == GI_TYPE_TAG_INT32 || ktag == GI_TYPE_TAG_UINT32;
|
|
709
|
+
bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr);
|
|
710
|
+
if (!kok && why != nullptr) *why = "unsupported GHashTable key";
|
|
711
|
+
else if (!vok && why != nullptr) *why = "unsupported GHashTable value";
|
|
712
|
+
if (kt != nullptr) gi_base_info_unref(kt);
|
|
713
|
+
if (vt != nullptr) gi_base_info_unref(vt);
|
|
714
|
+
return kok && vok;
|
|
715
|
+
}
|
|
716
|
+
default:
|
|
717
|
+
if (why != nullptr) *why = "container";
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// In-memory size of one C-array element. 0 for an unsupported element type.
|
|
723
|
+
size_t CElementSize(GITypeInfo* elem) {
|
|
724
|
+
switch (gi_type_info_get_tag(elem)) {
|
|
725
|
+
case GI_TYPE_TAG_BOOLEAN: return sizeof(gboolean);
|
|
726
|
+
case GI_TYPE_TAG_INT8:
|
|
727
|
+
case GI_TYPE_TAG_UINT8: return 1;
|
|
728
|
+
case GI_TYPE_TAG_INT16:
|
|
729
|
+
case GI_TYPE_TAG_UINT16: return 2;
|
|
730
|
+
case GI_TYPE_TAG_INT32:
|
|
731
|
+
case GI_TYPE_TAG_UINT32: return 4;
|
|
732
|
+
case GI_TYPE_TAG_INT64:
|
|
733
|
+
case GI_TYPE_TAG_UINT64: return 8;
|
|
734
|
+
case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
|
|
735
|
+
case GI_TYPE_TAG_DOUBLE: return sizeof(gdouble);
|
|
736
|
+
case GI_TYPE_TAG_UTF8:
|
|
737
|
+
case GI_TYPE_TAG_FILENAME:
|
|
738
|
+
case GI_TYPE_TAG_INTERFACE: return sizeof(gpointer);
|
|
739
|
+
default: return 0;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Write the array's element count into a length arg's GIArgument slot (IN length
|
|
744
|
+
// autofill). The field is selected by the length arg's own tag; the slot is
|
|
745
|
+
// zero-initialised so writing the matching field is correct on little-endian
|
|
746
|
+
// (the only targets — Fedora x86_64/aarch64).
|
|
747
|
+
void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n) {
|
|
748
|
+
switch (gi_type_info_get_tag(lenType)) {
|
|
749
|
+
case GI_TYPE_TAG_INT8: slot->v_int8 = static_cast<gint8>(n); break;
|
|
750
|
+
case GI_TYPE_TAG_UINT8: slot->v_uint8 = static_cast<guint8>(n); break;
|
|
751
|
+
case GI_TYPE_TAG_INT16: slot->v_int16 = static_cast<gint16>(n); break;
|
|
752
|
+
case GI_TYPE_TAG_UINT16: slot->v_uint16 = static_cast<guint16>(n); break;
|
|
753
|
+
case GI_TYPE_TAG_INT32: slot->v_int32 = static_cast<gint32>(n); break;
|
|
754
|
+
case GI_TYPE_TAG_UINT32: slot->v_uint32 = static_cast<guint32>(n); break;
|
|
755
|
+
case GI_TYPE_TAG_INT64: slot->v_int64 = static_cast<gint64>(n); break;
|
|
756
|
+
case GI_TYPE_TAG_UINT64: slot->v_uint64 = static_cast<guint64>(n); break;
|
|
757
|
+
default: slot->v_int64 = static_cast<gint64>(n); break; // gsize/gtype: LE-safe
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Read a length value the callee wrote into a length arg's slot (OUT length).
|
|
762
|
+
static long ReadLengthValue(GITypeInfo* lenType, GIArgument* slot) {
|
|
763
|
+
switch (gi_type_info_get_tag(lenType)) {
|
|
764
|
+
case GI_TYPE_TAG_INT8: return slot->v_int8;
|
|
765
|
+
case GI_TYPE_TAG_UINT8: return slot->v_uint8;
|
|
766
|
+
case GI_TYPE_TAG_INT16: return slot->v_int16;
|
|
767
|
+
case GI_TYPE_TAG_UINT16: return slot->v_uint16;
|
|
768
|
+
case GI_TYPE_TAG_INT32: return slot->v_int32;
|
|
769
|
+
case GI_TYPE_TAG_UINT32: return static_cast<long>(slot->v_uint32);
|
|
770
|
+
case GI_TYPE_TAG_INT64: return static_cast<long>(slot->v_int64);
|
|
771
|
+
case GI_TYPE_TAG_UINT64: return static_cast<long>(slot->v_uint64);
|
|
772
|
+
default: return static_cast<long>(slot->v_int64); // gsize/gtype: LE-safe
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Read one C-array element from raw storage into a JS value, honouring the
|
|
777
|
+
// per-element transfer (EVERYTHING frees strings / adopts object refs).
|
|
778
|
+
static Napi::Value ReadCElement(Napi::Env env, GITypeInfo* elem, const void* src,
|
|
779
|
+
GITransfer elemTransfer) {
|
|
780
|
+
GIArgument a;
|
|
781
|
+
memset(&a, 0, sizeof(a));
|
|
782
|
+
switch (gi_type_info_get_tag(elem)) {
|
|
783
|
+
case GI_TYPE_TAG_BOOLEAN: a.v_boolean = *static_cast<const gboolean*>(src); break;
|
|
784
|
+
case GI_TYPE_TAG_INT8: a.v_int8 = *static_cast<const gint8*>(src); break;
|
|
785
|
+
case GI_TYPE_TAG_UINT8: a.v_uint8 = *static_cast<const guint8*>(src); break;
|
|
786
|
+
case GI_TYPE_TAG_INT16: a.v_int16 = *static_cast<const gint16*>(src); break;
|
|
787
|
+
case GI_TYPE_TAG_UINT16: a.v_uint16 = *static_cast<const guint16*>(src); break;
|
|
788
|
+
case GI_TYPE_TAG_INT32: a.v_int32 = *static_cast<const gint32*>(src); break;
|
|
789
|
+
case GI_TYPE_TAG_UINT32: a.v_uint32 = *static_cast<const guint32*>(src); break;
|
|
790
|
+
case GI_TYPE_TAG_INT64: a.v_int64 = *static_cast<const gint64*>(src); break;
|
|
791
|
+
case GI_TYPE_TAG_UINT64: a.v_uint64 = *static_cast<const guint64*>(src); break;
|
|
792
|
+
case GI_TYPE_TAG_FLOAT: a.v_float = *static_cast<const gfloat*>(src); break;
|
|
793
|
+
case GI_TYPE_TAG_DOUBLE: a.v_double = *static_cast<const gdouble*>(src); break;
|
|
794
|
+
case GI_TYPE_TAG_UTF8:
|
|
795
|
+
case GI_TYPE_TAG_FILENAME: a.v_string = *static_cast<char* const*>(src); break;
|
|
796
|
+
case GI_TYPE_TAG_INTERFACE: a.v_pointer = *static_cast<gpointer const*>(src); break;
|
|
797
|
+
default: return env.Undefined();
|
|
798
|
+
}
|
|
799
|
+
return GIArgumentToJs(env, elem, &a, elemTransfer);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Free a read-side array container after marshalling out its elements. For
|
|
803
|
+
// TRANSFER_EVERYTHING the elements were already released per-element during the
|
|
804
|
+
// read; this frees the container itself. NOTHING frees nothing (callee owns).
|
|
805
|
+
static void FreeReadArrayContainer(gpointer container, GIArrayType at, GITransfer transfer) {
|
|
806
|
+
if (container == nullptr || transfer == GI_TRANSFER_NOTHING) return;
|
|
807
|
+
switch (at) {
|
|
808
|
+
case GI_ARRAY_TYPE_C: g_free(container); break;
|
|
809
|
+
case GI_ARRAY_TYPE_BYTE_ARRAY:
|
|
810
|
+
// CONTAINER frees only the GByteArray wrapper (callee keeps the bytes);
|
|
811
|
+
// EVERYTHING frees the segment too. Mirrors the GArray/GPtrArray cases.
|
|
812
|
+
g_byte_array_free(static_cast<GByteArray*>(container), transfer == GI_TRANSFER_EVERYTHING);
|
|
813
|
+
break;
|
|
814
|
+
case GI_ARRAY_TYPE_ARRAY:
|
|
815
|
+
g_array_free(static_cast<GArray*>(container), transfer == GI_TRANSFER_EVERYTHING);
|
|
816
|
+
break;
|
|
817
|
+
case GI_ARRAY_TYPE_PTR_ARRAY:
|
|
818
|
+
g_ptr_array_free(static_cast<GPtrArray*>(container), transfer == GI_TRANSFER_EVERYTHING);
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Marshal a C array / GStrv / GByteArray / GArray / GPtrArray into a JS value.
|
|
824
|
+
// `length` is the resolved element count (or -1 = derive from zero-terminated /
|
|
825
|
+
// fixed-size). guint8/gint8 arrays surface as a Node Buffer; everything else as
|
|
826
|
+
// a JS Array. Frees the container per `transfer`.
|
|
827
|
+
static Napi::Value GIArrayToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
828
|
+
GITransfer transfer, long length) {
|
|
829
|
+
GIArrayType at = gi_type_info_get_array_type(type);
|
|
830
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
831
|
+
GITypeTag etag = elem != nullptr ? gi_type_info_get_tag(elem) : GI_TYPE_TAG_VOID;
|
|
832
|
+
GITransfer elemTransfer =
|
|
833
|
+
transfer == GI_TRANSFER_EVERYTHING ? GI_TRANSFER_EVERYTHING : GI_TRANSFER_NOTHING;
|
|
834
|
+
gpointer container = arg->v_pointer;
|
|
835
|
+
void* data = container;
|
|
836
|
+
size_t elemSize = CElementSize(elem);
|
|
837
|
+
bool isByte = etag == GI_TYPE_TAG_UINT8 || etag == GI_TYPE_TAG_INT8;
|
|
838
|
+
// A NULL container maps to `null` for every array kind EXCEPT a length-annotated
|
|
839
|
+
// C array (an explicit length was passed → an empty array, matching GJS's
|
|
840
|
+
// gjs_array_from_basic_c_array_internal "null pointer takes precedence over
|
|
841
|
+
// length" for the length path vs the fixed/zero-terminated/GArray/GPtrArray/
|
|
842
|
+
// GByteArray readers, which each return null on a NULL container).
|
|
843
|
+
bool hasExplicitLength = length >= 0;
|
|
844
|
+
|
|
845
|
+
// Resolve data + length for the boxed array kinds (their own struct carries it).
|
|
846
|
+
// A NULL boxed container → JS null (GJS gjs_value_from_basic_{garray,gptrarray,
|
|
847
|
+
// byte_array}_gi_argument each null-guard before reading).
|
|
848
|
+
if (at == GI_ARRAY_TYPE_BYTE_ARRAY) {
|
|
849
|
+
GByteArray* ba = static_cast<GByteArray*>(container);
|
|
850
|
+
if (ba == nullptr) {
|
|
851
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
852
|
+
return env.Null();
|
|
853
|
+
}
|
|
854
|
+
data = ba->data;
|
|
855
|
+
length = static_cast<long>(ba->len);
|
|
856
|
+
isByte = true;
|
|
857
|
+
elemSize = 1;
|
|
858
|
+
} else if (at == GI_ARRAY_TYPE_ARRAY) {
|
|
859
|
+
GArray* ga = static_cast<GArray*>(container);
|
|
860
|
+
if (ga == nullptr) {
|
|
861
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
862
|
+
return env.Null();
|
|
863
|
+
}
|
|
864
|
+
data = ga->data;
|
|
865
|
+
length = static_cast<long>(ga->len);
|
|
866
|
+
elemSize = g_array_get_element_size(ga);
|
|
867
|
+
} else if (at == GI_ARRAY_TYPE_PTR_ARRAY) {
|
|
868
|
+
GPtrArray* pa = static_cast<GPtrArray*>(container);
|
|
869
|
+
if (pa == nullptr) {
|
|
870
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
871
|
+
return env.Null();
|
|
872
|
+
}
|
|
873
|
+
data = pa->pdata;
|
|
874
|
+
length = static_cast<long>(pa->len);
|
|
875
|
+
elemSize = sizeof(gpointer);
|
|
876
|
+
} else if (data == nullptr) {
|
|
877
|
+
// NULL C-array pointer: a fixed/zero-terminated array (no length param) → null;
|
|
878
|
+
// a length-annotated array (explicit length given) → an empty array below.
|
|
879
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
880
|
+
return hasExplicitLength ? static_cast<Napi::Value>(Napi::Array::New(env, 0)) : env.Null();
|
|
881
|
+
} else if (length < 0) { // C array, length not given by a length arg
|
|
882
|
+
if (gi_type_info_is_zero_terminated(type)) {
|
|
883
|
+
length = 0;
|
|
884
|
+
if (data != nullptr) {
|
|
885
|
+
if (etag == GI_TYPE_TAG_UTF8 || etag == GI_TYPE_TAG_FILENAME ||
|
|
886
|
+
etag == GI_TYPE_TAG_INTERFACE) {
|
|
887
|
+
gpointer* p = static_cast<gpointer*>(data);
|
|
888
|
+
while (p[length] != nullptr) length++;
|
|
889
|
+
} else { // numeric: scan for an all-zero element
|
|
890
|
+
for (;; length++) {
|
|
891
|
+
const char* e = static_cast<const char*>(data) + length * elemSize;
|
|
892
|
+
bool zero = true;
|
|
893
|
+
for (size_t b = 0; b < elemSize; b++)
|
|
894
|
+
if (e[b] != 0) {
|
|
895
|
+
zero = false;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
if (zero) break;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
size_t fixed = 0;
|
|
904
|
+
length = gi_type_info_get_array_fixed_size(type, &fixed) ? static_cast<long>(fixed) : 0;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Byte arrays → a Node Buffer (a Uint8Array subclass).
|
|
909
|
+
if (isByte) {
|
|
910
|
+
Napi::Value buf = (data == nullptr || length <= 0)
|
|
911
|
+
? static_cast<Napi::Value>(Napi::Buffer<uint8_t>::New(env, 0))
|
|
912
|
+
: static_cast<Napi::Value>(Napi::Buffer<uint8_t>::Copy(
|
|
913
|
+
env, static_cast<const uint8_t*>(data), static_cast<size_t>(length)));
|
|
914
|
+
FreeReadArrayContainer(container, at, transfer);
|
|
915
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
916
|
+
return buf;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
Napi::Array out = Napi::Array::New(env, (data != nullptr && length > 0) ? length : 0);
|
|
920
|
+
for (long i = 0; data != nullptr && i < length && !env.IsExceptionPending(); i++) {
|
|
921
|
+
if (at == GI_ARRAY_TYPE_PTR_ARRAY) {
|
|
922
|
+
GIArgument a;
|
|
923
|
+
memset(&a, 0, sizeof(a));
|
|
924
|
+
a.v_pointer = static_cast<gpointer*>(data)[i];
|
|
925
|
+
out.Set(static_cast<uint32_t>(i), GIArgumentToJs(env, elem, &a, elemTransfer));
|
|
926
|
+
} else {
|
|
927
|
+
out.Set(static_cast<uint32_t>(i),
|
|
928
|
+
ReadCElement(env, elem, static_cast<const char*>(data) + i * elemSize, elemTransfer));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
FreeReadArrayContainer(container, at, transfer);
|
|
932
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Marshal a GList / GSList into a JS Array. Elements are unpacked from each
|
|
937
|
+
// node's data pointer via the introspection helper (strings/objects keep the
|
|
938
|
+
// pointer, fundamentals are GPOINTER_TO_INT-style unpacked). Frees the node
|
|
939
|
+
// chain (not the elements — those follow `transfer` per element) for
|
|
940
|
+
// EVERYTHING/CONTAINER.
|
|
941
|
+
static Napi::Value GListToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
942
|
+
GITransfer transfer, bool isSList) {
|
|
943
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
944
|
+
GITransfer elemTransfer =
|
|
945
|
+
transfer == GI_TRANSFER_EVERYTHING ? GI_TRANSFER_EVERYTHING : GI_TRANSFER_NOTHING;
|
|
946
|
+
Napi::Array out = Napi::Array::New(env);
|
|
947
|
+
uint32_t i = 0;
|
|
948
|
+
if (!isSList) {
|
|
949
|
+
for (GList* l = static_cast<GList*>(arg->v_pointer); l != nullptr && !env.IsExceptionPending();
|
|
950
|
+
l = l->next) {
|
|
951
|
+
GIArgument a;
|
|
952
|
+
gi_type_info_argument_from_hash_pointer(elem, l->data, &a);
|
|
953
|
+
out.Set(i++, GIArgumentToJs(env, elem, &a, elemTransfer));
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
for (GSList* l = static_cast<GSList*>(arg->v_pointer); l != nullptr && !env.IsExceptionPending();
|
|
957
|
+
l = l->next) {
|
|
958
|
+
GIArgument a;
|
|
959
|
+
gi_type_info_argument_from_hash_pointer(elem, l->data, &a);
|
|
960
|
+
out.Set(i++, GIArgumentToJs(env, elem, &a, elemTransfer));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (transfer == GI_TRANSFER_EVERYTHING || transfer == GI_TRANSFER_CONTAINER) {
|
|
964
|
+
if (!isSList) g_list_free(static_cast<GList*>(arg->v_pointer));
|
|
965
|
+
else g_slist_free(static_cast<GSList*>(arg->v_pointer));
|
|
966
|
+
}
|
|
967
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Marshal a GHashTable into a plain JS object (string keys). Keys + values are
|
|
972
|
+
// read with TRANSFER_NOTHING (copied / ref'd) because, for an owned table, the
|
|
973
|
+
// table's own GDestroyNotify funcs free the originals when we g_hash_table_unref
|
|
974
|
+
// below — freeing here too would double-free.
|
|
975
|
+
static Napi::Value GHashToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
976
|
+
GITransfer transfer) {
|
|
977
|
+
GHashTable* ht = static_cast<GHashTable*>(arg->v_pointer);
|
|
978
|
+
// A NULL GHashTable maps to `null` (GJS gjs_value_from_basic_ghash null-guards
|
|
979
|
+
// the table before reading) — NOT an empty object; e.g. an uninitialized OUT.
|
|
980
|
+
if (ht == nullptr) return env.Null();
|
|
981
|
+
Napi::Object out = Napi::Object::New(env);
|
|
982
|
+
if (ht != nullptr) {
|
|
983
|
+
GITypeInfo* kt = gi_type_info_get_param_type(type, 0);
|
|
984
|
+
GITypeInfo* vt = gi_type_info_get_param_type(type, 1);
|
|
985
|
+
GHashTableIter it;
|
|
986
|
+
gpointer k = nullptr;
|
|
987
|
+
gpointer v = nullptr;
|
|
988
|
+
g_hash_table_iter_init(&it, ht);
|
|
989
|
+
while (g_hash_table_iter_next(&it, &k, &v) && !env.IsExceptionPending()) {
|
|
990
|
+
GIArgument ka;
|
|
991
|
+
GIArgument va;
|
|
992
|
+
gi_type_info_argument_from_hash_pointer(kt, k, &ka);
|
|
993
|
+
gi_type_info_argument_from_hash_pointer(vt, v, &va);
|
|
994
|
+
Napi::Value key = GIArgumentToJs(env, kt, &ka, GI_TRANSFER_NOTHING);
|
|
995
|
+
Napi::Value value = GIArgumentToJs(env, vt, &va, GI_TRANSFER_NOTHING);
|
|
996
|
+
if (env.IsExceptionPending()) break;
|
|
997
|
+
out.Set(key, value);
|
|
998
|
+
}
|
|
999
|
+
if (kt != nullptr) gi_base_info_unref(kt);
|
|
1000
|
+
if (vt != nullptr) gi_base_info_unref(vt);
|
|
1001
|
+
}
|
|
1002
|
+
if (ht != nullptr && (transfer == GI_TRANSFER_EVERYTHING || transfer == GI_TRANSFER_CONTAINER)) {
|
|
1003
|
+
// CONTAINER transfers the table but NOT its keys/values (callee keeps them).
|
|
1004
|
+
// g_hash_table_unref would run the key/value destroy-notifiers → over-free,
|
|
1005
|
+
// so steal the entries first; the unref then frees only the table itself.
|
|
1006
|
+
// EVERYTHING keeps the destroy-notifiers (we own keys + values). Mirrors the
|
|
1007
|
+
// C-array / GList CONTAINER handling that frees only the container.
|
|
1008
|
+
if (transfer == GI_TRANSFER_CONTAINER) g_hash_table_steal_all(ht);
|
|
1009
|
+
g_hash_table_unref(ht);
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Dispatch a return / OUT GIArgument to the right reader. `slots` lets an array
|
|
1015
|
+
// resolve its length from the (already-populated) length arg slot. Scalars fall
|
|
1016
|
+
// through to GIArgumentToJs.
|
|
1017
|
+
Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo* ti,
|
|
1018
|
+
GIArgument* arg, GITransfer transfer,
|
|
1019
|
+
std::vector<GIArgument>* slots) {
|
|
1020
|
+
switch (gi_type_info_get_tag(ti)) {
|
|
1021
|
+
case GI_TYPE_TAG_ARRAY: {
|
|
1022
|
+
long len = -1;
|
|
1023
|
+
unsigned int L = 0;
|
|
1024
|
+
if (gi_type_info_get_array_length_index(ti, &L) && slots != nullptr && L < slots->size()) {
|
|
1025
|
+
GIArgInfo* la = gi_callable_info_get_arg(callable, L);
|
|
1026
|
+
GITypeInfo* lt = gi_arg_info_get_type_info(la);
|
|
1027
|
+
len = ReadLengthValue(lt, &(*slots)[L]);
|
|
1028
|
+
gi_base_info_unref(lt);
|
|
1029
|
+
gi_base_info_unref(la);
|
|
1030
|
+
}
|
|
1031
|
+
return GIArrayToJs(env, ti, arg, transfer, len);
|
|
1032
|
+
}
|
|
1033
|
+
case GI_TYPE_TAG_GLIST: return GListToJs(env, ti, arg, transfer, false);
|
|
1034
|
+
case GI_TYPE_TAG_GSLIST: return GListToJs(env, ti, arg, transfer, true);
|
|
1035
|
+
case GI_TYPE_TAG_GHASH: return GHashToJs(env, ti, arg, transfer);
|
|
1036
|
+
default: return GIArgumentToJs(env, ti, arg, transfer);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Fill a GIArgument for a single list/hash element from a JS value. Strings are
|
|
1041
|
+
// g_strdup'd (the container owns them); objects contribute their borrowed
|
|
1042
|
+
// handle pointer. Throws + returns false on an unsupported element.
|
|
1043
|
+
static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a) {
|
|
1044
|
+
memset(a, 0, sizeof(*a));
|
|
1045
|
+
switch (gi_type_info_get_tag(elem)) {
|
|
1046
|
+
// Scalar coercions via the terminate-safe helpers (common.h): a swallowed
|
|
1047
|
+
// napi failure mid worker.terminate() must degrade to a zero, not cascade
|
|
1048
|
+
// into Error::New(nullptr)'s fatal sites.
|
|
1049
|
+
case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
|
|
1050
|
+
case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
|
|
1051
|
+
case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
|
|
1052
|
+
case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
|
|
1053
|
+
case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
|
|
1054
|
+
case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
|
|
1055
|
+
case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
|
|
1056
|
+
case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
|
|
1057
|
+
case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(v); return true;
|
|
1058
|
+
case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
|
|
1059
|
+
case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
|
|
1060
|
+
case GI_TYPE_TAG_UTF8:
|
|
1061
|
+
case GI_TYPE_TAG_FILENAME:
|
|
1062
|
+
a->v_string = (v.IsEmpty() || v.IsNull() || v.IsUndefined())
|
|
1063
|
+
? nullptr
|
|
1064
|
+
: g_strdup(NodeGiToUtf8(v).c_str());
|
|
1065
|
+
return true;
|
|
1066
|
+
case GI_TYPE_TAG_INTERFACE: {
|
|
1067
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
1068
|
+
a->v_pointer = nullptr;
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
if (v.IsExternal() && v.As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag)) {
|
|
1072
|
+
a->v_pointer = v.As<Napi::External<GObject>>().Data();
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
Napi::TypeError::New(env, "expected a GObject handle as a container element")
|
|
1076
|
+
.ThrowAsJavaScriptException();
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
default:
|
|
1080
|
+
Napi::TypeError::New(env, "unsupported container element type")
|
|
1081
|
+
.ThrowAsJavaScriptException();
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Build a C array / GStrv / GByteArray / GArray / GPtrArray from a JS value.
|
|
1087
|
+
// *outCount = element count (for the IN length autofill + later free). Throws +
|
|
1088
|
+
// returns false on refusal (BEFORE the invoke).
|
|
1089
|
+
static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer* outPtr,
|
|
1090
|
+
long* outCount) {
|
|
1091
|
+
GIArrayType at = gi_type_info_get_array_type(type);
|
|
1092
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
1093
|
+
GITypeTag etag = gi_type_info_get_tag(elem);
|
|
1094
|
+
bool zt = gi_type_info_is_zero_terminated(type);
|
|
1095
|
+
size_t elemSize = CElementSize(elem);
|
|
1096
|
+
bool isByte = etag == GI_TYPE_TAG_UINT8 || etag == GI_TYPE_TAG_INT8;
|
|
1097
|
+
*outPtr = nullptr;
|
|
1098
|
+
*outCount = 0;
|
|
1099
|
+
|
|
1100
|
+
// null/undefined → a NULL array (count 0), as GJS marshals a null array arg
|
|
1101
|
+
// (refs/gjs/gi/arg.cpp gjs_array_to_explicit_array: null in → NULL out).
|
|
1102
|
+
// Exposing call: `Gst.init(null)` — a NULLABLE (inout) argv array every GJS
|
|
1103
|
+
// GStreamer consumer passes null for (`@gjsify/webaudio`'s ensureGstInit on
|
|
1104
|
+
// the jelly-jumper-on-node path).
|
|
1105
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
1106
|
+
gi_base_info_unref(elem);
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Raw bytes from a TypedArray / Buffer (Uint8Array round-trips, etc.).
|
|
1111
|
+
// `hasRawBytes` (NOT `rawBytes != nullptr`) marks a TypedArray/Buffer SOURCE: an
|
|
1112
|
+
// EMPTY Uint8Array/Buffer has a NULL backing-store pointer (V8 hands out nullptr
|
|
1113
|
+
// for a zero-length ArrayBuffer), yet it is still a valid 0-length byte source.
|
|
1114
|
+
// gjs marshals `new Uint8Array([])` to an empty C container (count 0); it must
|
|
1115
|
+
// NOT fall through to the `!v.IsArray()` throw below (a TypedArray isn't a JS
|
|
1116
|
+
// Array). Verified vs gjs 1.88: GLib.base64_encode(new Uint8Array([])) === ''.
|
|
1117
|
+
const uint8_t* rawBytes = nullptr;
|
|
1118
|
+
size_t rawLen = 0;
|
|
1119
|
+
bool hasRawBytes = false;
|
|
1120
|
+
if (v.IsBuffer()) {
|
|
1121
|
+
Napi::Buffer<uint8_t> b = v.As<Napi::Buffer<uint8_t>>();
|
|
1122
|
+
rawBytes = b.Data();
|
|
1123
|
+
rawLen = b.Length();
|
|
1124
|
+
hasRawBytes = true;
|
|
1125
|
+
} else if (v.IsTypedArray()) {
|
|
1126
|
+
Napi::TypedArray ta = v.As<Napi::TypedArray>();
|
|
1127
|
+
rawBytes = static_cast<const uint8_t*>(ta.ArrayBuffer().Data()) + ta.ByteOffset();
|
|
1128
|
+
rawLen = ta.ByteLength();
|
|
1129
|
+
hasRawBytes = true;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (at == GI_ARRAY_TYPE_BYTE_ARRAY) {
|
|
1133
|
+
GByteArray* ba = g_byte_array_new();
|
|
1134
|
+
if (hasRawBytes) {
|
|
1135
|
+
// append is a no-op for len 0; guard so a NULL data ptr (empty typed array)
|
|
1136
|
+
// is never handed to g_byte_array_append's memcpy.
|
|
1137
|
+
if (rawLen > 0) g_byte_array_append(ba, rawBytes, static_cast<guint>(rawLen));
|
|
1138
|
+
*outCount = static_cast<long>(rawLen);
|
|
1139
|
+
} else if (v.IsArray()) {
|
|
1140
|
+
Napi::Array arr = v.As<Napi::Array>();
|
|
1141
|
+
for (uint32_t i = 0; i < arr.Length(); i++) {
|
|
1142
|
+
guint8 byte = static_cast<guint8>(NodeGiToUint32(arr.Get(i)));
|
|
1143
|
+
g_byte_array_append(ba, &byte, 1);
|
|
1144
|
+
}
|
|
1145
|
+
*outCount = static_cast<long>(arr.Length());
|
|
1146
|
+
} else {
|
|
1147
|
+
g_byte_array_unref(ba);
|
|
1148
|
+
gi_base_info_unref(elem);
|
|
1149
|
+
Napi::TypeError::New(env, "expected an array or Uint8Array for the GByteArray argument")
|
|
1150
|
+
.ThrowAsJavaScriptException();
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
*outPtr = ba;
|
|
1154
|
+
gi_base_info_unref(elem);
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// C / GArray / GPtrArray share the element-marshalling loop below; only their
|
|
1159
|
+
// final container wrapping differs. The byte-specific fast paths (TypedArray /
|
|
1160
|
+
// string → bytes) are C-array-only ergonomics (a GArray/GPtrArray of bytes is
|
|
1161
|
+
// not something GJS accepts a TypedArray for), so gate them on the C kind.
|
|
1162
|
+
|
|
1163
|
+
// Byte C array from a TypedArray / Buffer. `hasRawBytes` (not `rawBytes !=
|
|
1164
|
+
// nullptr`) so an empty typed array (NULL data, len 0) still lands here: it
|
|
1165
|
+
// allocates a 0-length buffer (g_malloc0(0) → NULL, i.e. an empty C array,
|
|
1166
|
+
// count 0 — the same shape an empty JS array `[]` already produced) instead of
|
|
1167
|
+
// falling through to the `!v.IsArray()` throw. memcpy is guarded on rawLen>0.
|
|
1168
|
+
if (at == GI_ARRAY_TYPE_C && isByte && hasRawBytes) {
|
|
1169
|
+
void* buf = g_malloc0(elemSize * (rawLen + (zt ? 1 : 0)));
|
|
1170
|
+
if (rawLen > 0) memcpy(buf, rawBytes, rawLen);
|
|
1171
|
+
*outPtr = buf;
|
|
1172
|
+
*outCount = static_cast<long>(rawLen);
|
|
1173
|
+
gi_base_info_unref(elem);
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// A JS string as an int8/uint8 C array → its UTF-8 bytes, exactly as GJS does
|
|
1178
|
+
// (refs/gjs/gi/arg.cpp: "Allow strings as int8/uint8/int16/uint16 arrays" →
|
|
1179
|
+
// gjs_string_to_intarray → gjs_string_to_utf8_n for the int8/uint8 element tags).
|
|
1180
|
+
// The length is the UTF-8 BYTE count (not the JS string length, not NUL-inclusive);
|
|
1181
|
+
// a zero-terminated array still gets its trailing NUL slot from g_malloc0.
|
|
1182
|
+
// Verified against gjs 1.88: GIMarshallingTests.utf8_as_uint8array_in('const ♥ utf8')
|
|
1183
|
+
// is accepted (no throw). A real Uint8Array/Array still works via the paths around this.
|
|
1184
|
+
if (at == GI_ARRAY_TYPE_C && isByte && v.IsString()) {
|
|
1185
|
+
std::string s = v.As<Napi::String>().Utf8Value();
|
|
1186
|
+
size_t n = s.size();
|
|
1187
|
+
void* buf = g_malloc0(elemSize * (n + (zt ? 1 : 0)));
|
|
1188
|
+
memcpy(buf, s.data(), n);
|
|
1189
|
+
*outPtr = buf;
|
|
1190
|
+
*outCount = static_cast<long>(n);
|
|
1191
|
+
gi_base_info_unref(elem);
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (!v.IsArray()) {
|
|
1196
|
+
gi_base_info_unref(elem);
|
|
1197
|
+
Napi::TypeError::New(env, "expected an array for the array argument")
|
|
1198
|
+
.ThrowAsJavaScriptException();
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
Napi::Array arr = v.As<Napi::Array>();
|
|
1202
|
+
long count = static_cast<long>(arr.Length());
|
|
1203
|
+
// Build the flat element buffer first (the storage a C array uses directly).
|
|
1204
|
+
// GArray / GPtrArray then wrap this buffer — mirroring gjs, which marshals the
|
|
1205
|
+
// JS array into a C buffer and hands it to g_array_append_vals / the GPtrArray
|
|
1206
|
+
// pdata memcpy (refs/gjs/gi/arg.cpp gjs_value_to_basic_array_gi_argument).
|
|
1207
|
+
void* buf = g_malloc0(elemSize * (count + (zt ? 1 : 0)));
|
|
1208
|
+
bool ok = true;
|
|
1209
|
+
for (long i = 0; i < count && ok; i++) {
|
|
1210
|
+
GIArgument a;
|
|
1211
|
+
ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a);
|
|
1212
|
+
if (!ok) break;
|
|
1213
|
+
void* dst = static_cast<char*>(buf) + i * elemSize;
|
|
1214
|
+
// Copy the element's storage bytes (LE: the low elemSize bytes of the union
|
|
1215
|
+
// alias the active field — v_string/v_pointer for pointers, the scalar
|
|
1216
|
+
// otherwise).
|
|
1217
|
+
memcpy(dst, &a, elemSize);
|
|
1218
|
+
}
|
|
1219
|
+
if (!ok) {
|
|
1220
|
+
g_free(buf); // partial g_strdup'd strings leak on this error path (pre-checked, rare)
|
|
1221
|
+
gi_base_info_unref(elem);
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (at == GI_ARRAY_TYPE_ARRAY) {
|
|
1226
|
+
// GArray: a zero-terminated, sized GArray over `elemSize` elements
|
|
1227
|
+
// (garray_new_for_basic_type in gjs uses zero_terminated=true). append_vals
|
|
1228
|
+
// COPIES the buffer, so free our scratch buffer afterwards. String elements
|
|
1229
|
+
// (g_strdup'd pointers) now live in the GArray data; freed per-transfer in
|
|
1230
|
+
// FreeInContainer.
|
|
1231
|
+
GArray* ga = g_array_sized_new(TRUE, FALSE, static_cast<guint>(elemSize),
|
|
1232
|
+
static_cast<guint>(count));
|
|
1233
|
+
if (count > 0) g_array_append_vals(ga, buf, static_cast<guint>(count));
|
|
1234
|
+
g_free(buf);
|
|
1235
|
+
*outPtr = ga;
|
|
1236
|
+
} else if (at == GI_ARRAY_TYPE_PTR_ARRAY) {
|
|
1237
|
+
// GPtrArray: element pointers copied into pdata (gjs memcpy's the same way).
|
|
1238
|
+
GPtrArray* pa = g_ptr_array_sized_new(static_cast<guint>(count));
|
|
1239
|
+
g_ptr_array_set_size(pa, static_cast<int>(count));
|
|
1240
|
+
if (count > 0) memcpy(pa->pdata, buf, sizeof(gpointer) * static_cast<size_t>(count));
|
|
1241
|
+
g_free(buf);
|
|
1242
|
+
*outPtr = pa;
|
|
1243
|
+
} else {
|
|
1244
|
+
*outPtr = buf; // GI_ARRAY_TYPE_C
|
|
1245
|
+
}
|
|
1246
|
+
*outCount = count;
|
|
1247
|
+
gi_base_info_unref(elem);
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Build a GList / GSList from a JS array.
|
|
1252
|
+
static bool JsToGListLike(Napi::Env env, Napi::Value v, GITypeInfo* type, bool isSList,
|
|
1253
|
+
gpointer* outPtr) {
|
|
1254
|
+
*outPtr = nullptr;
|
|
1255
|
+
if (!v.IsArray()) {
|
|
1256
|
+
Napi::TypeError::New(env, "expected an array for the list argument").ThrowAsJavaScriptException();
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
Napi::Array arr = v.As<Napi::Array>();
|
|
1260
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
1261
|
+
GList* glist = nullptr;
|
|
1262
|
+
GSList* gslist = nullptr;
|
|
1263
|
+
bool ok = true;
|
|
1264
|
+
for (uint32_t i = 0; i < arr.Length() && ok; i++) {
|
|
1265
|
+
GIArgument a;
|
|
1266
|
+
ok = ElementToGIArgument(env, elem, arr.Get(i), &a);
|
|
1267
|
+
if (!ok) break;
|
|
1268
|
+
gpointer p = gi_type_info_hash_pointer_from_argument(elem, &a);
|
|
1269
|
+
if (!isSList) glist = g_list_prepend(glist, p);
|
|
1270
|
+
else gslist = g_slist_prepend(gslist, p);
|
|
1271
|
+
}
|
|
1272
|
+
if (!isSList) *outPtr = g_list_reverse(glist);
|
|
1273
|
+
else *outPtr = g_slist_reverse(gslist);
|
|
1274
|
+
gi_base_info_unref(elem);
|
|
1275
|
+
return ok;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Build a GHashTable from a JS object. Keys ∈ {string, pointer-fitting int};
|
|
1279
|
+
// values ∈ {string, object, int, and the heap-boxed int64/uint64/float/double}.
|
|
1280
|
+
// Mirrors GJS (refs/gjs/gi/arg.cpp gjs_object_to_g_hash + value_to_ghashtable_key):
|
|
1281
|
+
// * string keys → g_strdup + g_str_hash/equal; integer keys → GINT_TO_POINTER
|
|
1282
|
+
// via gi_type_info_hash_pointer_from_argument + g_direct_hash/equal.
|
|
1283
|
+
// * int/int32/string/object values fit a pointer (hash_pointer_from_argument);
|
|
1284
|
+
// int64/uint64/float/double do NOT, so they are heap-allocated (g_new) and a
|
|
1285
|
+
// pointer to the heap value is stored (the hash owns + g_free's it).
|
|
1286
|
+
// Key/value GDestroyNotify funcs are attached so a transfer-none teardown
|
|
1287
|
+
// (g_hash_table_unref in FreeInContainer) releases everything; a transfer-full
|
|
1288
|
+
// hash is adopted by the callee, which frees it (and thus runs the notifies).
|
|
1289
|
+
static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer* outPtr) {
|
|
1290
|
+
*outPtr = nullptr;
|
|
1291
|
+
if (!v.IsObject() || v.IsArray()) {
|
|
1292
|
+
Napi::TypeError::New(env, "expected an object for the GHashTable argument")
|
|
1293
|
+
.ThrowAsJavaScriptException();
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
GITypeInfo* kt = gi_type_info_get_param_type(type, 0);
|
|
1297
|
+
GITypeInfo* vt = gi_type_info_get_param_type(type, 1);
|
|
1298
|
+
GITypeTag ktag = gi_type_info_get_tag(kt);
|
|
1299
|
+
GITypeTag vtag = gi_type_info_get_tag(vt);
|
|
1300
|
+
bool keyIsString = ktag == GI_TYPE_TAG_UTF8 || ktag == GI_TYPE_TAG_FILENAME;
|
|
1301
|
+
bool valueIsString = vtag == GI_TYPE_TAG_UTF8 || vtag == GI_TYPE_TAG_FILENAME;
|
|
1302
|
+
bool valueHeap = vtag == GI_TYPE_TAG_INT64 || vtag == GI_TYPE_TAG_UINT64 ||
|
|
1303
|
+
vtag == GI_TYPE_TAG_FLOAT || vtag == GI_TYPE_TAG_DOUBLE;
|
|
1304
|
+
GDestroyNotify keyFree = keyIsString ? g_free : nullptr;
|
|
1305
|
+
GDestroyNotify valFree = (valueIsString || valueHeap) ? g_free : nullptr;
|
|
1306
|
+
GHashTable* ht = g_hash_table_new_full(keyIsString ? g_str_hash : g_direct_hash,
|
|
1307
|
+
keyIsString ? g_str_equal : g_direct_equal,
|
|
1308
|
+
keyFree, valFree);
|
|
1309
|
+
Napi::Object obj = v.As<Napi::Object>();
|
|
1310
|
+
Napi::Array keys = obj.GetPropertyNames();
|
|
1311
|
+
if (keys.IsEmpty()) {
|
|
1312
|
+
// napi_get_property_names failed with the throw swallowed (terminating env /
|
|
1313
|
+
// a throwing proxy trap left the exception pending). keys.Length() on the
|
|
1314
|
+
// empty Array would abort via Error::New(nullptr) — fail the marshal.
|
|
1315
|
+
g_hash_table_unref(ht);
|
|
1316
|
+
gi_base_info_unref(kt);
|
|
1317
|
+
gi_base_info_unref(vt);
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
bool ok = true;
|
|
1321
|
+
for (uint32_t i = 0; i < keys.Length() && ok; i++) {
|
|
1322
|
+
Napi::Value key = keys.Get(i);
|
|
1323
|
+
std::string ks = NodeGiToUtf8(key); // JS property keys are strings
|
|
1324
|
+
|
|
1325
|
+
// Key pointer: g_strdup for strings, else the integer key parsed from the
|
|
1326
|
+
// (string) property name and GINT_TO_POINTER-encoded per its tag.
|
|
1327
|
+
gpointer kp = nullptr;
|
|
1328
|
+
if (keyIsString) {
|
|
1329
|
+
kp = g_strdup(ks.c_str());
|
|
1330
|
+
} else {
|
|
1331
|
+
GIArgument ka;
|
|
1332
|
+
Napi::Value keyNum = Napi::Number::New(env, static_cast<double>(g_ascii_strtoll(ks.c_str(), nullptr, 10)));
|
|
1333
|
+
ok = ElementToGIArgument(env, kt, keyNum, &ka);
|
|
1334
|
+
if (!ok) break;
|
|
1335
|
+
kp = gi_type_info_hash_pointer_from_argument(kt, &ka);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Value pointer: heap-box the wide/float values (they don't fit a pointer),
|
|
1339
|
+
// else pointer-encode via hash_pointer_from_argument (strings g_strdup'd by
|
|
1340
|
+
// ElementToGIArgument, ints GINT_TO_POINTER, objects the borrowed handle).
|
|
1341
|
+
Napi::Value val = obj.Get(key);
|
|
1342
|
+
gpointer vp = nullptr;
|
|
1343
|
+
GIArgument va;
|
|
1344
|
+
ok = ElementToGIArgument(env, vt, val, &va);
|
|
1345
|
+
if (!ok) {
|
|
1346
|
+
g_free(kp);
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
if (vtag == GI_TYPE_TAG_INT64) {
|
|
1350
|
+
gint64* p = g_new(gint64, 1);
|
|
1351
|
+
*p = va.v_int64;
|
|
1352
|
+
vp = p;
|
|
1353
|
+
} else if (vtag == GI_TYPE_TAG_UINT64) {
|
|
1354
|
+
guint64* p = g_new(guint64, 1);
|
|
1355
|
+
*p = va.v_uint64;
|
|
1356
|
+
vp = p;
|
|
1357
|
+
} else if (vtag == GI_TYPE_TAG_FLOAT) {
|
|
1358
|
+
gfloat* p = g_new(gfloat, 1);
|
|
1359
|
+
*p = va.v_float;
|
|
1360
|
+
vp = p;
|
|
1361
|
+
} else if (vtag == GI_TYPE_TAG_DOUBLE) {
|
|
1362
|
+
gdouble* p = g_new(gdouble, 1);
|
|
1363
|
+
*p = va.v_double;
|
|
1364
|
+
vp = p;
|
|
1365
|
+
} else {
|
|
1366
|
+
vp = gi_type_info_hash_pointer_from_argument(vt, &va);
|
|
1367
|
+
}
|
|
1368
|
+
g_hash_table_insert(ht, kp, vp);
|
|
1369
|
+
}
|
|
1370
|
+
*outPtr = ht;
|
|
1371
|
+
gi_base_info_unref(kt);
|
|
1372
|
+
gi_base_info_unref(vt);
|
|
1373
|
+
return ok;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Free an IN container after the invoke. Only TRANSFER_NOTHING is freed here —
|
|
1377
|
+
// EVERYTHING/CONTAINER were adopted by the callee.
|
|
1378
|
+
void FreeInContainer(const InContainer& c) {
|
|
1379
|
+
if (c.ptr == nullptr || c.transfer != GI_TRANSFER_NOTHING) {
|
|
1380
|
+
gi_base_info_unref(c.type);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
GITypeTag tag = gi_type_info_get_tag(c.type);
|
|
1384
|
+
if (tag == GI_TYPE_TAG_ARRAY) {
|
|
1385
|
+
GIArrayType at = gi_type_info_get_array_type(c.type);
|
|
1386
|
+
GITypeInfo* elem = gi_type_info_get_param_type(c.type, 0);
|
|
1387
|
+
GITypeTag etag = elem != nullptr ? gi_type_info_get_tag(elem) : GI_TYPE_TAG_VOID;
|
|
1388
|
+
bool freeStrings = etag == GI_TYPE_TAG_UTF8 || etag == GI_TYPE_TAG_FILENAME;
|
|
1389
|
+
if (at == GI_ARRAY_TYPE_BYTE_ARRAY) {
|
|
1390
|
+
g_byte_array_unref(static_cast<GByteArray*>(c.ptr));
|
|
1391
|
+
} else if (at == GI_ARRAY_TYPE_ARRAY) {
|
|
1392
|
+
// GArray: free the g_strdup'd string elements first (g_array_free's
|
|
1393
|
+
// free_segment=TRUE releases the data block but not what its pointers
|
|
1394
|
+
// point at), then the GArray + its data segment.
|
|
1395
|
+
GArray* ga = static_cast<GArray*>(c.ptr);
|
|
1396
|
+
if (freeStrings)
|
|
1397
|
+
for (guint i = 0; i < ga->len; i++) g_free(g_array_index(ga, char*, i));
|
|
1398
|
+
g_array_free(ga, TRUE);
|
|
1399
|
+
} else if (at == GI_ARRAY_TYPE_PTR_ARRAY) {
|
|
1400
|
+
// GPtrArray: same — free owned string elements, then the array. Object
|
|
1401
|
+
// elements are borrowed handle pointers (never freed here).
|
|
1402
|
+
GPtrArray* pa = static_cast<GPtrArray*>(c.ptr);
|
|
1403
|
+
if (freeStrings)
|
|
1404
|
+
for (guint i = 0; i < pa->len; i++) g_free(g_ptr_array_index(pa, i));
|
|
1405
|
+
g_ptr_array_free(pa, TRUE);
|
|
1406
|
+
} else { // C array
|
|
1407
|
+
if (freeStrings) {
|
|
1408
|
+
char** s = static_cast<char**>(c.ptr);
|
|
1409
|
+
if (gi_type_info_is_zero_terminated(c.type)) {
|
|
1410
|
+
g_strfreev(s);
|
|
1411
|
+
} else {
|
|
1412
|
+
for (long i = 0; i < c.count; i++) g_free(s[i]);
|
|
1413
|
+
g_free(s);
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
g_free(c.ptr);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
1420
|
+
} else if (tag == GI_TYPE_TAG_GLIST || tag == GI_TYPE_TAG_GSLIST) {
|
|
1421
|
+
GITypeInfo* elem = gi_type_info_get_param_type(c.type, 0);
|
|
1422
|
+
GITypeTag etag = elem != nullptr ? gi_type_info_get_tag(elem) : GI_TYPE_TAG_VOID;
|
|
1423
|
+
bool freeStrings = etag == GI_TYPE_TAG_UTF8 || etag == GI_TYPE_TAG_FILENAME;
|
|
1424
|
+
if (tag == GI_TYPE_TAG_GLIST) {
|
|
1425
|
+
if (freeStrings)
|
|
1426
|
+
for (GList* l = static_cast<GList*>(c.ptr); l != nullptr; l = l->next) g_free(l->data);
|
|
1427
|
+
g_list_free(static_cast<GList*>(c.ptr));
|
|
1428
|
+
} else {
|
|
1429
|
+
if (freeStrings)
|
|
1430
|
+
for (GSList* l = static_cast<GSList*>(c.ptr); l != nullptr; l = l->next) g_free(l->data);
|
|
1431
|
+
g_slist_free(static_cast<GSList*>(c.ptr));
|
|
1432
|
+
}
|
|
1433
|
+
if (elem != nullptr) gi_base_info_unref(elem);
|
|
1434
|
+
} else if (tag == GI_TYPE_TAG_GHASH) {
|
|
1435
|
+
g_hash_table_unref(static_cast<GHashTable*>(c.ptr)); // destroy funcs free keys/values
|
|
1436
|
+
}
|
|
1437
|
+
gi_base_info_unref(c.type);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Build any supported IN container from a JS value, recording cleanup. The
|
|
1441
|
+
// container type must already have passed IsSupportedContainerType. Throws +
|
|
1442
|
+
// returns false on refusal (BEFORE the invoke).
|
|
1443
|
+
bool JsToInContainer(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
|
|
1444
|
+
gpointer* outPtr, long* outCount,
|
|
1445
|
+
std::vector<InContainer>* containers) {
|
|
1446
|
+
GITypeTag tag = gi_type_info_get_tag(type);
|
|
1447
|
+
*outCount = 0;
|
|
1448
|
+
bool ok = false;
|
|
1449
|
+
if (tag == GI_TYPE_TAG_ARRAY) {
|
|
1450
|
+
ok = JsToCArray(env, v, type, outPtr, outCount);
|
|
1451
|
+
} else if (tag == GI_TYPE_TAG_GLIST || tag == GI_TYPE_TAG_GSLIST) {
|
|
1452
|
+
ok = JsToGListLike(env, v, type, tag == GI_TYPE_TAG_GSLIST, outPtr);
|
|
1453
|
+
} else if (tag == GI_TYPE_TAG_GHASH) {
|
|
1454
|
+
ok = JsToGHashIn(env, v, type, outPtr);
|
|
1455
|
+
}
|
|
1456
|
+
if (ok && *outPtr != nullptr) {
|
|
1457
|
+
containers->push_back(InContainer{
|
|
1458
|
+
reinterpret_cast<GITypeInfo*>(gi_base_info_ref(type)), *outPtr, transfer, *outCount});
|
|
1459
|
+
} else if (!ok && *outPtr != nullptr) {
|
|
1460
|
+
// An element conversion threw mid-build (JsToGListLike / JsToGHashIn still
|
|
1461
|
+
// published the partial container). It was never recorded for cleanup, so
|
|
1462
|
+
// the nodes (and any g_strdup'd keys/string values) would leak. Free it now
|
|
1463
|
+
// with NOTHING semantics — we still own all of it; the callee never saw it.
|
|
1464
|
+
InContainer partial{reinterpret_cast<GITypeInfo*>(gi_base_info_ref(type)), *outPtr,
|
|
1465
|
+
GI_TRANSFER_NOTHING, *outCount};
|
|
1466
|
+
FreeInContainer(partial); // unrefs the type ref taken above
|
|
1467
|
+
*outPtr = nullptr;
|
|
1468
|
+
}
|
|
1469
|
+
return ok;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ---- struct / boxed / union FIELD access (phase 2.6) -----------------------
|
|
1473
|
+
//
|
|
1474
|
+
// Read/write a named field of a boxed/struct/union handle, mirroring GJS's
|
|
1475
|
+
// gi/boxed.cpp field_getter_impl / field_setter_impl. Simple-typed fields
|
|
1476
|
+
// (ints/floats/bool/enum) go through gi_field_info_get/set_field; pointer-backed
|
|
1477
|
+
// fields (strings, arrays, pointer-to-boxed) are read via the raw offset because
|
|
1478
|
+
// gi_field_info_get_field refuses non-simple fields on some libgirepository
|
|
1479
|
+
// versions; a nested BY-VALUE struct/union field returns a borrowing sub-handle.
|
|
1480
|
+
// Resolution rule (VERIFIED against gjs 1.88, find_unique_js_field_name): a name
|
|
1481
|
+
// that is BOTH a method and a field resolves to the METHOD (gjs renames the field
|
|
1482
|
+
// to `_name`); so boxedMemberKind checks methods first.
|
|
1483
|
+
|
|
1484
|
+
// Resolve the struct/union GIBaseInfo backing a boxed handle. Prefers the stored
|
|
1485
|
+
// static info (required for an unregistered struct whose GType is G_TYPE_NONE),
|
|
1486
|
+
// else looks it up by the registered GType. Returns a NEW ref (unref by caller) or
|
|
1487
|
+
// nullptr when neither source resolves a struct/union.
|
|
1488
|
+
static GIBaseInfo* DupBoxedTypeInfo(BoxedHandle* bh) {
|
|
1489
|
+
if (bh->info != nullptr && (GI_IS_STRUCT_INFO(bh->info) || GI_IS_UNION_INFO(bh->info))) {
|
|
1490
|
+
return gi_base_info_ref(bh->info);
|
|
1491
|
+
}
|
|
1492
|
+
if (bh->gtype != G_TYPE_INVALID && bh->gtype != G_TYPE_NONE) {
|
|
1493
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1494
|
+
GIBaseInfo* bi = gi_repository_find_by_gtype(repo, bh->gtype);
|
|
1495
|
+
g_object_unref(repo);
|
|
1496
|
+
if (bi != nullptr && (GI_IS_STRUCT_INFO(bi) || GI_IS_UNION_INFO(bi))) return bi;
|
|
1497
|
+
if (bi != nullptr) gi_base_info_unref(bi);
|
|
1498
|
+
}
|
|
1499
|
+
return nullptr;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Find a field by name on a struct or union info. GIUnionInfo has no find_field,
|
|
1503
|
+
// so iterate. Returns a NEW ref (unref by caller) or nullptr.
|
|
1504
|
+
static GIFieldInfo* FindBoxedField(GIBaseInfo* info, const char* name) {
|
|
1505
|
+
if (GI_IS_STRUCT_INFO(info)) {
|
|
1506
|
+
return gi_struct_info_find_field(reinterpret_cast<GIStructInfo*>(info), name);
|
|
1507
|
+
}
|
|
1508
|
+
if (GI_IS_UNION_INFO(info)) {
|
|
1509
|
+
GIUnionInfo* u = reinterpret_cast<GIUnionInfo*>(info);
|
|
1510
|
+
unsigned n = gi_union_info_get_n_fields(u);
|
|
1511
|
+
for (unsigned i = 0; i < n; i++) {
|
|
1512
|
+
GIFieldInfo* f = gi_union_info_get_field(u, i);
|
|
1513
|
+
if (f != nullptr &&
|
|
1514
|
+
g_strcmp0(gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(f)), name) == 0) {
|
|
1515
|
+
return f;
|
|
1516
|
+
}
|
|
1517
|
+
if (f != nullptr) gi_base_info_unref(f);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return nullptr;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Whether `name` is a method on the struct/union (methods win over fields).
|
|
1524
|
+
static bool BoxedHasMethod(GIBaseInfo* info, const char* name) {
|
|
1525
|
+
GIFunctionInfo* m = nullptr;
|
|
1526
|
+
if (GI_IS_STRUCT_INFO(info)) {
|
|
1527
|
+
m = gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(info), name);
|
|
1528
|
+
} else if (GI_IS_UNION_INFO(info)) {
|
|
1529
|
+
m = gi_union_info_find_method(reinterpret_cast<GIUnionInfo*>(info), name);
|
|
1530
|
+
}
|
|
1531
|
+
if (m != nullptr) {
|
|
1532
|
+
gi_base_info_unref(m);
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Read the field's raw GIArgument. Uses gi_field_info_get_field for simple types;
|
|
1539
|
+
// falls back to a raw pointer read at the field offset for pointer-typed fields
|
|
1540
|
+
// (strings/arrays/pointer-to-interface) that the info API declines. Returns false
|
|
1541
|
+
// only for a genuinely unsupported field (a non-pointer composite the caller must
|
|
1542
|
+
// have already special-cased). BORROWS: the container owns the pointed-to memory.
|
|
1543
|
+
static bool ReadFieldArg(GIFieldInfo* field, gpointer base, GITypeInfo* ftype,
|
|
1544
|
+
GIArgument* arg) {
|
|
1545
|
+
memset(arg, 0, sizeof(*arg));
|
|
1546
|
+
if (gi_field_info_get_field(field, base, arg)) return true;
|
|
1547
|
+
GITypeTag tag = gi_type_info_get_tag(ftype);
|
|
1548
|
+
bool pointerish = gi_type_info_is_pointer(ftype) || tag == GI_TYPE_TAG_UTF8 ||
|
|
1549
|
+
tag == GI_TYPE_TAG_FILENAME || tag == GI_TYPE_TAG_ARRAY;
|
|
1550
|
+
if (pointerish) {
|
|
1551
|
+
size_t off = gi_field_info_get_offset(field);
|
|
1552
|
+
arg->v_pointer = *reinterpret_cast<gpointer*>(static_cast<char*>(base) + off);
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Marshal a field GIArgument to JS. ARRAY tags dispatch to GIArrayToJs (length
|
|
1559
|
+
// derived from zero-terminated / fixed-size — the common GStrv field case);
|
|
1560
|
+
// everything else through GIArgumentToJs. TRANSFER NOTHING throughout — a field
|
|
1561
|
+
// read is a borrow (the container keeps ownership), so the JS value is a copy and
|
|
1562
|
+
// no container/element memory is freed.
|
|
1563
|
+
static Napi::Value FieldArgToJs(Napi::Env env, GITypeInfo* ftype, GIArgument* arg) {
|
|
1564
|
+
if (gi_type_info_get_tag(ftype) == GI_TYPE_TAG_ARRAY) {
|
|
1565
|
+
return GIArrayToJs(env, ftype, arg, GI_TRANSFER_NOTHING, /*length=*/-1);
|
|
1566
|
+
}
|
|
1567
|
+
return GIArgumentToJs(env, ftype, arg, GI_TRANSFER_NOTHING);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Validate arg 0 as a boxed handle and return its record (throwing on mismatch).
|
|
1571
|
+
static BoxedHandle* UnwrapBoxedArg(const Napi::CallbackInfo& info) {
|
|
1572
|
+
Napi::Env env = info.Env();
|
|
1573
|
+
if (!info[0].IsExternal() ||
|
|
1574
|
+
!info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag)) {
|
|
1575
|
+
Napi::TypeError::New(env, "expected a node-gi boxed handle").ThrowAsJavaScriptException();
|
|
1576
|
+
return nullptr;
|
|
1577
|
+
}
|
|
1578
|
+
BoxedHandle* bh = info[0].As<Napi::External<BoxedHandle>>().Data();
|
|
1579
|
+
if (bh == nullptr || bh->ptr == nullptr) {
|
|
1580
|
+
Napi::TypeError::New(env, "invalid boxed handle").ThrowAsJavaScriptException();
|
|
1581
|
+
return nullptr;
|
|
1582
|
+
}
|
|
1583
|
+
return bh;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// boxedMemberKind(handle, name) → 0 (neither) | 1 (method) | 2 (field) | 3 (type
|
|
1587
|
+
// info unavailable — membership undecidable). Methods take priority (gjs
|
|
1588
|
+
// find_unique_js_field_name renames a colliding field to `_name`), so L1 wrapBoxed
|
|
1589
|
+
// checks this to route method-dispatch vs field access. The 3 case only arises for
|
|
1590
|
+
// a boxed handle whose GType is unregistered AND carries no static info (neither
|
|
1591
|
+
// gtype nor stored info resolves a struct/union): L1 keeps its method-dispatch
|
|
1592
|
+
// fallback there. When info IS resolvable, 0 is AUTHORITATIVE ("not a member"), so
|
|
1593
|
+
// L1 returns `undefined` for it — matching gjs, where `typeof boxed.noSuchName` is
|
|
1594
|
+
// `'undefined'` (a fabricated dispatcher would make it `'function'`, breaking JS
|
|
1595
|
+
// duck-typing like `typeof x.toArray === 'function'`).
|
|
1596
|
+
Napi::Value BoxedMemberKind(const Napi::CallbackInfo& info) {
|
|
1597
|
+
Napi::Env env = info.Env();
|
|
1598
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
1599
|
+
Napi::TypeError::New(env, "boxedMemberKind(handle, name: string)")
|
|
1600
|
+
.ThrowAsJavaScriptException();
|
|
1601
|
+
return env.Null();
|
|
1602
|
+
}
|
|
1603
|
+
BoxedHandle* bh = UnwrapBoxedArg(info);
|
|
1604
|
+
if (bh == nullptr) return env.Null();
|
|
1605
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
1606
|
+
GIBaseInfo* ti = DupBoxedTypeInfo(bh);
|
|
1607
|
+
int kind = 3; // no resolvable type info → membership undecidable (L1 keeps its fallback)
|
|
1608
|
+
if (ti != nullptr) {
|
|
1609
|
+
kind = 0; // info resolvable → 0 is authoritative ("not a member")
|
|
1610
|
+
if (BoxedHasMethod(ti, name.c_str())) {
|
|
1611
|
+
kind = 1;
|
|
1612
|
+
} else {
|
|
1613
|
+
GIFieldInfo* f = FindBoxedField(ti, name.c_str());
|
|
1614
|
+
if (f != nullptr) {
|
|
1615
|
+
kind = 2;
|
|
1616
|
+
gi_base_info_unref(f);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
gi_base_info_unref(ti);
|
|
1620
|
+
}
|
|
1621
|
+
return Napi::Number::New(env, kind);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// getBoxedField(handle, name) → the field value (throws if not a field, or the
|
|
1625
|
+
// field is unreadable / unsupported — matching gjs's error-message shape).
|
|
1626
|
+
Napi::Value GetBoxedField(const Napi::CallbackInfo& info) {
|
|
1627
|
+
Napi::Env env = info.Env();
|
|
1628
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
1629
|
+
Napi::TypeError::New(env, "getBoxedField(handle, name: string)")
|
|
1630
|
+
.ThrowAsJavaScriptException();
|
|
1631
|
+
return env.Null();
|
|
1632
|
+
}
|
|
1633
|
+
BoxedHandle* bh = UnwrapBoxedArg(info);
|
|
1634
|
+
if (bh == nullptr) return env.Null();
|
|
1635
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
1636
|
+
GIBaseInfo* ti = DupBoxedTypeInfo(bh);
|
|
1637
|
+
if (ti == nullptr) {
|
|
1638
|
+
Napi::Error::New(env, "boxed handle has no introspection info for field access")
|
|
1639
|
+
.ThrowAsJavaScriptException();
|
|
1640
|
+
return env.Null();
|
|
1641
|
+
}
|
|
1642
|
+
std::string typeName = gi_base_info_get_name(ti) != nullptr ? gi_base_info_get_name(ti) : "?";
|
|
1643
|
+
GIFieldInfo* field = FindBoxedField(ti, name.c_str());
|
|
1644
|
+
if (field == nullptr) {
|
|
1645
|
+
gi_base_info_unref(ti);
|
|
1646
|
+
Napi::Error::New(env, std::string("no field '") + name + "' on " + typeName)
|
|
1647
|
+
.ThrowAsJavaScriptException();
|
|
1648
|
+
return env.Null();
|
|
1649
|
+
}
|
|
1650
|
+
Napi::Value result = env.Undefined();
|
|
1651
|
+
if (!(gi_field_info_get_flags(field) & GI_FIELD_IS_READABLE)) {
|
|
1652
|
+
Napi::Error::New(env, std::string("Reading field ") + typeName + "." + name + " is not supported")
|
|
1653
|
+
.ThrowAsJavaScriptException();
|
|
1654
|
+
} else {
|
|
1655
|
+
GITypeInfo* ftype = gi_field_info_get_type_info(field);
|
|
1656
|
+
// A nested BY-VALUE struct/union field: return a borrowing sub-handle into the
|
|
1657
|
+
// parent's memory at the field offset (mirrors gjs get_nested_interface_object).
|
|
1658
|
+
if (!gi_type_info_is_pointer(ftype) && gi_type_info_get_tag(ftype) == GI_TYPE_TAG_INTERFACE) {
|
|
1659
|
+
GIBaseInfo* iface = gi_type_info_get_interface(ftype);
|
|
1660
|
+
if (iface != nullptr && (GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface))) {
|
|
1661
|
+
size_t off = gi_field_info_get_offset(field);
|
|
1662
|
+
GType nt = gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(iface));
|
|
1663
|
+
GType nested = (nt != G_TYPE_INVALID && nt != G_TYPE_NONE) ? nt : G_TYPE_INVALID;
|
|
1664
|
+
result = MakeBoxedHandle(env, static_cast<char*>(bh->ptr) + off, nested,
|
|
1665
|
+
/*owns=*/false, iface);
|
|
1666
|
+
gi_base_info_unref(iface);
|
|
1667
|
+
gi_base_info_unref(ftype);
|
|
1668
|
+
gi_base_info_unref(field);
|
|
1669
|
+
gi_base_info_unref(ti);
|
|
1670
|
+
return result;
|
|
1671
|
+
}
|
|
1672
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
1673
|
+
}
|
|
1674
|
+
GIArgument arg;
|
|
1675
|
+
if (ReadFieldArg(field, bh->ptr, ftype, &arg)) {
|
|
1676
|
+
result = FieldArgToJs(env, ftype, &arg);
|
|
1677
|
+
} else {
|
|
1678
|
+
Napi::Error::New(env, std::string("Reading field ") + typeName + "." + name + " is not supported")
|
|
1679
|
+
.ThrowAsJavaScriptException();
|
|
1680
|
+
}
|
|
1681
|
+
gi_base_info_unref(ftype);
|
|
1682
|
+
}
|
|
1683
|
+
gi_base_info_unref(field);
|
|
1684
|
+
gi_base_info_unref(ti);
|
|
1685
|
+
return result;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// setBoxedField(handle, name, value) — writes a simple-typed field (throws if not
|
|
1689
|
+
// a field, the field is unwritable, or the type is unsupported — gjs message shape).
|
|
1690
|
+
Napi::Value SetBoxedField(const Napi::CallbackInfo& info) {
|
|
1691
|
+
Napi::Env env = info.Env();
|
|
1692
|
+
if (info.Length() < 3 || !info[1].IsString()) {
|
|
1693
|
+
Napi::TypeError::New(env, "setBoxedField(handle, name: string, value)")
|
|
1694
|
+
.ThrowAsJavaScriptException();
|
|
1695
|
+
return env.Null();
|
|
1696
|
+
}
|
|
1697
|
+
BoxedHandle* bh = UnwrapBoxedArg(info);
|
|
1698
|
+
if (bh == nullptr) return env.Null();
|
|
1699
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
1700
|
+
Napi::Value value = info[2];
|
|
1701
|
+
GIBaseInfo* ti = DupBoxedTypeInfo(bh);
|
|
1702
|
+
if (ti == nullptr) {
|
|
1703
|
+
Napi::Error::New(env, "boxed handle has no introspection info for field access")
|
|
1704
|
+
.ThrowAsJavaScriptException();
|
|
1705
|
+
return env.Null();
|
|
1706
|
+
}
|
|
1707
|
+
std::string typeName = gi_base_info_get_name(ti) != nullptr ? gi_base_info_get_name(ti) : "?";
|
|
1708
|
+
GIFieldInfo* field = FindBoxedField(ti, name.c_str());
|
|
1709
|
+
if (field == nullptr) {
|
|
1710
|
+
gi_base_info_unref(ti);
|
|
1711
|
+
Napi::Error::New(env, std::string("no field '") + name + "' on " + typeName)
|
|
1712
|
+
.ThrowAsJavaScriptException();
|
|
1713
|
+
return env.Null();
|
|
1714
|
+
}
|
|
1715
|
+
if (!(gi_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE)) {
|
|
1716
|
+
gi_base_info_unref(field);
|
|
1717
|
+
gi_base_info_unref(ti);
|
|
1718
|
+
Napi::Error::New(env, std::string("Writing field ") + typeName + "." + name + " is not supported")
|
|
1719
|
+
.ThrowAsJavaScriptException();
|
|
1720
|
+
return env.Null();
|
|
1721
|
+
}
|
|
1722
|
+
GITypeInfo* ftype = gi_field_info_get_type_info(field);
|
|
1723
|
+
GIArgument arg;
|
|
1724
|
+
memset(&arg, 0, sizeof(arg));
|
|
1725
|
+
std::string heldString;
|
|
1726
|
+
if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr)) {
|
|
1727
|
+
if (!gi_field_info_set_field(field, bh->ptr, &arg)) {
|
|
1728
|
+
Napi::Error::New(env, std::string("Writing field ") + typeName + "." + name + " is not supported")
|
|
1729
|
+
.ThrowAsJavaScriptException();
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
gi_base_info_unref(ftype);
|
|
1733
|
+
gi_base_info_unref(field);
|
|
1734
|
+
gi_base_info_unref(ti);
|
|
1735
|
+
return env.Undefined();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
} // namespace nodegi
|