@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/private.cc
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// GjsPrivate-mirroring helpers: the structured-log writer func + the
|
|
3
|
+
// bind_property_full / BindingGroup.bind_full transform trampolines.
|
|
4
|
+
//
|
|
5
|
+
// Reference: refs/gjs/libgjs-private/gjs-util.c (gjs_log_set_writer_func /
|
|
6
|
+
// gjs_log_writer_func_wrapper / gjs_log_set_writer_default /
|
|
7
|
+
// gjs_g_object_bind_property_full / gjs_g_binding_group_bind_full). GJS routes
|
|
8
|
+
// these APIs through a private C library instead of introspection because:
|
|
9
|
+
// • a GLogWriterFunc can fire on ANY thread, but JS only runs on the thread
|
|
10
|
+
// that registered the writer — off-thread logs must fall back to the default
|
|
11
|
+
// writer in C, before any JS is entered;
|
|
12
|
+
// • the GLogField array (key + length-or-NUL-terminated value) is not
|
|
13
|
+
// generically introspectable — the C wrapper converts it field-by-field;
|
|
14
|
+
// • the binding transform's `to_value` is a WRITE-BACK GValue pre-initialised
|
|
15
|
+
// to the target property's type; a marshaled GClosure (which unboxes GValue
|
|
16
|
+
// params, per gjs's own closure marshal) cannot reach it, so gjs — and we —
|
|
17
|
+
// use plain C transform callbacks with g_object_bind_property_full.
|
|
18
|
+
// node-gi mirrors that architecture 1:1 so the JS-visible contract matches gjs
|
|
19
|
+
// byte-for-byte (verified: scratch gold runs vs gjs 1.88 — see the test file).
|
|
20
|
+
|
|
21
|
+
#include <cstring>
|
|
22
|
+
|
|
23
|
+
#include "common.h"
|
|
24
|
+
|
|
25
|
+
namespace nodegi {
|
|
26
|
+
|
|
27
|
+
// ---- GLib.log_set_writer_func ----------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// Process-global writer state, mirroring gjs-util.c's statics. GLib enforces at
|
|
30
|
+
// most ONE g_log_set_writer_func call per process (a second call is a fatal
|
|
31
|
+
// g_error — gjs behaves identically), so a single static slot is faithful.
|
|
32
|
+
static bool g_logWriterCleared = false;
|
|
33
|
+
static napi_env g_logWriterEnv = nullptr;
|
|
34
|
+
static napi_ref g_logWriterFn = nullptr;
|
|
35
|
+
static GThread* g_logWriterThread = nullptr;
|
|
36
|
+
|
|
37
|
+
// Env teardown with the writer still installed: logs after this point must go to
|
|
38
|
+
// the default writer, and the napi_ref must not be touched on a dead env.
|
|
39
|
+
static void LogWriterEnvCleanup(void* arg) {
|
|
40
|
+
if (g_logWriterEnv != static_cast<napi_env>(arg)) return;
|
|
41
|
+
g_logWriterCleared = true;
|
|
42
|
+
// Deleting the ref during env cleanup is safe (the env is still tearing down,
|
|
43
|
+
// not freed); afterwards the wrapper never dereferences it again.
|
|
44
|
+
if (g_logWriterFn != nullptr) {
|
|
45
|
+
napi_delete_reference(g_logWriterEnv, g_logWriterFn);
|
|
46
|
+
g_logWriterFn = nullptr;
|
|
47
|
+
}
|
|
48
|
+
g_logWriterEnv = nullptr;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The C writer installed into GLib — the twin of gjs_log_writer_func_wrapper.
|
|
52
|
+
// Converts the GLogField array to a plain JS object whose values match gjs's
|
|
53
|
+
// `{...stringFields.recursiveUnpack()}` shape exactly (verified vs gjs 1.88):
|
|
54
|
+
// length < 0 (NUL-terminated) → Uint8Array of strlen bytes
|
|
55
|
+
// length > 0 (binary) → Uint8Array of length bytes
|
|
56
|
+
// length == 0 → null (gjs packs a maybe-nothing → null)
|
|
57
|
+
// and calls the JS writer as (logLevel: number, fields: object) expecting a
|
|
58
|
+
// GLib.LogWriterOutput number back; UNHANDLED (or a throw, or an off-thread /
|
|
59
|
+
// cleared / teardown call) falls back to g_log_writer_default.
|
|
60
|
+
static GLogWriterOutput NodeGiLogWriterWrapper(GLogLevelFlags log_level, const GLogField* fields,
|
|
61
|
+
gsize n_fields, gpointer /*user_data*/) {
|
|
62
|
+
if (g_logWriterCleared || g_thread_self() != g_logWriterThread || g_logWriterFn == nullptr ||
|
|
63
|
+
g_logWriterEnv == nullptr) {
|
|
64
|
+
return g_log_writer_default(log_level, fields, n_fields, nullptr);
|
|
65
|
+
}
|
|
66
|
+
napi_env env = g_logWriterEnv;
|
|
67
|
+
if (!NodeGiJsAvailable(env)) {
|
|
68
|
+
return g_log_writer_default(log_level, fields, n_fields, nullptr);
|
|
69
|
+
}
|
|
70
|
+
Napi::Env napiEnv(env);
|
|
71
|
+
Napi::HandleScope scope(napiEnv);
|
|
72
|
+
|
|
73
|
+
// Field values are plain Uint8Arrays (NOT Node Buffers): gjs's recursiveUnpack
|
|
74
|
+
// of the maybe-bytestring fields yields Uint8Array — and e.g. JSON.stringify of
|
|
75
|
+
// the two differs, so the distinction is observable byte-for-byte.
|
|
76
|
+
auto bytesValue = [&](const void* data, size_t len) -> Napi::Value {
|
|
77
|
+
Napi::Uint8Array arr = Napi::Uint8Array::New(env, len);
|
|
78
|
+
if (len > 0) memcpy(arr.Data(), data, len);
|
|
79
|
+
return arr;
|
|
80
|
+
};
|
|
81
|
+
Napi::Object obj = Napi::Object::New(napiEnv);
|
|
82
|
+
for (gsize i = 0; i < n_fields; i++) {
|
|
83
|
+
const GLogField* field = &fields[i];
|
|
84
|
+
if (field->key == nullptr) continue;
|
|
85
|
+
Napi::Value value;
|
|
86
|
+
if (field->length < 0) {
|
|
87
|
+
const char* s = static_cast<const char*>(field->value);
|
|
88
|
+
value = bytesValue(s, s != nullptr ? strlen(s) : 0);
|
|
89
|
+
} else if (field->length > 0) {
|
|
90
|
+
value = bytesValue(field->value, static_cast<size_t>(field->length));
|
|
91
|
+
} else {
|
|
92
|
+
value = napiEnv.Null();
|
|
93
|
+
}
|
|
94
|
+
obj.Set(field->key, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
napi_value fn = nullptr;
|
|
98
|
+
if (napi_get_reference_value(env, g_logWriterFn, &fn) != napi_ok || fn == nullptr) {
|
|
99
|
+
return g_log_writer_default(log_level, fields, n_fields, nullptr);
|
|
100
|
+
}
|
|
101
|
+
napi_value global = nullptr;
|
|
102
|
+
napi_get_global(env, &global);
|
|
103
|
+
napi_value args[2] = {Napi::Number::New(napiEnv, static_cast<double>(log_level)), obj};
|
|
104
|
+
napi_value ret = nullptr;
|
|
105
|
+
napi_status st = napi_make_callback(env, nullptr, global, fn, 2, args, &ret);
|
|
106
|
+
if (st != napi_ok || napiEnv.IsExceptionPending()) {
|
|
107
|
+
// A throwing writer must never wedge the logging path (nor leave a pending
|
|
108
|
+
// exception across the C boundary): surface + clear, then default-write.
|
|
109
|
+
SurfacePendingException(env, "GLib log writer func");
|
|
110
|
+
return g_log_writer_default(log_level, fields, n_fields, nullptr);
|
|
111
|
+
}
|
|
112
|
+
GLogWriterOutput output =
|
|
113
|
+
static_cast<GLogWriterOutput>(NodeGiToInt32(Napi::Value(env, ret)));
|
|
114
|
+
if (output == G_LOG_WRITER_UNHANDLED) {
|
|
115
|
+
return g_log_writer_default(log_level, fields, n_fields, nullptr);
|
|
116
|
+
}
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// logSetWriterFunc(fn | null) -> void. Installs the wrapper into GLib (a second
|
|
121
|
+
// call aborts inside GLib itself — "g_log_set_writer_func() called multiple
|
|
122
|
+
// times" — exactly as under gjs). fn == null re-arms the default fallback.
|
|
123
|
+
Napi::Value LogSetWriterFunc(const Napi::CallbackInfo& info) {
|
|
124
|
+
Napi::Env env = info.Env();
|
|
125
|
+
Napi::Value fn = info.Length() >= 1 ? info[0] : env.Null();
|
|
126
|
+
|
|
127
|
+
// Replace any previous JS-side state (the GLib-side wrapper can only ever be
|
|
128
|
+
// installed once; the JS fn/env slots are ours to swap).
|
|
129
|
+
if (g_logWriterFn != nullptr && g_logWriterEnv != nullptr) {
|
|
130
|
+
napi_delete_reference(g_logWriterEnv, g_logWriterFn);
|
|
131
|
+
g_logWriterFn = nullptr;
|
|
132
|
+
}
|
|
133
|
+
g_logWriterEnv = env;
|
|
134
|
+
g_logWriterThread = g_thread_self();
|
|
135
|
+
g_logWriterCleared = false;
|
|
136
|
+
if (fn.IsFunction()) {
|
|
137
|
+
napi_create_reference(env, fn, 1, &g_logWriterFn);
|
|
138
|
+
}
|
|
139
|
+
// The cleanup hook is registered once (adding an identical fn+arg pair twice is
|
|
140
|
+
// an N-API fatal). GLib itself enforces at most one g_log_set_writer_func call
|
|
141
|
+
// per process, so a single hook covers every reachable state.
|
|
142
|
+
static bool hookAdded = false;
|
|
143
|
+
if (!hookAdded) {
|
|
144
|
+
napi_add_env_cleanup_hook(env, LogWriterEnvCleanup, env);
|
|
145
|
+
hookAdded = true;
|
|
146
|
+
}
|
|
147
|
+
g_log_set_writer_func(NodeGiLogWriterWrapper, nullptr, nullptr);
|
|
148
|
+
return env.Undefined();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// logSetWriterDefault() -> void. The twin of gjs_log_set_writer_default: the
|
|
152
|
+
// GLib-side writer stays installed (it can never be swapped back), but every
|
|
153
|
+
// subsequent log is routed to g_log_writer_default by the cleared flag.
|
|
154
|
+
Napi::Value LogSetWriterDefault(const Napi::CallbackInfo& info) {
|
|
155
|
+
Napi::Env env = info.Env();
|
|
156
|
+
if (g_logWriterFn != nullptr && g_logWriterEnv != nullptr) {
|
|
157
|
+
napi_delete_reference(g_logWriterEnv, g_logWriterFn);
|
|
158
|
+
g_logWriterFn = nullptr;
|
|
159
|
+
}
|
|
160
|
+
g_logWriterEnv = nullptr;
|
|
161
|
+
g_logWriterThread = g_thread_self();
|
|
162
|
+
g_logWriterCleared = true;
|
|
163
|
+
return env.Undefined();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- bind_property_full / BindingGroup.bind_full ---------------------------
|
|
167
|
+
//
|
|
168
|
+
// The twin of gjs_g_object_bind_property_full: the JS transform functions are
|
|
169
|
+
// held per binding and driven by plain C GBindingTransformFunc trampolines. The
|
|
170
|
+
// JS contract (gjs gold standard, verified vs gjs 1.88):
|
|
171
|
+
// (binding, sourceValue) => [ok: boolean, targetValue]
|
|
172
|
+
// — sourceValue arrives UNPACKED, a truthy ok writes targetValue into the
|
|
173
|
+
// pre-initialised target GValue, [false, …] leaves the target unchanged, and a
|
|
174
|
+
// non-Array return is reported (gjs logs "returned unexpected value, expecting
|
|
175
|
+
// an Array") and treated as no-transform.
|
|
176
|
+
struct NodeGiBindingTransforms {
|
|
177
|
+
napi_env env;
|
|
178
|
+
napi_ref toFn; // nullptr when no transform-to
|
|
179
|
+
napi_ref fromFn; // nullptr when no transform-from
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
static gboolean NodeGiBindingTransformInvoke(GBinding* binding, const GValue* from_value,
|
|
183
|
+
GValue* to_value, NodeGiBindingTransforms* data,
|
|
184
|
+
napi_ref fnRef) {
|
|
185
|
+
if (data == nullptr || fnRef == nullptr) return FALSE;
|
|
186
|
+
napi_env env = data->env;
|
|
187
|
+
// A binding transform fired at env teardown (a dispose cascade) must not enter
|
|
188
|
+
// JS — treat as no-transform, like the signal/callback teardown gates.
|
|
189
|
+
if (!NodeGiJsAvailable(env)) return FALSE;
|
|
190
|
+
Napi::Env napiEnv(env);
|
|
191
|
+
Napi::HandleScope scope(napiEnv);
|
|
192
|
+
|
|
193
|
+
napi_value fn = nullptr;
|
|
194
|
+
if (napi_get_reference_value(env, fnRef, &fn) != napi_ok || fn == nullptr) return FALSE;
|
|
195
|
+
|
|
196
|
+
napi_value args[2] = {WrapGObject(napiEnv, G_OBJECT(binding), GI_TRANSFER_NOTHING),
|
|
197
|
+
GValueToJs(napiEnv, from_value)};
|
|
198
|
+
if (napiEnv.IsExceptionPending()) {
|
|
199
|
+
SurfacePendingException(env, "property binding transform");
|
|
200
|
+
return FALSE;
|
|
201
|
+
}
|
|
202
|
+
napi_value global = nullptr;
|
|
203
|
+
napi_get_global(env, &global);
|
|
204
|
+
napi_value ret = nullptr;
|
|
205
|
+
napi_status st = napi_make_callback(env, nullptr, global, fn, 2, args, &ret);
|
|
206
|
+
if (st != napi_ok || napiEnv.IsExceptionPending()) {
|
|
207
|
+
SurfacePendingException(env, "property binding transform");
|
|
208
|
+
return FALSE;
|
|
209
|
+
}
|
|
210
|
+
Napi::Value retVal(env, ret);
|
|
211
|
+
if (!retVal.IsArray()) {
|
|
212
|
+
// gjs: "Call to unnamed function (GjsPrivate.BindingTransformFunc) returned
|
|
213
|
+
// unexpected value, expecting an Array" — logged, transform treated as FALSE.
|
|
214
|
+
g_printerr("\n(node-gi) property binding transform returned unexpected value, "
|
|
215
|
+
"expecting an Array [ok, value]\n");
|
|
216
|
+
return FALSE;
|
|
217
|
+
}
|
|
218
|
+
Napi::Array arr = retVal.As<Napi::Array>();
|
|
219
|
+
if (!NodeGiToBool(arr.Get(uint32_t{0}))) return FALSE;
|
|
220
|
+
if (!JsToGValue(napiEnv, arr.Get(uint32_t{1}), to_value)) {
|
|
221
|
+
SurfacePendingException(env, "property binding transform");
|
|
222
|
+
return FALSE;
|
|
223
|
+
}
|
|
224
|
+
return TRUE;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static gboolean NodeGiBindingTransformTo(GBinding* binding, const GValue* from_value,
|
|
228
|
+
GValue* to_value, gpointer user_data) {
|
|
229
|
+
NodeGiBindingTransforms* data = static_cast<NodeGiBindingTransforms*>(user_data);
|
|
230
|
+
return NodeGiBindingTransformInvoke(binding, from_value, to_value, data,
|
|
231
|
+
data != nullptr ? data->toFn : nullptr);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static gboolean NodeGiBindingTransformFrom(GBinding* binding, const GValue* from_value,
|
|
235
|
+
GValue* to_value, gpointer user_data) {
|
|
236
|
+
NodeGiBindingTransforms* data = static_cast<NodeGiBindingTransforms*>(user_data);
|
|
237
|
+
return NodeGiBindingTransformInvoke(binding, from_value, to_value, data,
|
|
238
|
+
data != nullptr ? data->fromFn : nullptr);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// GDestroyNotify for the transforms — runs when the binding is destroyed
|
|
242
|
+
// (unbind / source finalized). Deleting a napi_ref needs a live env; at real env
|
|
243
|
+
// teardown the refs are reclaimed with the env, so skipping is leak-free.
|
|
244
|
+
static void NodeGiBindingTransformsFree(gpointer user_data) {
|
|
245
|
+
NodeGiBindingTransforms* data = static_cast<NodeGiBindingTransforms*>(user_data);
|
|
246
|
+
if (data == nullptr) return;
|
|
247
|
+
if (NodeGiJsAvailable(data->env)) {
|
|
248
|
+
if (data->toFn != nullptr) napi_delete_reference(data->env, data->toFn);
|
|
249
|
+
if (data->fromFn != nullptr) napi_delete_reference(data->env, data->fromFn);
|
|
250
|
+
}
|
|
251
|
+
delete data;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Shared arg parsing + invocation for bindPropertyFull / bindingGroupBindFull.
|
|
255
|
+
// info: (sourceHandle|groupHandle, sourceProperty, targetHandle, targetProperty,
|
|
256
|
+
// flags, toFn|null, fromFn|null)
|
|
257
|
+
static bool ParseBindFullArgs(const Napi::CallbackInfo& info, GObject** instance,
|
|
258
|
+
std::string* sourceProp, GObject** target, std::string* targetProp,
|
|
259
|
+
GBindingFlags* flags, NodeGiBindingTransforms** outData) {
|
|
260
|
+
Napi::Env env = info.Env();
|
|
261
|
+
if (info.Length() < 5 || !info[1].IsString() || !info[3].IsString()) {
|
|
262
|
+
Napi::TypeError::New(env,
|
|
263
|
+
"bindPropertyFull(handle, sourceProperty: string, targetHandle, "
|
|
264
|
+
"targetProperty: string, flags: number, transformTo?, transformFrom?)")
|
|
265
|
+
.ThrowAsJavaScriptException();
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
*instance = UnwrapGObject(env, info[0]);
|
|
269
|
+
if (*instance == nullptr) return false;
|
|
270
|
+
*sourceProp = info[1].As<Napi::String>().Utf8Value();
|
|
271
|
+
*target = UnwrapGObject(env, info[2]);
|
|
272
|
+
if (*target == nullptr) return false;
|
|
273
|
+
*targetProp = info[3].As<Napi::String>().Utf8Value();
|
|
274
|
+
*flags = static_cast<GBindingFlags>(NodeGiToInt32(info[4]));
|
|
275
|
+
|
|
276
|
+
Napi::Value toFn = info.Length() >= 6 ? info[5] : env.Null();
|
|
277
|
+
Napi::Value fromFn = info.Length() >= 7 ? info[6] : env.Null();
|
|
278
|
+
bool hasTo = toFn.IsFunction();
|
|
279
|
+
bool hasFrom = fromFn.IsFunction();
|
|
280
|
+
NodeGiBindingTransforms* data = nullptr;
|
|
281
|
+
if (hasTo || hasFrom) {
|
|
282
|
+
data = new NodeGiBindingTransforms();
|
|
283
|
+
data->env = env;
|
|
284
|
+
data->toFn = nullptr;
|
|
285
|
+
data->fromFn = nullptr;
|
|
286
|
+
if (hasTo) napi_create_reference(env, toFn, 1, &data->toFn);
|
|
287
|
+
if (hasFrom) napi_create_reference(env, fromFn, 1, &data->fromFn);
|
|
288
|
+
}
|
|
289
|
+
*outData = data;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// bindPropertyFull(sourceHandle, sourceProperty, targetHandle, targetProperty,
|
|
294
|
+
// flags, transformTo|null, transformFrom|null) -> Binding handle
|
|
295
|
+
Napi::Value BindPropertyFull(const Napi::CallbackInfo& info) {
|
|
296
|
+
Napi::Env env = info.Env();
|
|
297
|
+
GObject* source = nullptr;
|
|
298
|
+
GObject* target = nullptr;
|
|
299
|
+
std::string sourceProp, targetProp;
|
|
300
|
+
GBindingFlags flags = G_BINDING_DEFAULT;
|
|
301
|
+
NodeGiBindingTransforms* data = nullptr;
|
|
302
|
+
if (!ParseBindFullArgs(info, &source, &sourceProp, &target, &targetProp, &flags, &data)) {
|
|
303
|
+
return env.Null();
|
|
304
|
+
}
|
|
305
|
+
GBinding* binding = g_object_bind_property_full(
|
|
306
|
+
source, sourceProp.c_str(), target, targetProp.c_str(), flags,
|
|
307
|
+
data != nullptr && data->toFn != nullptr ? NodeGiBindingTransformTo : nullptr,
|
|
308
|
+
data != nullptr && data->fromFn != nullptr ? NodeGiBindingTransformFrom : nullptr, data,
|
|
309
|
+
data != nullptr ? NodeGiBindingTransformsFree : nullptr);
|
|
310
|
+
if (binding == nullptr) {
|
|
311
|
+
// g_object_bind_property_full already freed `data` via the destroy notify on
|
|
312
|
+
// its g_return_val_if_fail paths? It does NOT (precondition failures return
|
|
313
|
+
// before adopting user_data) — free it here so a bad property name doesn't
|
|
314
|
+
// leak the refs.
|
|
315
|
+
NodeGiBindingTransformsFree(data);
|
|
316
|
+
Napi::Error::New(env, "bind_property_full failed (unknown property?)")
|
|
317
|
+
.ThrowAsJavaScriptException();
|
|
318
|
+
return env.Null();
|
|
319
|
+
}
|
|
320
|
+
// The binding return is (transfer none) — WrapGObject takes its own ref.
|
|
321
|
+
return WrapGObject(env, G_OBJECT(binding), GI_TRANSFER_NOTHING);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// bindingGroupBindFull(groupHandle, sourceProperty, targetHandle, targetProperty,
|
|
325
|
+
// flags, transformTo|null, transformFrom|null) -> undefined
|
|
326
|
+
// The twin of gjs_g_binding_group_bind_full (GObject.BindingGroup.bind_full).
|
|
327
|
+
Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info) {
|
|
328
|
+
Napi::Env env = info.Env();
|
|
329
|
+
GObject* group = nullptr;
|
|
330
|
+
GObject* target = nullptr;
|
|
331
|
+
std::string sourceProp, targetProp;
|
|
332
|
+
GBindingFlags flags = G_BINDING_DEFAULT;
|
|
333
|
+
NodeGiBindingTransforms* data = nullptr;
|
|
334
|
+
if (!ParseBindFullArgs(info, &group, &sourceProp, &target, &targetProp, &flags, &data)) {
|
|
335
|
+
return env.Undefined();
|
|
336
|
+
}
|
|
337
|
+
if (!G_IS_BINDING_GROUP(group)) {
|
|
338
|
+
NodeGiBindingTransformsFree(data);
|
|
339
|
+
Napi::TypeError::New(env, "bind_full: expected a GObject.BindingGroup instance")
|
|
340
|
+
.ThrowAsJavaScriptException();
|
|
341
|
+
return env.Undefined();
|
|
342
|
+
}
|
|
343
|
+
g_binding_group_bind_full(
|
|
344
|
+
G_BINDING_GROUP(group), sourceProp.c_str(), target, targetProp.c_str(), flags,
|
|
345
|
+
data != nullptr && data->toFn != nullptr ? NodeGiBindingTransformTo : nullptr,
|
|
346
|
+
data != nullptr && data->fromFn != nullptr ? NodeGiBindingTransformFrom : nullptr, data,
|
|
347
|
+
data != nullptr ? NodeGiBindingTransformsFree : nullptr);
|
|
348
|
+
return env.Undefined();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
} // namespace nodegi
|
package/src/repo.cc
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Repository / introspection entry points + per-env instance-data helpers.
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
namespace nodegi {
|
|
7
|
+
|
|
8
|
+
// gi_repository_dup_default() returns a new owning ref to the process-default
|
|
9
|
+
// GIRepository (lazily created). Callers must g_object_unref() it.
|
|
10
|
+
GIRepository* DupDefaultRepository() { return gi_repository_dup_default(); }
|
|
11
|
+
|
|
12
|
+
void NodeGiEnvDataFinalize(napi_env env, void* data, void* /*hint*/) {
|
|
13
|
+
NodeGiEnvData* d = static_cast<NodeGiEnvData*>(data);
|
|
14
|
+
if (d == nullptr) return;
|
|
15
|
+
if (d->errorBuilder != nullptr) napi_delete_reference(env, d->errorBuilder);
|
|
16
|
+
if (d->templateCallbackResolver != nullptr)
|
|
17
|
+
napi_delete_reference(env, d->templateCallbackResolver);
|
|
18
|
+
if (d->cairoWrappers != nullptr) napi_delete_reference(env, d->cairoWrappers);
|
|
19
|
+
if (d->microtaskDrain != nullptr) napi_delete_reference(env, d->microtaskDrain);
|
|
20
|
+
delete d;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// This env's instance data (created in Init); null only if Init's set failed.
|
|
24
|
+
NodeGiEnvData* EnvData(napi_env env) {
|
|
25
|
+
void* raw = nullptr;
|
|
26
|
+
return napi_get_instance_data(env, &raw) == napi_ok ? static_cast<NodeGiEnvData*>(raw) : nullptr;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// requireNamespace(namespace: string, version?: string)
|
|
30
|
+
// -> { namespace: string, version: string, infoCount: number }
|
|
31
|
+
//
|
|
32
|
+
// Mirrors the load step every GJS gi:// / imports.gi access performs:
|
|
33
|
+
// gi_repository_require() with an optional pinned version (null = let
|
|
34
|
+
// GIRepository resolve the system default), then read back the resolved
|
|
35
|
+
// version and the top-level info count.
|
|
36
|
+
Napi::Value RequireNamespace(const Napi::CallbackInfo& info) {
|
|
37
|
+
Napi::Env env = info.Env();
|
|
38
|
+
|
|
39
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
40
|
+
Napi::TypeError::New(env, "requireNamespace(namespace: string, version?: string)")
|
|
41
|
+
.ThrowAsJavaScriptException();
|
|
42
|
+
return env.Null();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
46
|
+
bool has_version = info.Length() >= 2 && info[1].IsString();
|
|
47
|
+
std::string version = has_version ? info[1].As<Napi::String>().Utf8Value() : std::string();
|
|
48
|
+
|
|
49
|
+
GIRepository* repo = DupDefaultRepository();
|
|
50
|
+
GError* error = nullptr;
|
|
51
|
+
GITypelib* typelib = gi_repository_require(
|
|
52
|
+
repo, ns.c_str(), has_version ? version.c_str() : nullptr,
|
|
53
|
+
GI_REPOSITORY_LOAD_FLAG_NONE, &error);
|
|
54
|
+
|
|
55
|
+
if (typelib == nullptr) {
|
|
56
|
+
std::string message = error != nullptr ? error->message : "unknown error";
|
|
57
|
+
if (error != nullptr) g_error_free(error);
|
|
58
|
+
g_object_unref(repo);
|
|
59
|
+
Napi::Error::New(env, "Failed to require " + ns +
|
|
60
|
+
(has_version ? (" " + version) : "") + ": " + message)
|
|
61
|
+
.ThrowAsJavaScriptException();
|
|
62
|
+
return env.Null();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const char* resolved = gi_repository_get_version(repo, ns.c_str());
|
|
66
|
+
unsigned int n_infos = gi_repository_get_n_infos(repo, ns.c_str());
|
|
67
|
+
|
|
68
|
+
Napi::Object result = Napi::Object::New(env);
|
|
69
|
+
result.Set("namespace", Napi::String::New(env, ns));
|
|
70
|
+
result.Set("version", Napi::String::New(env, resolved != nullptr ? resolved : ""));
|
|
71
|
+
result.Set("infoCount", Napi::Number::New(env, static_cast<double>(n_infos)));
|
|
72
|
+
|
|
73
|
+
g_object_unref(repo);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// listInfoNames(namespace: string) -> string[]
|
|
78
|
+
// Enumerates the top-level introspection infos of an already-required
|
|
79
|
+
// namespace. Proves gi_repository_get_info() + gi_base_info_get_name()/unref().
|
|
80
|
+
Napi::Value ListInfoNames(const Napi::CallbackInfo& info) {
|
|
81
|
+
Napi::Env env = info.Env();
|
|
82
|
+
|
|
83
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
84
|
+
Napi::TypeError::New(env, "listInfoNames(namespace: string)")
|
|
85
|
+
.ThrowAsJavaScriptException();
|
|
86
|
+
return env.Null();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
90
|
+
GIRepository* repo = DupDefaultRepository();
|
|
91
|
+
unsigned int n_infos = gi_repository_get_n_infos(repo, ns.c_str());
|
|
92
|
+
|
|
93
|
+
Napi::Array names = Napi::Array::New(env, n_infos);
|
|
94
|
+
for (unsigned int i = 0; i < n_infos; i++) {
|
|
95
|
+
GIBaseInfo* base = gi_repository_get_info(repo, ns.c_str(), i);
|
|
96
|
+
const char* name = gi_base_info_get_name(base);
|
|
97
|
+
names.Set(i, Napi::String::New(env, name != nullptr ? name : ""));
|
|
98
|
+
gi_base_info_unref(base);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
g_object_unref(repo);
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// findInfo(namespace, name) -> { kind: string } | null
|
|
106
|
+
// Classify a top-level namespace member so the L1 wrapper knows whether to treat
|
|
107
|
+
// it as a constructible class, a callable function, an enum, a constant, etc.
|
|
108
|
+
// kind ∈ function|object|interface|struct|union|enum|flags|constant|callback|other.
|
|
109
|
+
Napi::Value FindInfo(const Napi::CallbackInfo& info) {
|
|
110
|
+
Napi::Env env = info.Env();
|
|
111
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
112
|
+
Napi::TypeError::New(env, "findInfo(namespace: string, name: string)")
|
|
113
|
+
.ThrowAsJavaScriptException();
|
|
114
|
+
return env.Null();
|
|
115
|
+
}
|
|
116
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
117
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
118
|
+
GIRepository* repo = DupDefaultRepository();
|
|
119
|
+
GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
|
|
120
|
+
if (base == nullptr) {
|
|
121
|
+
g_object_unref(repo);
|
|
122
|
+
return env.Null();
|
|
123
|
+
}
|
|
124
|
+
// Order matters: GIFlagsInfo derives from GIEnumInfo, so test flags first.
|
|
125
|
+
const char* kind;
|
|
126
|
+
if (GI_IS_FUNCTION_INFO(base)) {
|
|
127
|
+
kind = "function";
|
|
128
|
+
} else if (GI_IS_FLAGS_INFO(base)) {
|
|
129
|
+
kind = "flags";
|
|
130
|
+
} else if (GI_IS_ENUM_INFO(base)) {
|
|
131
|
+
kind = "enum";
|
|
132
|
+
} else if (GI_IS_OBJECT_INFO(base)) {
|
|
133
|
+
kind = "object";
|
|
134
|
+
} else if (GI_IS_INTERFACE_INFO(base)) {
|
|
135
|
+
kind = "interface";
|
|
136
|
+
} else if (GI_IS_STRUCT_INFO(base)) {
|
|
137
|
+
kind = "struct";
|
|
138
|
+
} else if (GI_IS_UNION_INFO(base)) {
|
|
139
|
+
kind = "union";
|
|
140
|
+
} else if (GI_IS_CONSTANT_INFO(base)) {
|
|
141
|
+
kind = "constant";
|
|
142
|
+
} else if (GI_IS_CALLBACK_INFO(base)) {
|
|
143
|
+
kind = "callback";
|
|
144
|
+
} else {
|
|
145
|
+
kind = "other";
|
|
146
|
+
}
|
|
147
|
+
gi_base_info_unref(base);
|
|
148
|
+
g_object_unref(repo);
|
|
149
|
+
Napi::Object result = Napi::Object::New(env);
|
|
150
|
+
result.Set("kind", Napi::String::New(env, kind));
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// getConstantValue(namespace, name) -> unknown
|
|
155
|
+
// Read a namespace-level GI constant (e.g. GLib.PRIORITY_DEFAULT) and marshal it
|
|
156
|
+
// to JS. The constant owns its storage, so we copy out (transfer NOTHING) then
|
|
157
|
+
// free it.
|
|
158
|
+
Napi::Value GetConstantValue(const Napi::CallbackInfo& info) {
|
|
159
|
+
Napi::Env env = info.Env();
|
|
160
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
161
|
+
Napi::TypeError::New(env, "getConstantValue(namespace: string, name: string)")
|
|
162
|
+
.ThrowAsJavaScriptException();
|
|
163
|
+
return env.Null();
|
|
164
|
+
}
|
|
165
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
166
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
167
|
+
GIRepository* repo = DupDefaultRepository();
|
|
168
|
+
GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
|
|
169
|
+
if (base == nullptr || !GI_IS_CONSTANT_INFO(base)) {
|
|
170
|
+
if (base != nullptr) gi_base_info_unref(base);
|
|
171
|
+
g_object_unref(repo);
|
|
172
|
+
Napi::TypeError::New(env, ns + "." + name + " is not a constant")
|
|
173
|
+
.ThrowAsJavaScriptException();
|
|
174
|
+
return env.Null();
|
|
175
|
+
}
|
|
176
|
+
GIConstantInfo* ci = reinterpret_cast<GIConstantInfo*>(base);
|
|
177
|
+
GIArgument arg;
|
|
178
|
+
gi_constant_info_get_value(ci, &arg);
|
|
179
|
+
GITypeInfo* ti = gi_constant_info_get_type_info(ci);
|
|
180
|
+
// transfer NOTHING: GIArgumentToJs copies strings (Napi::String::New) rather
|
|
181
|
+
// than g_free'ing them; gi_constant_info_free_value owns the release.
|
|
182
|
+
Napi::Value result = GIArgumentToJs(env, ti, &arg, GI_TRANSFER_NOTHING);
|
|
183
|
+
gi_constant_info_free_value(ci, &arg);
|
|
184
|
+
gi_base_info_unref(ti);
|
|
185
|
+
gi_base_info_unref(base);
|
|
186
|
+
g_object_unref(repo);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// getEnumValues(namespace, name) -> Record<string, number>
|
|
191
|
+
// Enumerate an enum/flags type's members as { rawGiName: int }. The L1 wrapper
|
|
192
|
+
// re-keys them GJS-style (UPPER_CASE, '-' -> '_').
|
|
193
|
+
Napi::Value GetEnumValues(const Napi::CallbackInfo& info) {
|
|
194
|
+
Napi::Env env = info.Env();
|
|
195
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
196
|
+
Napi::TypeError::New(env, "getEnumValues(namespace: string, name: string)")
|
|
197
|
+
.ThrowAsJavaScriptException();
|
|
198
|
+
return env.Null();
|
|
199
|
+
}
|
|
200
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
201
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
202
|
+
GIRepository* repo = DupDefaultRepository();
|
|
203
|
+
GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
|
|
204
|
+
if (base == nullptr || !GI_IS_ENUM_INFO(base)) {
|
|
205
|
+
if (base != nullptr) gi_base_info_unref(base);
|
|
206
|
+
g_object_unref(repo);
|
|
207
|
+
Napi::TypeError::New(env, ns + "." + name + " is not an enum or flags type")
|
|
208
|
+
.ThrowAsJavaScriptException();
|
|
209
|
+
return env.Null();
|
|
210
|
+
}
|
|
211
|
+
GIEnumInfo* ei = reinterpret_cast<GIEnumInfo*>(base);
|
|
212
|
+
unsigned int n = gi_enum_info_get_n_values(ei);
|
|
213
|
+
Napi::Object result = Napi::Object::New(env);
|
|
214
|
+
for (unsigned int i = 0; i < n; i++) {
|
|
215
|
+
GIValueInfo* vi = gi_enum_info_get_value(ei, i);
|
|
216
|
+
const char* vname = gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(vi));
|
|
217
|
+
int64_t val = gi_value_info_get_value(vi);
|
|
218
|
+
result.Set(vname != nullptr ? vname : "", Napi::Number::New(env, static_cast<double>(val)));
|
|
219
|
+
gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(vi));
|
|
220
|
+
}
|
|
221
|
+
gi_base_info_unref(base);
|
|
222
|
+
g_object_unref(repo);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// getErrorDomain(namespace, name) -> { name: string, quark: number } | null
|
|
227
|
+
// For an enum type registered as a GError domain (e.g. Gio.IOErrorEnum), report
|
|
228
|
+
// its domain quark name + numeric quark. L1 attaches these to the enum object so
|
|
229
|
+
// `error.matches(Gio.IOErrorEnum, code)` can resolve the enum to its domain.
|
|
230
|
+
// Returns null for a plain (non-error-domain) enum or a non-enum name.
|
|
231
|
+
Napi::Value GetErrorDomain(const Napi::CallbackInfo& info) {
|
|
232
|
+
Napi::Env env = info.Env();
|
|
233
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
234
|
+
Napi::TypeError::New(env, "getErrorDomain(namespace: string, name: string)")
|
|
235
|
+
.ThrowAsJavaScriptException();
|
|
236
|
+
return env.Null();
|
|
237
|
+
}
|
|
238
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
239
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
240
|
+
GIRepository* repo = DupDefaultRepository();
|
|
241
|
+
GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), name.c_str());
|
|
242
|
+
if (base == nullptr || !GI_IS_ENUM_INFO(base)) {
|
|
243
|
+
if (base != nullptr) gi_base_info_unref(base);
|
|
244
|
+
g_object_unref(repo);
|
|
245
|
+
return env.Null();
|
|
246
|
+
}
|
|
247
|
+
const char* domain = gi_enum_info_get_error_domain(reinterpret_cast<GIEnumInfo*>(base));
|
|
248
|
+
Napi::Value result = env.Null();
|
|
249
|
+
if (domain != nullptr) {
|
|
250
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
251
|
+
obj.Set("name", Napi::String::New(env, domain));
|
|
252
|
+
obj.Set("quark", Napi::Number::New(env, static_cast<double>(g_quark_from_string(domain))));
|
|
253
|
+
result = obj;
|
|
254
|
+
}
|
|
255
|
+
gi_base_info_unref(base);
|
|
256
|
+
g_object_unref(repo);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// setErrorBuilder(builder: (domainName, domainQuark, code, message) => Error) -> void
|
|
261
|
+
// Register the L1 GLib.Error factory the engine calls when a GI invoke fails, so a
|
|
262
|
+
// failed sync call throws a real GLib.Error. Stored per-env (a napi_ref is env-
|
|
263
|
+
// specific; the throw path is gated on the same env).
|
|
264
|
+
Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info) {
|
|
265
|
+
Napi::Env env = info.Env();
|
|
266
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
267
|
+
Napi::TypeError::New(env, "setErrorBuilder(builder: function)").ThrowAsJavaScriptException();
|
|
268
|
+
return env.Undefined();
|
|
269
|
+
}
|
|
270
|
+
NodeGiEnvData* d = EnvData(env);
|
|
271
|
+
if (d == nullptr) return env.Undefined(); // instance data unavailable (Init failed)
|
|
272
|
+
if (d->errorBuilder != nullptr) {
|
|
273
|
+
napi_delete_reference(env, d->errorBuilder);
|
|
274
|
+
d->errorBuilder = nullptr;
|
|
275
|
+
}
|
|
276
|
+
napi_create_reference(env, info[0], 1, &d->errorBuilder);
|
|
277
|
+
return env.Undefined();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// prependSearchPath(path: string) -> void
|
|
281
|
+
// Lets a caller add a typelib search directory before requiring (the Node twin
|
|
282
|
+
// of GIRepository.prepend_search_path(); needed for non-system typelibs).
|
|
283
|
+
Napi::Value PrependSearchPath(const Napi::CallbackInfo& info) {
|
|
284
|
+
Napi::Env env = info.Env();
|
|
285
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
286
|
+
Napi::TypeError::New(env, "prependSearchPath(path: string)")
|
|
287
|
+
.ThrowAsJavaScriptException();
|
|
288
|
+
return env.Undefined();
|
|
289
|
+
}
|
|
290
|
+
std::string path = info[0].As<Napi::String>().Utf8Value();
|
|
291
|
+
GIRepository* repo = DupDefaultRepository();
|
|
292
|
+
gi_repository_prepend_search_path(repo, path.c_str());
|
|
293
|
+
g_object_unref(repo);
|
|
294
|
+
return env.Undefined();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
} // namespace nodegi
|