@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/calls.cc
ADDED
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// GI invocation: JS callbacks (ffi closures), GError -> GLib.Error, the shared invoke core + call entry points.
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
namespace nodegi {
|
|
7
|
+
|
|
8
|
+
// Shared invocation core: marshal the JS args into a GIArgument vector (the
|
|
9
|
+
// instance prepended for methods), call gi_function_info_invoke, marshal the
|
|
10
|
+
// return + any OUT/INOUT values. IN is passed by value; OUT/INOUT route through
|
|
11
|
+
// a per-arg storage slot whose address the invoker hands the C callee. The JS
|
|
12
|
+
// return follows the GJS convention (see InvokeFunctionInfo).
|
|
13
|
+
// ---- callbacks (JS function -> GI callback via an ffi closure) ----
|
|
14
|
+
//
|
|
15
|
+
// A JS function passed where a GI callback is expected (e.g. GLib.timeout_add's
|
|
16
|
+
// GSourceFunc) is wrapped in an ffi closure created by girepository
|
|
17
|
+
// (gi_callable_info_create_closure). When the C library invokes it, the
|
|
18
|
+
// trampoline marshals the C args to JS, calls the function, and marshals the
|
|
19
|
+
// return back. The function's associated user_data slot carries the wrapper
|
|
20
|
+
// pointer and the destroy-notify slot frees it (scope = notified); call-scope
|
|
21
|
+
// closures are freed by the caller after the invoke; async closures free
|
|
22
|
+
// themselves after the first call. Reference: refs/node-gtk src/callback.cc.
|
|
23
|
+
struct NodeGiCallback {
|
|
24
|
+
napi_env env;
|
|
25
|
+
napi_ref jsFn;
|
|
26
|
+
GICallableInfo* info; // the callback type (owned)
|
|
27
|
+
ffi_cif cif;
|
|
28
|
+
ffi_closure* closure; // from gi_callable_info_create_closure
|
|
29
|
+
gpointer native; // executable trampoline address
|
|
30
|
+
GIScopeType scope;
|
|
31
|
+
// scope=async only: counted in the auto-pump's in-flight keep-alive (the
|
|
32
|
+
// pending completion keeps the Node process alive, like in-flight Node I/O).
|
|
33
|
+
// Released exactly once — at the trampoline's single invocation, or at free
|
|
34
|
+
// if the callback never fired (an error path).
|
|
35
|
+
bool pumpCounted = false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
static void NodeGiCallbackPumpRelease(NodeGiCallback* cb) {
|
|
39
|
+
if (cb != nullptr && cb->pumpCounted) {
|
|
40
|
+
cb->pumpCounted = false;
|
|
41
|
+
NodeGiPumpAsyncEnd();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static void NodeGiCallbackFree(NodeGiCallback* cb) {
|
|
46
|
+
if (cb == nullptr) return;
|
|
47
|
+
NodeGiCallbackPumpRelease(cb); // never fired (error/teardown path)
|
|
48
|
+
if (cb->jsFn != nullptr) napi_delete_reference(cb->env, cb->jsFn);
|
|
49
|
+
if (cb->closure != nullptr) gi_callable_info_destroy_closure(cb->info, cb->closure);
|
|
50
|
+
if (cb->info != nullptr) gi_base_info_unref(cb->info);
|
|
51
|
+
delete cb;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// GDestroyNotify for scope=notified callbacks; user_data is the NodeGiCallback*.
|
|
55
|
+
static void NodeGiCallbackDestroyNotify(gpointer user_data) {
|
|
56
|
+
NodeGiCallbackFree(static_cast<NodeGiCallback*>(user_data));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One-shot GSourceFunc that frees a scope=async callback AFTER its single
|
|
60
|
+
// invocation. A GAsyncReadyCallback fires exactly once and has no destroy-notify,
|
|
61
|
+
// so the trampoline schedules this on the main loop once the call returns —
|
|
62
|
+
// freeing it inline would destroy the ffi closure's own executable trampoline
|
|
63
|
+
// while it is still on the call stack (UB). See NodeGiCallbackTrampoline tail.
|
|
64
|
+
static gboolean NodeGiCallbackFreeIdle(gpointer user_data) {
|
|
65
|
+
NodeGiCallbackFree(static_cast<NodeGiCallback*>(user_data));
|
|
66
|
+
return G_SOURCE_REMOVE;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// girepository-2.0's closure/destroy index getters are out-param + gboolean;
|
|
70
|
+
// return the index or -1 when the arg has no associated slot.
|
|
71
|
+
static int ArgClosureIndex(GIArgInfo* ai) {
|
|
72
|
+
unsigned int idx = 0;
|
|
73
|
+
return gi_arg_info_get_closure_index(ai, &idx) ? static_cast<int>(idx) : -1;
|
|
74
|
+
}
|
|
75
|
+
static int ArgDestroyIndex(GIArgInfo* ai) {
|
|
76
|
+
unsigned int idx = 0;
|
|
77
|
+
return gi_arg_info_get_destroy_index(ai, &idx) ? static_cast<int>(idx) : -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Depth of a synchronous emitSignal() in progress on the (main) thread. While
|
|
81
|
+
// > 0, a signal-handler exception must PROPAGATE to the JS emit() caller (the
|
|
82
|
+
// node-gi contract — see test/signals.test.mjs "a handler exception propagates
|
|
83
|
+
// out of emit"). At depth 0 the closure was dispatched from the GLib main loop (a
|
|
84
|
+
// GTK signal, a notify::, a template handler) where there is NO JS caller to
|
|
85
|
+
// catch it, so it is surfaced + cleared (below) to keep the loop alive instead of
|
|
86
|
+
// wedging libuv on a never-cleared pending exception.
|
|
87
|
+
int g_syncEmitDepth = 0;
|
|
88
|
+
|
|
89
|
+
// Nesting depth of loop-dispatched C->JS callbacks (see common.h). Incremented
|
|
90
|
+
// around every loop-dispatched napi_make_callback (signal closures, GI
|
|
91
|
+
// callbacks, vfuncs) so the cross-runtime microtask checkpoint
|
|
92
|
+
// (NodeGiMaybeDrainMicrotasks) fires only at the OUTERMOST callback boundary.
|
|
93
|
+
int g_loopDispatchDepth = 0;
|
|
94
|
+
|
|
95
|
+
// Surface (log) + clear a pending JS exception left by a C-invoked callback so it
|
|
96
|
+
// cannot wedge the GLib/libuv main loop. A signal handler / idle / timeout that
|
|
97
|
+
// throws is reported to stderr (message + stack) and swallowed — mirroring how
|
|
98
|
+
// GJS reports an uncaught signal-handler exception (gjs_log_exception) instead of
|
|
99
|
+
// crashing the loop. Uses g_printerr, NOT g_warning, so a G_DEBUG=fatal-warnings
|
|
100
|
+
// test environment cannot turn a reported handler bug into an abort. No-op when
|
|
101
|
+
// nothing is pending. Returns true if it cleared an exception.
|
|
102
|
+
bool SurfacePendingException(napi_env env, const char* context) {
|
|
103
|
+
bool pending = false;
|
|
104
|
+
if (napi_is_exception_pending(env, &pending) != napi_ok || !pending) return false;
|
|
105
|
+
napi_value ex = nullptr;
|
|
106
|
+
if (napi_get_and_clear_last_exception(env, &ex) != napi_ok || ex == nullptr) return false;
|
|
107
|
+
|
|
108
|
+
// Prefer Error.stack (message + trace); fall back to String(ex).
|
|
109
|
+
std::string text;
|
|
110
|
+
napi_value field = nullptr;
|
|
111
|
+
napi_valuetype vt = napi_undefined;
|
|
112
|
+
if (napi_get_named_property(env, ex, "stack", &field) == napi_ok &&
|
|
113
|
+
napi_typeof(env, field, &vt) == napi_ok && vt == napi_string) {
|
|
114
|
+
size_t len = 0;
|
|
115
|
+
if (napi_get_value_string_utf8(env, field, nullptr, 0, &len) == napi_ok) {
|
|
116
|
+
text.resize(len);
|
|
117
|
+
napi_get_value_string_utf8(env, field, text.data(), len + 1, &len);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (text.empty()) {
|
|
121
|
+
napi_value str = nullptr;
|
|
122
|
+
if (napi_coerce_to_string(env, ex, &str) == napi_ok) {
|
|
123
|
+
size_t len = 0;
|
|
124
|
+
if (napi_get_value_string_utf8(env, str, nullptr, 0, &len) == napi_ok) {
|
|
125
|
+
text.resize(len);
|
|
126
|
+
napi_get_value_string_utf8(env, str, text.data(), len + 1, &len);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
g_printerr("\n(node-gi) Unhandled exception in %s:\n%s\n", context,
|
|
131
|
+
text.empty() ? "<no message>" : text.c_str());
|
|
132
|
+
|
|
133
|
+
// Building `text` above can itself run user JS — a throwing `stack` getter
|
|
134
|
+
// (napi_get_named_property) or a throwing `toString` (napi_coerce_to_string) —
|
|
135
|
+
// which leaves a NEW pending exception. If we returned now the caller would be
|
|
136
|
+
// env-dirty and could wedge the loop exactly like the original throw. Re-check
|
|
137
|
+
// and clear so the env is guaranteed clean on return.
|
|
138
|
+
bool stillPending = false;
|
|
139
|
+
if (napi_is_exception_pending(env, &stillPending) == napi_ok && stillPending) {
|
|
140
|
+
napi_value ignored = nullptr;
|
|
141
|
+
napi_get_and_clear_last_exception(env, &ignored);
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// RAII: pin g_syncEmitDepth to 0 for the duration of a LOOP-DISPATCHED callback,
|
|
147
|
+
// restoring the prior value on scope exit. g_syncEmitDepth marks "a synchronous
|
|
148
|
+
// emitSignal() is on the stack" — but a synchronous handler may re-enter the GLib
|
|
149
|
+
// loop (a nested loop.run() / g_main_context_iteration, or a blocking GIO call that
|
|
150
|
+
// pumps the context). Any closure the NESTED loop dispatches (a notify:: from
|
|
151
|
+
// g_object_set inside a timeout, a GTK signal) is genuinely loop-dispatched and has
|
|
152
|
+
// no JS emit() caller to catch its throw, yet the global counter would still read
|
|
153
|
+
// > 0 and JsClosureMarshal would leave the exception PENDING → DrainMicrotasks
|
|
154
|
+
// early-returns → the libuv↔GLib pump wedges. Resetting to 0 across the loop-
|
|
155
|
+
// dispatch boundary makes those closures evaluate at depth 0 (surface + clear),
|
|
156
|
+
// while the OUTER synchronous emit() — resumed once the nested loop returns and the
|
|
157
|
+
// prior depth is restored — still propagates its own handler's throw.
|
|
158
|
+
struct SyncEmitDepthReset {
|
|
159
|
+
int saved;
|
|
160
|
+
SyncEmitDepthReset() : saved(g_syncEmitDepth) { g_syncEmitDepth = 0; }
|
|
161
|
+
~SyncEmitDepthReset() { g_syncEmitDepth = saved; }
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// The ffi closure entry point: marshal C args -> JS, call the JS fn, marshal the
|
|
165
|
+
// JS return -> C. Runs on the main thread (the GLib loop the bridge pumps).
|
|
166
|
+
static void NodeGiCallbackTrampoline(ffi_cif* /*cif*/, void* result, void** args,
|
|
167
|
+
gpointer user_data) {
|
|
168
|
+
NodeGiCallback* cb = static_cast<NodeGiCallback*>(user_data);
|
|
169
|
+
napi_env env = cb->env;
|
|
170
|
+
// scope=async fires exactly once — the in-flight completion has arrived, so
|
|
171
|
+
// drop its auto-pump keep-alive ref (whether or not JS can still be entered).
|
|
172
|
+
NodeGiCallbackPumpRelease(cb);
|
|
173
|
+
// ENV-TEARDOWN GATE: an idle/timeout/async callback (or a destroy-notify-driven
|
|
174
|
+
// one) can fire while the env may no longer enter JS (env cleanup, worker
|
|
175
|
+
// terminate). Re-entering N-API then aborts via node-addon-api's noexcept throw
|
|
176
|
+
// path — degrade to a no-op with a zeroed return slot instead (see toggle.cc).
|
|
177
|
+
if (!NodeGiJsAvailable(env)) {
|
|
178
|
+
if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
|
|
179
|
+
if (NodeGiToggleDebugEnabled())
|
|
180
|
+
NodeGiToggleDebugLog("GI callback skipped: JS unavailable on env %p (teardown/terminate)",
|
|
181
|
+
static_cast<void*>(env));
|
|
182
|
+
// The probe is also false when a JS exception is PENDING on a live env. Keep
|
|
183
|
+
// the pre-existing scope semantics: a loop-dispatched (non-CALL) callback
|
|
184
|
+
// surfaces + clears so the pump stays alive; a CALL-scope callback leaves it
|
|
185
|
+
// pending for the JS invoke caller. At real teardown Surface... is a no-op.
|
|
186
|
+
if (cb->scope != GI_SCOPE_TYPE_CALL) {
|
|
187
|
+
SurfacePendingException(env, "GI callback (idle/timeout/async)");
|
|
188
|
+
}
|
|
189
|
+
// scope=async frees exactly once via the trampoline (see below) — keep that
|
|
190
|
+
// contract even when the JS dispatch is skipped, so the closure is not leaked.
|
|
191
|
+
if (cb->scope == GI_SCOPE_TYPE_ASYNC) {
|
|
192
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, NodeGiCallbackFreeIdle, cb, nullptr);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
Napi::Env napiEnv(env);
|
|
197
|
+
Napi::HandleScope scope(napiEnv);
|
|
198
|
+
// JS dispatched from a pump-driven context iteration (and every microtask
|
|
199
|
+
// continuation napi_make_callback runs at its boundary) must not inherit the
|
|
200
|
+
// pump's in-iteration flag — a blocking loop.run() it starts needs the
|
|
201
|
+
// UvLoopSource co-pump live. No-op when the flag is already clear.
|
|
202
|
+
NodeGiPumpJsDispatchScope pumpWindow;
|
|
203
|
+
// An idle/timeout/async (NOTIFIED/ASYNC scope) callback is dispatched FROM the
|
|
204
|
+
// GLib loop. Pin the synchronous-emit depth to 0 so a signal this callback emits
|
|
205
|
+
// (e.g. a notify:: from g_object_set) is treated as loop-dispatched even when a
|
|
206
|
+
// synchronous emitSignal() is suspended higher on the stack across a nested loop.
|
|
207
|
+
// A CALL-scope callback runs synchronously inside a JS-initiated GI invoke, so it
|
|
208
|
+
// keeps the ambient depth (its exception must reach that JS caller).
|
|
209
|
+
std::unique_ptr<SyncEmitDepthReset> depthReset;
|
|
210
|
+
if (cb->scope != GI_SCOPE_TYPE_CALL) depthReset = std::make_unique<SyncEmitDepthReset>();
|
|
211
|
+
|
|
212
|
+
GICallableInfo* ci = cb->info;
|
|
213
|
+
unsigned int n = gi_callable_info_get_n_args(ci);
|
|
214
|
+
std::vector<napi_value> jsArgs;
|
|
215
|
+
jsArgs.reserve(n);
|
|
216
|
+
bool ok = true;
|
|
217
|
+
for (unsigned int i = 0; i < n; i++) {
|
|
218
|
+
GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
|
|
219
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
220
|
+
// ffi hands each argument's storage as args[i]; reinterpret as a GIArgument
|
|
221
|
+
// union (a user_data/void arg marshals to undefined, which JS ignores).
|
|
222
|
+
Napi::Value v = GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i]), GI_TRANSFER_NOTHING);
|
|
223
|
+
gi_base_info_unref(ti);
|
|
224
|
+
gi_base_info_unref(ai);
|
|
225
|
+
if (napiEnv.IsExceptionPending()) {
|
|
226
|
+
ok = false;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
jsArgs.push_back(v);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
|
|
233
|
+
// upper bytes indeterminate otherwise).
|
|
234
|
+
if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
|
|
235
|
+
|
|
236
|
+
GITypeInfo* retType = gi_callable_info_get_return_type(ci);
|
|
237
|
+
if (ok) {
|
|
238
|
+
napi_value fn = nullptr;
|
|
239
|
+
if (napi_get_reference_value(env, cb->jsFn, &fn) == napi_ok && fn != nullptr) {
|
|
240
|
+
napi_value global = nullptr;
|
|
241
|
+
napi_get_global(env, &global);
|
|
242
|
+
napi_value ret = nullptr;
|
|
243
|
+
// napi_make_callback drains nextTick/microtasks around the call (Node; on
|
|
244
|
+
// Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below).
|
|
245
|
+
g_loopDispatchDepth++;
|
|
246
|
+
napi_status st = napi_make_callback(env, nullptr, global, fn, jsArgs.size(), jsArgs.data(), &ret);
|
|
247
|
+
g_loopDispatchDepth--;
|
|
248
|
+
if (st == napi_ok && result != nullptr) {
|
|
249
|
+
GITypeTag rtag = gi_type_info_get_tag(retType);
|
|
250
|
+
if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
|
|
251
|
+
// Hand the caller an owned copy — a JsToGIArgument string would point
|
|
252
|
+
// into a std::string that dies with this frame.
|
|
253
|
+
Napi::Value rv(env, ret);
|
|
254
|
+
static_cast<GIArgument*>(result)->v_string =
|
|
255
|
+
rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
|
|
256
|
+
} else if (rtag != GI_TYPE_TAG_VOID) {
|
|
257
|
+
std::string held;
|
|
258
|
+
JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
|
|
259
|
+
&held);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
gi_base_info_unref(retType);
|
|
265
|
+
// A CALL-scope callback (e.g. a list `foreach`) runs synchronously inside a
|
|
266
|
+
// JS-initiated GI invoke, so a pending exception must propagate to that JS
|
|
267
|
+
// caller — leave it to surface at the invoke's N-API boundary. A NOTIFIED/ASYNC
|
|
268
|
+
// callback (idle/timeout/GAsyncReadyCallback) is dispatched FROM the GLib loop
|
|
269
|
+
// with no JS caller to catch it; a left-pending exception would wedge the
|
|
270
|
+
// libuv↔GLib pump (DrainMicrotasks stops draining while one is pending). Surface
|
|
271
|
+
// + clear it so the loop keeps running (GJS logs an uncaught callback exception
|
|
272
|
+
// rather than stalling the loop).
|
|
273
|
+
if (napiEnv.IsExceptionPending() && cb->scope != GI_SCOPE_TYPE_CALL) {
|
|
274
|
+
SurfacePendingException(env, "GI callback (idle/timeout/async)");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Cross-runtime microtask checkpoint (Bun/Deno — no-op on Node, see loop.cc):
|
|
278
|
+
// a loop-dispatched (NOTIFIED/ASYNC) callback with no dispatch nesting is the
|
|
279
|
+
// outermost C→JS boundary, so promise continuations it queued (a GLib-timeout
|
|
280
|
+
// handler resolving an awaited Promise — the async-DBus-reply chain) drain
|
|
281
|
+
// NOW, matching Node's napi_make_callback checkpoint and GJS. A CALL-scope
|
|
282
|
+
// callback runs under a live JS caller whose stack unwind drains normally.
|
|
283
|
+
if (cb->scope != GI_SCOPE_TYPE_CALL && g_loopDispatchDepth == 0) {
|
|
284
|
+
NodeGiMaybeDrainMicrotasks(env);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// scope=async (e.g. a GAsyncReadyCallback) fires EXACTLY once and carries no
|
|
288
|
+
// destroy-notify, so it is the trampoline's job to free it. It is NOT in
|
|
289
|
+
// callScope (so it survived the invoke), and freeing it inline would destroy
|
|
290
|
+
// this ffi closure's own executable trampoline mid-call — so defer the free to
|
|
291
|
+
// the next main-loop iteration, when the closure is no longer on the stack.
|
|
292
|
+
if (cb->scope == GI_SCOPE_TYPE_ASYNC) {
|
|
293
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, NodeGiCallbackFreeIdle, cb, nullptr);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create an ffi closure wrapping a JS function for a GI callback-typed arg.
|
|
298
|
+
static NodeGiCallback* CreateCallback(Napi::Env env, Napi::Function fn, GICallableInfo* callbackInfo,
|
|
299
|
+
GIScopeType scope) {
|
|
300
|
+
NodeGiCallback* cb = new NodeGiCallback();
|
|
301
|
+
cb->env = env;
|
|
302
|
+
cb->info = reinterpret_cast<GICallableInfo*>(gi_base_info_ref(callbackInfo));
|
|
303
|
+
cb->scope = scope;
|
|
304
|
+
// An async-scope callback is a one-shot in-flight operation (GAsyncReadyCallback
|
|
305
|
+
// et al): count it in the auto-pump keep-alive so the Node process stays alive
|
|
306
|
+
// until the completion dispatches (no-op off-Node / on non-owner envs).
|
|
307
|
+
if (scope == GI_SCOPE_TYPE_ASYNC) cb->pumpCounted = NodeGiPumpAsyncBegin(env);
|
|
308
|
+
napi_create_reference(env, fn, 1, &cb->jsFn);
|
|
309
|
+
cb->closure = gi_callable_info_create_closure(callbackInfo, &cb->cif, NodeGiCallbackTrampoline, cb);
|
|
310
|
+
cb->native = gi_callable_info_get_closure_native_address(callbackInfo, cb->closure);
|
|
311
|
+
if (cb->native == nullptr) cb->native = cb->closure;
|
|
312
|
+
return cb;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Whether `type` is a supported OUT/INOUT marshalling type for this milestone:
|
|
316
|
+
// fundamentals (numbers/bool), strings (utf8/filename), GObject/interface +
|
|
317
|
+
// enums/flags, struct/boxed/union (wrapped as boxed handles on return), and the
|
|
318
|
+
// containers (arrays, GList/GSList/GHashTable). GError (its own roadmap PR) is
|
|
319
|
+
// deferred: a clear error rather than silent mis-handling. *why receives a short
|
|
320
|
+
// type label on refusal. Declared in common.h — shared with the vfunc chain-up
|
|
321
|
+
// path (class.cc CallParentVfunc), which routes each OUT/INOUT vfunc arg the same
|
|
322
|
+
// way this function-invoke path does.
|
|
323
|
+
bool IsSupportedOutType(GITypeInfo* type, std::string* why) {
|
|
324
|
+
switch (gi_type_info_get_tag(type)) {
|
|
325
|
+
case GI_TYPE_TAG_BOOLEAN:
|
|
326
|
+
case GI_TYPE_TAG_INT8:
|
|
327
|
+
case GI_TYPE_TAG_UINT8:
|
|
328
|
+
case GI_TYPE_TAG_INT16:
|
|
329
|
+
case GI_TYPE_TAG_UINT16:
|
|
330
|
+
case GI_TYPE_TAG_INT32:
|
|
331
|
+
case GI_TYPE_TAG_UINT32:
|
|
332
|
+
case GI_TYPE_TAG_INT64:
|
|
333
|
+
case GI_TYPE_TAG_UINT64:
|
|
334
|
+
case GI_TYPE_TAG_FLOAT:
|
|
335
|
+
case GI_TYPE_TAG_DOUBLE:
|
|
336
|
+
case GI_TYPE_TAG_UTF8:
|
|
337
|
+
case GI_TYPE_TAG_FILENAME:
|
|
338
|
+
return true;
|
|
339
|
+
case GI_TYPE_TAG_INTERFACE: {
|
|
340
|
+
// struct/union OUT are read back via WrapBoxed (a boxed handle); enum/flags
|
|
341
|
+
// as numbers; object/interface as GObject wrappers. Field ACCESS on the
|
|
342
|
+
// returned struct is a later PR, but the OUT marshalling itself is here.
|
|
343
|
+
GIBaseInfo* iface = gi_type_info_get_interface(type);
|
|
344
|
+
bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface) ||
|
|
345
|
+
GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface) ||
|
|
346
|
+
GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface));
|
|
347
|
+
if (!ok && why != nullptr) *why = "interface";
|
|
348
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
349
|
+
return ok;
|
|
350
|
+
}
|
|
351
|
+
case GI_TYPE_TAG_ARRAY:
|
|
352
|
+
case GI_TYPE_TAG_GLIST:
|
|
353
|
+
case GI_TYPE_TAG_GSLIST:
|
|
354
|
+
case GI_TYPE_TAG_GHASH:
|
|
355
|
+
// Container OUT/return — arrays (incl. GStrv / byte arrays), GList/GSList,
|
|
356
|
+
// and string-keyed GHashTable. Element-type support is checked here so an
|
|
357
|
+
// exotic combo defers cleanly before the invoke.
|
|
358
|
+
return IsSupportedContainerType(type, why);
|
|
359
|
+
default:
|
|
360
|
+
if (why != nullptr)
|
|
361
|
+
*why = "type tag " + std::to_string(static_cast<int>(gi_type_info_get_tag(type)));
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---- GError -> GLib.Error ----
|
|
367
|
+
//
|
|
368
|
+
// A failed GI invoke yields a GError (domain quark + code + message). GJS surfaces
|
|
369
|
+
// it as a real `GLib.Error` (an Error subclass carrying `.domain`/`.code`/
|
|
370
|
+
// `.message` + a `.matches(domain, code)` method). The GLib.Error CLASS itself is
|
|
371
|
+
// owned by L1 (gi.js) so its `matches()` can resolve an error-enum object to its
|
|
372
|
+
// domain; the engine just reports the GError's fields to an L1-registered builder
|
|
373
|
+
// function (stored in this env's instance data — see NodeGiEnvData) and throws
|
|
374
|
+
// what it returns.
|
|
375
|
+
//
|
|
376
|
+
// Read a GError's domain (quark name + numeric quark), code and message, FREE it,
|
|
377
|
+
// and throw the JS error the L1 builder produces (instanceof GLib.Error). Falls
|
|
378
|
+
// back to a plain JS Error when no builder is registered for this env. `context`
|
|
379
|
+
// is the "Ns.method" display name, used only in the fallback message (a real
|
|
380
|
+
// GLib.Error carries the bare GError message, matching GJS).
|
|
381
|
+
void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
|
|
382
|
+
// Read everything out of the GError BEFORE g_error_free.
|
|
383
|
+
GQuark domainQuark = error != nullptr ? error->domain : 0;
|
|
384
|
+
const char* domainName = domainQuark != 0 ? g_quark_to_string(domainQuark) : nullptr;
|
|
385
|
+
std::string domain = domainName != nullptr ? domainName : "";
|
|
386
|
+
int code = error != nullptr ? error->code : 0;
|
|
387
|
+
std::string message =
|
|
388
|
+
(error != nullptr && error->message != nullptr) ? error->message : "invocation failed";
|
|
389
|
+
if (error != nullptr) g_error_free(error);
|
|
390
|
+
|
|
391
|
+
// Prefer this env's L1 GLib.Error builder (instanceof GLib.Error + matches()).
|
|
392
|
+
NodeGiEnvData* d = EnvData(env);
|
|
393
|
+
if (d != nullptr && d->errorBuilder != nullptr) {
|
|
394
|
+
napi_value builder = nullptr;
|
|
395
|
+
if (napi_get_reference_value(env, d->errorBuilder, &builder) == napi_ok &&
|
|
396
|
+
builder != nullptr) {
|
|
397
|
+
napi_value undef = nullptr;
|
|
398
|
+
napi_get_undefined(env, &undef);
|
|
399
|
+
napi_value bargs[4] = {nullptr, nullptr, nullptr, nullptr};
|
|
400
|
+
napi_create_string_utf8(env, domain.c_str(), NAPI_AUTO_LENGTH, &bargs[0]);
|
|
401
|
+
napi_create_uint32(env, static_cast<uint32_t>(domainQuark), &bargs[1]);
|
|
402
|
+
napi_create_int32(env, code, &bargs[2]);
|
|
403
|
+
napi_create_string_utf8(env, message.c_str(), NAPI_AUTO_LENGTH, &bargs[3]);
|
|
404
|
+
napi_value errObj = nullptr;
|
|
405
|
+
napi_status st = napi_call_function(env, undef, builder, 4, bargs, &errObj);
|
|
406
|
+
if (st == napi_ok && errObj != nullptr) {
|
|
407
|
+
napi_throw(env, errObj);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// The builder itself threw — let that exception surface rather than masking it.
|
|
411
|
+
bool pending = false;
|
|
412
|
+
if (napi_is_exception_pending(env, &pending) == napi_ok && pending) return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
Napi::Error::New(env, "Calling " + context + ": " + message).ThrowAsJavaScriptException();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpointer instance,
|
|
419
|
+
Napi::Array args, const std::string& displayName) {
|
|
420
|
+
GICallableInfo* callable = reinterpret_cast<GICallableInfo*>(func);
|
|
421
|
+
unsigned int n_args = gi_callable_info_get_n_args(callable);
|
|
422
|
+
bool isMethod = instance != nullptr;
|
|
423
|
+
size_t offset = isMethod ? 1 : 0;
|
|
424
|
+
|
|
425
|
+
// Per-argument dense in/out positions, mirroring gi_callable_info_invoke's own
|
|
426
|
+
// walk: the instance takes in-position 0; every IN/INOUT arg takes the next
|
|
427
|
+
// in-position; every OUT/INOUT arg takes the next out-position. The in_args and
|
|
428
|
+
// out_args arrays the invoker reads are dense (no holes), so we cannot index by
|
|
429
|
+
// raw arg index once OUT args interleave — these maps bridge that gap. For an
|
|
430
|
+
// all-IN callable inPos[i] == offset + i, so the IN-only path is unchanged.
|
|
431
|
+
std::vector<int> inPos(n_args, -1);
|
|
432
|
+
std::vector<int> outPos(n_args, -1);
|
|
433
|
+
std::vector<GIDirection> dirs(n_args);
|
|
434
|
+
size_t nIn = offset;
|
|
435
|
+
size_t nOut = 0;
|
|
436
|
+
for (unsigned int i = 0; i < n_args; i++) {
|
|
437
|
+
GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
|
|
438
|
+
GIDirection d = gi_arg_info_get_direction(ai);
|
|
439
|
+
gi_base_info_unref(ai);
|
|
440
|
+
dirs[i] = d;
|
|
441
|
+
if (d == GI_DIRECTION_IN || d == GI_DIRECTION_INOUT) inPos[i] = static_cast<int>(nIn++);
|
|
442
|
+
if (d == GI_DIRECTION_OUT || d == GI_DIRECTION_INOUT) outPos[i] = static_cast<int>(nOut++);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
std::vector<GIArgument> in_args(nIn);
|
|
446
|
+
std::vector<GIArgument> out_args(nOut);
|
|
447
|
+
// Stable per-arg storage the invoker writes OUT/INOUT values into (its address
|
|
448
|
+
// is handed to the C callee). Sized once to n_args so the addresses never move.
|
|
449
|
+
std::vector<GIArgument> slots(n_args);
|
|
450
|
+
std::vector<std::string> held(n_args);
|
|
451
|
+
bool instanceRefTaken = false; // the transfer-full-instance ref below, undone on early bail
|
|
452
|
+
if (isMethod) {
|
|
453
|
+
in_args[0].v_pointer = instance;
|
|
454
|
+
// A transfer-full INSTANCE parameter (rare but load-bearing:
|
|
455
|
+
// g_dbus_method_invocation_return_value/_return_error/… — "this method will
|
|
456
|
+
// take ownership of @invocation") CONSUMES one ref of the instance. Our JS
|
|
457
|
+
// wrapper keeps its own ref for the handle's lifetime (its finalizer unrefs),
|
|
458
|
+
// so hand the callee a ref of its own — exactly what gjs's arg-cache does for
|
|
459
|
+
// the instance argument — otherwise the wrapper's later unref double-frees.
|
|
460
|
+
// Only a GObject instance can be safely ref'd here; the transfer-full-instance
|
|
461
|
+
// methods in the base typelibs are all GObject methods (a boxed instance
|
|
462
|
+
// method reaching this path keeps the previous behaviour).
|
|
463
|
+
if (gi_callable_info_get_instance_ownership_transfer(callable) == GI_TRANSFER_EVERYTHING) {
|
|
464
|
+
// gi_base_info_get_container returns a borrowed ref — no unref.
|
|
465
|
+
GIBaseInfo* container = gi_base_info_get_container(reinterpret_cast<GIBaseInfo*>(func));
|
|
466
|
+
if (container != nullptr && GI_IS_OBJECT_INFO(container) &&
|
|
467
|
+
G_IS_OBJECT(static_cast<GObject*>(instance))) {
|
|
468
|
+
g_object_ref(static_cast<GObject*>(instance));
|
|
469
|
+
instanceRefTaken = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Pre-scan: the user_data + destroy-notify slots associated with a callback
|
|
475
|
+
// arg are filled from the callback, not consumed from the JS argument list.
|
|
476
|
+
std::vector<bool> skip(n_args, false);
|
|
477
|
+
for (unsigned int i = 0; i < n_args; i++) {
|
|
478
|
+
GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
|
|
479
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
480
|
+
if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE) {
|
|
481
|
+
GIBaseInfo* iface = gi_type_info_get_interface(ti);
|
|
482
|
+
if (iface != nullptr && GI_IS_CALLBACK_INFO(iface)) {
|
|
483
|
+
int ci = ArgClosureIndex(ai);
|
|
484
|
+
int di = ArgDestroyIndex(ai);
|
|
485
|
+
if (ci >= 0 && static_cast<unsigned int>(ci) < n_args) skip[ci] = true;
|
|
486
|
+
if (di >= 0 && static_cast<unsigned int>(di) < n_args) skip[di] = true;
|
|
487
|
+
}
|
|
488
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
489
|
+
}
|
|
490
|
+
gi_base_info_unref(ti);
|
|
491
|
+
gi_base_info_unref(ai);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Pre-scan: array-length args. An array IN/OUT arg — or the return value — may
|
|
495
|
+
// carry a separate introspectable length arg. Mark it skip (not JS-consumed)
|
|
496
|
+
// and flag it so its OUT slot is wired below / its IN value is autofilled from
|
|
497
|
+
// the array's JS length when the array arg is marshalled. This extends the same
|
|
498
|
+
// skip mechanism the callback closure/destroy slots already use.
|
|
499
|
+
std::vector<bool> isLenArg(n_args, false);
|
|
500
|
+
for (unsigned int i = 0; i < n_args; i++) {
|
|
501
|
+
GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
|
|
502
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
503
|
+
if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY) {
|
|
504
|
+
unsigned int L = 0;
|
|
505
|
+
if (gi_type_info_get_array_length_index(ti, &L) && L < n_args) {
|
|
506
|
+
skip[L] = true;
|
|
507
|
+
isLenArg[L] = true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
gi_base_info_unref(ti);
|
|
511
|
+
gi_base_info_unref(ai);
|
|
512
|
+
}
|
|
513
|
+
{
|
|
514
|
+
GITypeInfo* rt = gi_callable_info_get_return_type(callable);
|
|
515
|
+
if (gi_type_info_get_tag(rt) == GI_TYPE_TAG_ARRAY) {
|
|
516
|
+
unsigned int L = 0;
|
|
517
|
+
if (gi_type_info_get_array_length_index(rt, &L) && L < n_args) {
|
|
518
|
+
skip[L] = true;
|
|
519
|
+
isLenArg[L] = true;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
gi_base_info_unref(rt);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Caller-allocates OUT storage (fixed C array or boxed struct): blob != nullptr
|
|
526
|
+
// marks the arg; boxedGType != 0 selects the boxed copy/free path on read-back.
|
|
527
|
+
std::vector<gpointer> callerAllocBlob(n_args, nullptr);
|
|
528
|
+
std::vector<GType> callerAllocGType(n_args, 0);
|
|
529
|
+
|
|
530
|
+
std::vector<InContainer> inContainers; // IN containers to free after the invoke
|
|
531
|
+
// g_strdup'd transfer-full scalar string IN/INOUT args. The callee adopts + frees
|
|
532
|
+
// them only on a SUCCESSFUL invoke; if a later arg fails to marshal (the early
|
|
533
|
+
// !ok return) or the invoke itself fails, the callee never took them → we g_free
|
|
534
|
+
// them on those branches so they don't leak (#658).
|
|
535
|
+
std::vector<gpointer> ownedInStrings;
|
|
536
|
+
std::vector<NodeGiCallback*> created; // all callbacks made this call
|
|
537
|
+
std::vector<NodeGiCallback*> callScope; // scope=call → freed after the invoke
|
|
538
|
+
// JS functions marshalled as GClosure IN-args this call (see common.h). Each
|
|
539
|
+
// carries one ref taken at marshal time (gjs's ref+sink): transfer-none refs are
|
|
540
|
+
// dropped after the invoke, transfer-full refs belong to the callee (dropped
|
|
541
|
+
// only when the callee never ran).
|
|
542
|
+
CreatedClosures createdClosures;
|
|
543
|
+
// JS typed arrays marshalled as fresh GBytes IN-args this call (see common.h).
|
|
544
|
+
// Same release contract as the closures: transfer-none refs are dropped after
|
|
545
|
+
// the invoke (the callee ref'd the GBytes if it kept it), transfer-full refs
|
|
546
|
+
// belong to the callee (dropped only when the callee never ran).
|
|
547
|
+
CreatedBytes createdBytes;
|
|
548
|
+
bool ok = true;
|
|
549
|
+
size_t jsCursor = 0;
|
|
550
|
+
for (unsigned int i = 0; i < n_args && ok; i++) {
|
|
551
|
+
if (skip[i]) {
|
|
552
|
+
// A length arg that is OUT/INOUT: the callee writes the array length into
|
|
553
|
+
// our stable slot, which we read back to size the array. (IN length args
|
|
554
|
+
// are autofilled when their array arg is marshalled, below.) Callback
|
|
555
|
+
// closure/destroy slots (always IN) are filled by the callback handler.
|
|
556
|
+
if (isLenArg[i] && (dirs[i] == GI_DIRECTION_OUT || dirs[i] == GI_DIRECTION_INOUT))
|
|
557
|
+
out_args[outPos[i]].v_pointer = &slots[i];
|
|
558
|
+
// An INOUT length arg is a single `gint*` the callee reads (the in-count) AND
|
|
559
|
+
// writes (the out-count): the in_arg is a POINTER to the same stable slot. The
|
|
560
|
+
// in-count is written into slots[i] when the INOUT array is marshalled below.
|
|
561
|
+
if (isLenArg[i] && dirs[i] == GI_DIRECTION_INOUT)
|
|
562
|
+
in_args[inPos[i]].v_pointer = &slots[i];
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
|
|
566
|
+
GIDirection dir = dirs[i];
|
|
567
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
568
|
+
|
|
569
|
+
GIBaseInfo* iface = gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE
|
|
570
|
+
? gi_type_info_get_interface(ti)
|
|
571
|
+
: nullptr;
|
|
572
|
+
bool isCallback = iface != nullptr && GI_IS_CALLBACK_INFO(iface);
|
|
573
|
+
|
|
574
|
+
// OUT / INOUT (a callback is always IN-direction and handled below).
|
|
575
|
+
if ((dir == GI_DIRECTION_OUT || dir == GI_DIRECTION_INOUT) && !isCallback) {
|
|
576
|
+
std::string why;
|
|
577
|
+
if (gi_arg_info_is_caller_allocates(ai)) {
|
|
578
|
+
// Caller-allocates: the callee fills storage WE provide (a single
|
|
579
|
+
// pointer, not a **). gi_callable_info_invoke passes out_args[k].v_pointer
|
|
580
|
+
// straight through as the arg, so the blob pointer goes there directly.
|
|
581
|
+
// Supported: a fixed-size C array (size = fixed_size × element_size), a
|
|
582
|
+
// boxed struct/union (size = gi_struct/union_info_get_size, incl. the
|
|
583
|
+
// GValue auto-unbox), and a PLAIN non-boxed struct/union (same size
|
|
584
|
+
// source; the filled blob is handed to JS as a g_free-owning
|
|
585
|
+
// field-readable handle — PangoLayout.get_pixel_extents). Reference:
|
|
586
|
+
// refs/gjs/gi/arg-cache.cpp build_arg CALLER_ALLOCATES branch.
|
|
587
|
+
GITypeTag tg = gi_type_info_get_tag(ti);
|
|
588
|
+
size_t size = 0;
|
|
589
|
+
GType boxedGType = 0;
|
|
590
|
+
if (tg == GI_TYPE_TAG_ARRAY && gi_type_info_get_array_type(ti) == GI_ARRAY_TYPE_C) {
|
|
591
|
+
size_t fixed = 0;
|
|
592
|
+
if (gi_type_info_get_array_fixed_size(ti, &fixed) && fixed > 0) {
|
|
593
|
+
GITypeInfo* el = gi_type_info_get_param_type(ti, 0);
|
|
594
|
+
// Only fundamental (non-interface) element types: CElementSize is the
|
|
595
|
+
// true storage size for them. A struct-by-value element array would
|
|
596
|
+
// need gi_struct_info_get_size per element + field-access read-back
|
|
597
|
+
// (a later PR), so leave size=0 to defer it cleanly.
|
|
598
|
+
if (el != nullptr && gi_type_info_get_tag(el) != GI_TYPE_TAG_INTERFACE)
|
|
599
|
+
size = CElementSize(el) * fixed;
|
|
600
|
+
if (el != nullptr) gi_base_info_unref(el);
|
|
601
|
+
}
|
|
602
|
+
} else if (tg == GI_TYPE_TAG_INTERFACE) {
|
|
603
|
+
GIBaseInfo* si = gi_type_info_get_interface(ti);
|
|
604
|
+
if (si != nullptr && GI_IS_STRUCT_INFO(si)) {
|
|
605
|
+
size = gi_struct_info_get_size(reinterpret_cast<GIStructInfo*>(si));
|
|
606
|
+
} else if (si != nullptr && GI_IS_UNION_INFO(si)) {
|
|
607
|
+
size = gi_union_info_get_size(reinterpret_cast<GIUnionInfo*>(si));
|
|
608
|
+
}
|
|
609
|
+
if (si != nullptr) {
|
|
610
|
+
GType gt = gi_registered_type_info_get_g_type(
|
|
611
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(si));
|
|
612
|
+
if (gt != G_TYPE_INVALID && G_TYPE_IS_BOXED(gt)) boxedGType = gt;
|
|
613
|
+
// A FOREIGN struct's layout is module-owned (cairo) — GI cannot fill
|
|
614
|
+
// it here; defer cleanly (its introspected size is meaningless).
|
|
615
|
+
if (GI_IS_STRUCT_INFO(si) &&
|
|
616
|
+
gi_struct_info_is_foreign(reinterpret_cast<GIStructInfo*>(si)))
|
|
617
|
+
size = 0;
|
|
618
|
+
gi_base_info_unref(si);
|
|
619
|
+
}
|
|
620
|
+
// A NON-boxed plain struct (boxedGType stays 0, e.g. PangoRectangle in
|
|
621
|
+
// PangoLayout.get_pixel_extents) keeps its size: the blob is handed to
|
|
622
|
+
// JS as a g_free-owning handle with field access on read-back below —
|
|
623
|
+
// gjs's CallerAllocatesOut (g_malloc0 → fill → wrap → g_free). An
|
|
624
|
+
// opaque struct introspects size 0 and defers via the throw below.
|
|
625
|
+
}
|
|
626
|
+
if (size == 0) {
|
|
627
|
+
Napi::TypeError::New(
|
|
628
|
+
env, displayName + ": caller-allocates OUT parameter type is not yet supported")
|
|
629
|
+
.ThrowAsJavaScriptException();
|
|
630
|
+
ok = false;
|
|
631
|
+
} else {
|
|
632
|
+
gpointer blob = g_malloc0(size);
|
|
633
|
+
callerAllocBlob[i] = blob;
|
|
634
|
+
callerAllocGType[i] = boxedGType;
|
|
635
|
+
out_args[outPos[i]].v_pointer = blob;
|
|
636
|
+
}
|
|
637
|
+
} else if (!IsSupportedOutType(ti, &why)) {
|
|
638
|
+
Napi::TypeError::New(env, displayName + ": OUT " + why +
|
|
639
|
+
" parameters are not yet supported")
|
|
640
|
+
.ThrowAsJavaScriptException();
|
|
641
|
+
ok = false;
|
|
642
|
+
} else if (dir == GI_DIRECTION_INOUT) {
|
|
643
|
+
GITypeTag tg = gi_type_info_get_tag(ti);
|
|
644
|
+
if (tg == GI_TYPE_TAG_ARRAY || tg == GI_TYPE_TAG_GLIST || tg == GI_TYPE_TAG_GSLIST ||
|
|
645
|
+
tg == GI_TYPE_TAG_GHASH) {
|
|
646
|
+
// INOUT container (read-modify-write a caller-built container). The ABI is
|
|
647
|
+
// a single `container**` the callee reads (the in-container) and reassigns
|
|
648
|
+
// (the out-container). We stash the in-container pointer in the stable slot
|
|
649
|
+
// and point BOTH in_args + out_args at &slots[i]; the callee overwrites
|
|
650
|
+
// slots[i].v_pointer with the out-container, which the read-back loop below
|
|
651
|
+
// marshals via ReadOutOrReturn (OUT transfer). OWNERSHIP: JsToInContainer
|
|
652
|
+
// records the ORIGINAL in-container in `inContainers`, so FreeInContainer
|
|
653
|
+
// (after the reads) releases it per the IN transfer — NONE → we free the
|
|
654
|
+
// in-container we built; EVERYTHING/CONTAINER → the callee adopted it, we
|
|
655
|
+
// don't. The out-container is freed (or borrowed) by ReadOutOrReturn per the
|
|
656
|
+
// SAME (OUT) transfer. Verified vs gjs 1.88 (GIMarshallingTests.array_inout,
|
|
657
|
+
// garray/glist/gslist/gptrarray/ghashtable_utf8_none_inout, bytearray_full_inout).
|
|
658
|
+
Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
|
|
659
|
+
jsCursor++;
|
|
660
|
+
if (!IsSupportedContainerType(ti, &why)) {
|
|
661
|
+
Napi::TypeError::New(env, displayName + ": INOUT " + why +
|
|
662
|
+
" parameters are not yet supported")
|
|
663
|
+
.ThrowAsJavaScriptException();
|
|
664
|
+
ok = false;
|
|
665
|
+
} else {
|
|
666
|
+
GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
|
|
667
|
+
gpointer cptr = nullptr;
|
|
668
|
+
long ccount = 0;
|
|
669
|
+
// A nullable INOUT container passed null/undefined → a NULL in-container
|
|
670
|
+
// (count 0), matching gjs (e.g. length_array_utf8_optional_inout(null)
|
|
671
|
+
// → []). A non-nullable arg falls through to JsToInContainer, which
|
|
672
|
+
// throws for a non-array, exactly as before.
|
|
673
|
+
if ((v.IsNull() || v.IsUndefined()) && gi_arg_info_may_be_null(ai)) {
|
|
674
|
+
ok = true;
|
|
675
|
+
} else {
|
|
676
|
+
ok = JsToInContainer(env, v, ti, tr, &cptr, &ccount, &inContainers);
|
|
677
|
+
}
|
|
678
|
+
if (ok) {
|
|
679
|
+
slots[i].v_pointer = cptr;
|
|
680
|
+
in_args[inPos[i]].v_pointer = &slots[i];
|
|
681
|
+
out_args[outPos[i]].v_pointer = &slots[i];
|
|
682
|
+
// The length arg carries the in-count. An INOUT length gets it in ITS
|
|
683
|
+
// slot (the callee reads+writes that slot); a plain IN length gets the
|
|
684
|
+
// value directly in its in_arg.
|
|
685
|
+
if (tg == GI_TYPE_TAG_ARRAY) {
|
|
686
|
+
unsigned int L = 0;
|
|
687
|
+
if (gi_type_info_get_array_length_index(ti, &L) && L < n_args) {
|
|
688
|
+
GIArgInfo* la = gi_callable_info_get_arg(callable, L);
|
|
689
|
+
GITypeInfo* lt = gi_arg_info_get_type_info(la);
|
|
690
|
+
if (dirs[L] == GI_DIRECTION_INOUT) {
|
|
691
|
+
WriteLengthValue(lt, &slots[L], ccount);
|
|
692
|
+
} else if (dirs[L] == GI_DIRECTION_IN && inPos[L] >= 0) {
|
|
693
|
+
WriteLengthValue(lt, &in_args[inPos[L]], ccount);
|
|
694
|
+
}
|
|
695
|
+
gi_base_info_unref(lt);
|
|
696
|
+
gi_base_info_unref(la);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
// INOUT scalar: marshal the JS input into the slot (like IN); the
|
|
703
|
+
// invoker hands the slot's address to the callee, which reads + writes.
|
|
704
|
+
Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
|
|
705
|
+
jsCursor++;
|
|
706
|
+
GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
|
|
707
|
+
if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings)) {
|
|
708
|
+
in_args[inPos[i]].v_pointer = &slots[i];
|
|
709
|
+
out_args[outPos[i]].v_pointer = &slots[i];
|
|
710
|
+
} else {
|
|
711
|
+
ok = false; // JsToGIArgument already threw
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
// Pure OUT: the callee writes into the slot; no JS arg is consumed.
|
|
716
|
+
out_args[outPos[i]].v_pointer = &slots[i];
|
|
717
|
+
}
|
|
718
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
719
|
+
gi_base_info_unref(ti);
|
|
720
|
+
gi_base_info_unref(ai);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// IN (including callbacks): consume the next JS argument.
|
|
725
|
+
Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
|
|
726
|
+
jsCursor++;
|
|
727
|
+
|
|
728
|
+
if (isCallback) {
|
|
729
|
+
int ci = ArgClosureIndex(ai);
|
|
730
|
+
int di = ArgDestroyIndex(ai);
|
|
731
|
+
if (v.IsFunction()) {
|
|
732
|
+
GIScopeType scopeType = gi_arg_info_get_scope(ai);
|
|
733
|
+
NodeGiCallback* cb =
|
|
734
|
+
CreateCallback(env, v.As<Napi::Function>(), reinterpret_cast<GICallableInfo*>(iface),
|
|
735
|
+
scopeType);
|
|
736
|
+
created.push_back(cb);
|
|
737
|
+
if (scopeType == GI_SCOPE_TYPE_CALL) callScope.push_back(cb);
|
|
738
|
+
in_args[inPos[i]].v_pointer = cb->native;
|
|
739
|
+
if (ci >= 0 && static_cast<unsigned int>(ci) < n_args && inPos[ci] >= 0)
|
|
740
|
+
in_args[inPos[ci]].v_pointer = cb;
|
|
741
|
+
if (di >= 0 && static_cast<unsigned int>(di) < n_args && inPos[di] >= 0)
|
|
742
|
+
in_args[inPos[di]].v_pointer = reinterpret_cast<gpointer>(NodeGiCallbackDestroyNotify);
|
|
743
|
+
} else if (v.IsNull() || v.IsUndefined()) {
|
|
744
|
+
// Null is only valid for a nullable callback (e.g. an optional progress
|
|
745
|
+
// callback). For a NON-nullable callback (g_timeout_add / g_idle_add)
|
|
746
|
+
// passing null would make us hand GLib a NULL GSourceFunc → a GLib-CRITICAL
|
|
747
|
+
// (`assertion 'function != NULL'`) + a dead source. Reject it with a clean
|
|
748
|
+
// JS TypeError up front, matching GJS.
|
|
749
|
+
if (!gi_arg_info_may_be_null(ai)) {
|
|
750
|
+
gi_base_info_unref(iface);
|
|
751
|
+
gi_base_info_unref(ti);
|
|
752
|
+
gi_base_info_unref(ai);
|
|
753
|
+
Napi::TypeError::New(
|
|
754
|
+
env, displayName + ": the callback argument is not nullable (expected a function)")
|
|
755
|
+
.ThrowAsJavaScriptException();
|
|
756
|
+
ok = false;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
in_args[inPos[i]].v_pointer = nullptr;
|
|
760
|
+
} else {
|
|
761
|
+
gi_base_info_unref(iface);
|
|
762
|
+
gi_base_info_unref(ti);
|
|
763
|
+
gi_base_info_unref(ai);
|
|
764
|
+
Napi::TypeError::New(env, displayName + ": expected a function for the callback argument")
|
|
765
|
+
.ThrowAsJavaScriptException();
|
|
766
|
+
ok = false;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
GITypeTag tg = gi_type_info_get_tag(ti);
|
|
771
|
+
if (tg == GI_TYPE_TAG_ARRAY || tg == GI_TYPE_TAG_GLIST || tg == GI_TYPE_TAG_GSLIST ||
|
|
772
|
+
tg == GI_TYPE_TAG_GHASH) {
|
|
773
|
+
std::string why;
|
|
774
|
+
if (!IsSupportedContainerType(ti, &why)) {
|
|
775
|
+
Napi::TypeError::New(env, displayName + ": IN " + why +
|
|
776
|
+
" parameters are not yet supported")
|
|
777
|
+
.ThrowAsJavaScriptException();
|
|
778
|
+
ok = false;
|
|
779
|
+
} else {
|
|
780
|
+
GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
|
|
781
|
+
gpointer cptr = nullptr;
|
|
782
|
+
long ccount = 0;
|
|
783
|
+
ok = JsToInContainer(env, v, ti, tr, &cptr, &ccount, &inContainers);
|
|
784
|
+
if (ok) {
|
|
785
|
+
in_args[inPos[i]].v_pointer = cptr;
|
|
786
|
+
// Autofill an IN length arg from the array's element count.
|
|
787
|
+
if (tg == GI_TYPE_TAG_ARRAY) {
|
|
788
|
+
unsigned int L = 0;
|
|
789
|
+
if (gi_type_info_get_array_length_index(ti, &L) && L < n_args &&
|
|
790
|
+
dirs[L] == GI_DIRECTION_IN && inPos[L] >= 0) {
|
|
791
|
+
GIArgInfo* la = gi_callable_info_get_arg(callable, L);
|
|
792
|
+
GITypeInfo* lt = gi_arg_info_get_type_info(la);
|
|
793
|
+
WriteLengthValue(lt, &in_args[inPos[L]], ccount);
|
|
794
|
+
gi_base_info_unref(lt);
|
|
795
|
+
gi_base_info_unref(la);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
|
|
802
|
+
ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
|
|
803
|
+
&createdClosures, &createdBytes);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
808
|
+
gi_base_info_unref(ti);
|
|
809
|
+
gi_base_info_unref(ai);
|
|
810
|
+
}
|
|
811
|
+
if (!ok) {
|
|
812
|
+
for (NodeGiCallback* cb : created) NodeGiCallbackFree(cb);
|
|
813
|
+
for (const InContainer& c : inContainers) FreeInContainer(c);
|
|
814
|
+
// The callee never ran, so NO closure ref was adopted — release every created
|
|
815
|
+
// closure (refcount 1 → 0 → finalize drops the JS-function ref).
|
|
816
|
+
for (GClosure* c : createdClosures.transferNone) g_closure_unref(c);
|
|
817
|
+
for (GClosure* c : createdClosures.transferFull) g_closure_unref(c);
|
|
818
|
+
// Likewise every fresh GBytes built from a JS typed array — never adopted.
|
|
819
|
+
for (GBytes* b : createdBytes.transferNone) g_bytes_unref(b);
|
|
820
|
+
for (GBytes* b : createdBytes.transferFull) g_bytes_unref(b);
|
|
821
|
+
// Likewise the transfer-full-instance ref: the callee never consumed it.
|
|
822
|
+
if (instanceRefTaken) g_object_unref(static_cast<GObject*>(instance));
|
|
823
|
+
for (gpointer s : ownedInStrings) g_free(s); // never reached the callee (#658)
|
|
824
|
+
// Caller-allocated blobs never reached the callee (or it was never called) —
|
|
825
|
+
// they are zeroed g_malloc0 storage, so a plain g_free is correct here.
|
|
826
|
+
for (gpointer b : callerAllocBlob)
|
|
827
|
+
if (b != nullptr) g_free(b);
|
|
828
|
+
return env.Null();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
GIArgument retval;
|
|
832
|
+
GError* error = nullptr;
|
|
833
|
+
gboolean success = gi_function_info_invoke(func, in_args.data(), in_args.size(),
|
|
834
|
+
out_args.data(), out_args.size(), &retval, &error);
|
|
835
|
+
// A FAILED invoke never started its async op, so a scope=async callback it was
|
|
836
|
+
// handed will not fire — drop its auto-pump keep-alive ref here or the process
|
|
837
|
+
// could never exit. (The callback allocation itself is left alone: freeing it
|
|
838
|
+
// would be unsafe if a callee retained it despite the error — the pre-existing
|
|
839
|
+
// small leak on this rare path is unchanged.) Runs BEFORE the callScope free
|
|
840
|
+
// below so no freed pointer is revisited; releasing a CALL-scope cb is a no-op.
|
|
841
|
+
if (!success) {
|
|
842
|
+
for (NodeGiCallback* cb : created) NodeGiCallbackPumpRelease(cb);
|
|
843
|
+
}
|
|
844
|
+
// Call-scope closures are only valid for the duration of the invoke; free them
|
|
845
|
+
// now (notified/async closures are owned by the callee / self-freeing).
|
|
846
|
+
for (NodeGiCallback* cb : callScope) NodeGiCallbackFree(cb);
|
|
847
|
+
// Transfer-none GClosure IN-args: drop the ref taken at marshal time (gjs
|
|
848
|
+
// BoxedInTransferNone::release, which runs on success AND error paths). A
|
|
849
|
+
// callee that stored the closure (signal connect, source_set_closure, a DBus
|
|
850
|
+
// registration) holds its own ref+sink, so the closure lives on; a callee that
|
|
851
|
+
// only invoked it synchronously lets this unref finalize it (dropping the JS
|
|
852
|
+
// ref). Transfer-full closures were adopted by the callee — no release (gjs
|
|
853
|
+
// GClosureIn::release skips).
|
|
854
|
+
for (GClosure* c : createdClosures.transferNone) g_closure_unref(c);
|
|
855
|
+
// Transfer-none GBytes IN-args built from JS typed arrays: drop the fresh ref
|
|
856
|
+
// taken at marshal time (gjs GBytesInTransferNone::release, success AND error
|
|
857
|
+
// paths). A callee that kept the bytes (GdkPixbuf.Pixbuf.new_from_bytes) holds
|
|
858
|
+
// its own ref. Transfer-full GBytes were adopted by the callee — no release.
|
|
859
|
+
for (GBytes* b : createdBytes.transferNone) g_bytes_unref(b);
|
|
860
|
+
if (!success) {
|
|
861
|
+
for (const InContainer& c : inContainers) FreeInContainer(c);
|
|
862
|
+
// A failed invoke did not adopt the transfer-full IN/INOUT strings we g_strdup'd
|
|
863
|
+
// for it → free them here so the error path doesn't leak (#658).
|
|
864
|
+
for (gpointer s : ownedInStrings) g_free(s);
|
|
865
|
+
// Free the caller-allocated OUT storage (the callee may have partially filled
|
|
866
|
+
// it; g_free releases the block — a boxed free-func on a half-initialised blob
|
|
867
|
+
// is riskier than the small leak of any internal pointer it set).
|
|
868
|
+
for (gpointer b : callerAllocBlob)
|
|
869
|
+
if (b != nullptr) g_free(b);
|
|
870
|
+
// ThrowGError reads the GError's domain/code/message, frees it, and throws a
|
|
871
|
+
// real GLib.Error (instanceof, with .matches()) via the L1 builder.
|
|
872
|
+
ThrowGError(env, error, displayName);
|
|
873
|
+
return env.Null();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Assemble the JS return per the GJS convention: the function's own return
|
|
877
|
+
// value (if non-void) followed by each OUT/INOUT value in argument order.
|
|
878
|
+
// Exactly one element → return it bare; many → a JS Array; none → undefined.
|
|
879
|
+
std::vector<Napi::Value> results;
|
|
880
|
+
GITypeInfo* return_type = gi_callable_info_get_return_type(callable);
|
|
881
|
+
GITransfer return_transfer = gi_callable_info_get_caller_owns(callable);
|
|
882
|
+
// A non-void return ALWAYS leads the tuple — the `(skip)` annotation is IGNORED
|
|
883
|
+
// for the JS return, exactly as GJS does. GJS's arg-cache derives `m_has_return`
|
|
884
|
+
// purely from the return type (non-void, or a pointer) and unconditionally counts
|
|
885
|
+
// it (refs/gjs/gi/arg-cache.cpp: initialize() `m_has_return = tag != VOID || …`,
|
|
886
|
+
// build_return() `*inc_counter_out = true`); nothing in gjs's marshalling consults
|
|
887
|
+
// `gi_callable_info_skip_return`. Verified against gjs 1.88:
|
|
888
|
+
// GLib.uri_split('http://user@host:80/p?q=1#frag', 0)
|
|
889
|
+
// → [true, 'http', 'user', 'host', 80, '/p', 'q=1', 'frag'] (8, leading bool)
|
|
890
|
+
// even though g_uri_split's gboolean return carries `skip="1"` (and throws). node-gi
|
|
891
|
+
// previously honoured the annotation and dropped it (7) — a divergence from the gold
|
|
892
|
+
// standard. (node-gtk's ShouldSkipReturn does honour it; we match GJS, not node-gtk.)
|
|
893
|
+
if (gi_type_info_get_tag(return_type) != GI_TYPE_TAG_VOID) {
|
|
894
|
+
results.push_back(ReadOutOrReturn(env, callable, return_type, &retval, return_transfer, &slots));
|
|
895
|
+
}
|
|
896
|
+
gi_base_info_unref(return_type);
|
|
897
|
+
|
|
898
|
+
for (unsigned int i = 0; i < n_args && !env.IsExceptionPending(); i++) {
|
|
899
|
+
if (dirs[i] != GI_DIRECTION_OUT && dirs[i] != GI_DIRECTION_INOUT) continue;
|
|
900
|
+
if (skip[i]) continue; // an array length arg — surfaced via its array, not on its own
|
|
901
|
+
GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
|
|
902
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
903
|
+
GITransfer transfer = gi_arg_info_get_ownership_transfer(ai);
|
|
904
|
+
if (callerAllocBlob[i] != nullptr) {
|
|
905
|
+
// Caller-allocated OUT: the callee filled the blob IN PLACE (the blob is the
|
|
906
|
+
// data, not a pointer to it). Wrap it, then release the blob — mirroring
|
|
907
|
+
// gjs's CallerAllocatesOut (read the value out, then free the storage).
|
|
908
|
+
gpointer blob = callerAllocBlob[i];
|
|
909
|
+
if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY) {
|
|
910
|
+
// A fixed-size C array: read length from the fixed size (len=-1), transfer
|
|
911
|
+
// NOTHING (we own + free the blob ourselves right below).
|
|
912
|
+
GIArgument a;
|
|
913
|
+
memset(&a, 0, sizeof(a));
|
|
914
|
+
a.v_pointer = blob;
|
|
915
|
+
results.push_back(ReadOutOrReturn(env, callable, ti, &a, GI_TRANSFER_NOTHING, &slots));
|
|
916
|
+
g_free(blob);
|
|
917
|
+
} else if (g_type_is_a(callerAllocGType[i], G_TYPE_VALUE)) {
|
|
918
|
+
// A caller-allocated GValue OUT (e.g. GIMarshallingTests.gvalue_out_caller_allocates):
|
|
919
|
+
// GJS UNBOXES the filled GValue to its contained JS value (verified: → 42, a
|
|
920
|
+
// number, not a GObject.Value box), then frees the caller-allocated storage.
|
|
921
|
+
// The blob IS the GValue (g_value_init'd + set IN PLACE by the callee). Unbox
|
|
922
|
+
// via GValueToJs (same as the return/OUT-pointer path in marshal.cc), then
|
|
923
|
+
// g_value_unset (frees any contained data, e.g. a dup'd string) + g_free the
|
|
924
|
+
// blob WE g_malloc0'd — the direct GValue release GJS uses.
|
|
925
|
+
results.push_back(GValueToJs(env, static_cast<GValue*>(blob)));
|
|
926
|
+
g_value_unset(static_cast<GValue*>(blob));
|
|
927
|
+
g_free(blob);
|
|
928
|
+
} else if (callerAllocGType[i] != 0) {
|
|
929
|
+
// A boxed struct/union: hand the JS side an independent boxed COPY it owns
|
|
930
|
+
// (finalizer g_boxed_free's it), then free the caller-allocated blob (also
|
|
931
|
+
// via g_boxed_free — matches gjs BoxedCallerAllocatesOut::release).
|
|
932
|
+
GType gt = callerAllocGType[i];
|
|
933
|
+
gpointer owned = g_boxed_copy(gt, blob);
|
|
934
|
+
results.push_back(MakeBoxedHandle(env, owned, gt, true));
|
|
935
|
+
g_boxed_free(gt, blob);
|
|
936
|
+
} else {
|
|
937
|
+
// A PLAIN (non-boxed) struct/union filled in place (e.g. the
|
|
938
|
+
// PangoRectangles of PangoLayout.get_pixel_extents): no copy function
|
|
939
|
+
// exists, so hand the g_malloc0'd blob ITSELF to JS — the handle owns the
|
|
940
|
+
// block (g_free on finalize; gjs equivalently memdups into its Boxed and
|
|
941
|
+
// g_free's the blob, CallerAllocatesOut::release) and carries the static
|
|
942
|
+
// struct info so fields read back (rect.x / rect.width via getBoxedField).
|
|
943
|
+
// A registered-but-non-boxed GType (if any) is kept for method resolution.
|
|
944
|
+
GIBaseInfo* si = gi_type_info_get_interface(ti);
|
|
945
|
+
GType gt = si != nullptr ? gi_registered_type_info_get_g_type(
|
|
946
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(si))
|
|
947
|
+
: G_TYPE_INVALID;
|
|
948
|
+
GType handleGType = (gt != G_TYPE_INVALID && gt != G_TYPE_NONE) ? gt : G_TYPE_INVALID;
|
|
949
|
+
results.push_back(
|
|
950
|
+
MakeBoxedHandle(env, blob, handleGType, false, si, /* rawOwned = */ true));
|
|
951
|
+
if (si != nullptr) gi_base_info_unref(si);
|
|
952
|
+
}
|
|
953
|
+
callerAllocBlob[i] = nullptr;
|
|
954
|
+
gi_base_info_unref(ti);
|
|
955
|
+
gi_base_info_unref(ai);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
results.push_back(ReadOutOrReturn(env, callable, ti, &slots[i], transfer, &slots));
|
|
959
|
+
gi_base_info_unref(ti);
|
|
960
|
+
gi_base_info_unref(ai);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Free the transfer-nothing IN containers now that the results are marshalled
|
|
964
|
+
// into JS — a transfer-none return/OUT (e.g. g_environ_getenv) can point INTO
|
|
965
|
+
// a transfer-none IN container, so this MUST run after the reads above, never
|
|
966
|
+
// before. Transfer-everything/container IN were adopted by the callee, so
|
|
967
|
+
// FreeInContainer leaves those alone.
|
|
968
|
+
for (const InContainer& c : inContainers) FreeInContainer(c);
|
|
969
|
+
|
|
970
|
+
if (env.IsExceptionPending()) return env.Null();
|
|
971
|
+
if (results.empty()) return env.Undefined();
|
|
972
|
+
if (results.size() == 1) return results[0];
|
|
973
|
+
Napi::Array arr = Napi::Array::New(env, results.size());
|
|
974
|
+
for (size_t k = 0; k < results.size(); k++) arr.Set(static_cast<uint32_t>(k), results[k]);
|
|
975
|
+
return arr;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// callFunction(namespace, functionName, args?: unknown[]) -> unknown
|
|
979
|
+
// Invokes a namespace-level GI function (not an instance method) with IN-only
|
|
980
|
+
// primitive/string/object args. Instance methods go through callMethod.
|
|
981
|
+
Napi::Value CallFunction(const Napi::CallbackInfo& info) {
|
|
982
|
+
Napi::Env env = info.Env();
|
|
983
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
984
|
+
Napi::TypeError::New(env, "callFunction(namespace: string, functionName: string, args?: unknown[])")
|
|
985
|
+
.ThrowAsJavaScriptException();
|
|
986
|
+
return env.Null();
|
|
987
|
+
}
|
|
988
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
989
|
+
std::string fn = info[1].As<Napi::String>().Utf8Value();
|
|
990
|
+
Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
|
|
991
|
+
: Napi::Array::New(env, 0);
|
|
992
|
+
|
|
993
|
+
GIRepository* repo = DupDefaultRepository();
|
|
994
|
+
GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), fn.c_str());
|
|
995
|
+
if (base == nullptr) {
|
|
996
|
+
g_object_unref(repo);
|
|
997
|
+
Napi::Error::New(env, "No such symbol: " + ns + "." + fn).ThrowAsJavaScriptException();
|
|
998
|
+
return env.Null();
|
|
999
|
+
}
|
|
1000
|
+
if (!GI_IS_FUNCTION_INFO(base)) {
|
|
1001
|
+
gi_base_info_unref(base);
|
|
1002
|
+
g_object_unref(repo);
|
|
1003
|
+
Napi::TypeError::New(env, ns + "." + fn + " is not a function").ThrowAsJavaScriptException();
|
|
1004
|
+
return env.Null();
|
|
1005
|
+
}
|
|
1006
|
+
GIFunctionInfo* func = reinterpret_cast<GIFunctionInfo*>(base);
|
|
1007
|
+
if (gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(base))) {
|
|
1008
|
+
gi_base_info_unref(base);
|
|
1009
|
+
g_object_unref(repo);
|
|
1010
|
+
Napi::TypeError::New(
|
|
1011
|
+
env, ns + "." + fn + " is an instance method — use callMethod(handle, name, args)")
|
|
1012
|
+
.ThrowAsJavaScriptException();
|
|
1013
|
+
return env.Null();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + fn);
|
|
1017
|
+
gi_base_info_unref(base);
|
|
1018
|
+
g_object_unref(repo);
|
|
1019
|
+
return result;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// The JS-side camelCase → snake_case alias (gi.js camelToSnake), mirrored here so
|
|
1023
|
+
// method resolution can try the LITERAL introspected name first and only fall
|
|
1024
|
+
// back to the alias. GJS exposes GI method names VERBATIM — a Vala-generated GIR
|
|
1025
|
+
// carries camelCase names (Gwebgl's `getString`), which an unconditional
|
|
1026
|
+
// camelCase→snake conversion destroys (the webgl-glarea spike wall).
|
|
1027
|
+
static std::string CamelToSnake(const std::string& name) {
|
|
1028
|
+
std::string out;
|
|
1029
|
+
out.reserve(name.size() + 4);
|
|
1030
|
+
for (char c : name) {
|
|
1031
|
+
if (c >= 'A' && c <= 'Z') {
|
|
1032
|
+
out += '_';
|
|
1033
|
+
out += static_cast<char>(c - 'A' + 'a');
|
|
1034
|
+
} else {
|
|
1035
|
+
out += c;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return out;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// The instance's concrete GType may lack introspection info (e.g. a private
|
|
1042
|
+
// GLocalFile); walk up to the nearest ancestor GType that has an object info.
|
|
1043
|
+
// Returns a new ref or nullptr. Shared by CallMethod (invoke) and HasMethod
|
|
1044
|
+
// (feature detection) so both resolve identically.
|
|
1045
|
+
static GIObjectInfo* FindNearestObjectInfo(GIRepository* repo, GType gtype) {
|
|
1046
|
+
for (GType t = gtype; t != 0; t = g_type_parent(t)) {
|
|
1047
|
+
GIBaseInfo* bi = gi_repository_find_by_gtype(repo, t);
|
|
1048
|
+
if (bi != nullptr) {
|
|
1049
|
+
if (GI_IS_OBJECT_INFO(bi)) return reinterpret_cast<GIObjectInfo*>(bi);
|
|
1050
|
+
gi_base_info_unref(bi);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return nullptr;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Resolve one candidate name: own + implemented-interface methods at each
|
|
1057
|
+
// level, then the parent chain; then the live GType's interface list (the
|
|
1058
|
+
// concrete GType may implement introspectable interfaces its nearest
|
|
1059
|
+
// introspectable ANCESTOR's info does not list — e.g. a private GLocalFile
|
|
1060
|
+
// (no info; ancestor = GObject) implementing GFile, so g_file_get_path
|
|
1061
|
+
// resolves). Returns a new ref or nullptr.
|
|
1062
|
+
static GIFunctionInfo* ResolveMethodByName(GIRepository* repo, GType gtype, GIObjectInfo* objInfo,
|
|
1063
|
+
const char* name) {
|
|
1064
|
+
GIObjectInfo* walk =
|
|
1065
|
+
reinterpret_cast<GIObjectInfo*>(gi_base_info_ref(reinterpret_cast<GIBaseInfo*>(objInfo)));
|
|
1066
|
+
GIFunctionInfo* found = nullptr;
|
|
1067
|
+
while (walk != nullptr) {
|
|
1068
|
+
GIBaseInfo* declarer = nullptr;
|
|
1069
|
+
found = gi_object_info_find_method_using_interfaces(walk, name, &declarer);
|
|
1070
|
+
if (declarer != nullptr) gi_base_info_unref(declarer);
|
|
1071
|
+
if (found != nullptr) break;
|
|
1072
|
+
GIObjectInfo* parent = gi_object_info_get_parent(walk);
|
|
1073
|
+
gi_base_info_unref(walk);
|
|
1074
|
+
walk = parent;
|
|
1075
|
+
}
|
|
1076
|
+
if (walk != nullptr) gi_base_info_unref(walk);
|
|
1077
|
+
if (found == nullptr) {
|
|
1078
|
+
unsigned int n_ifaces = 0;
|
|
1079
|
+
GType* ifaces = g_type_interfaces(gtype, &n_ifaces);
|
|
1080
|
+
for (unsigned int i = 0; i < n_ifaces && found == nullptr; i++) {
|
|
1081
|
+
GIBaseInfo* ii = gi_repository_find_by_gtype(repo, ifaces[i]);
|
|
1082
|
+
if (ii != nullptr) {
|
|
1083
|
+
if (GI_IS_INTERFACE_INFO(ii)) {
|
|
1084
|
+
found = gi_interface_info_find_method(reinterpret_cast<GIInterfaceInfo*>(ii), name);
|
|
1085
|
+
}
|
|
1086
|
+
gi_base_info_unref(ii);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
g_free(ifaces);
|
|
1090
|
+
}
|
|
1091
|
+
return found;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Resolve `method` — LITERAL introspected name first (GJS fidelity: GIR names
|
|
1095
|
+
// are exposed verbatim, incl. Vala camelCase like Gwebgl's `getString`),
|
|
1096
|
+
// snake_case alias second. Returns a new ref or nullptr.
|
|
1097
|
+
static GIFunctionInfo* ResolveInstanceMethod(GIRepository* repo, GType gtype, GIObjectInfo* objInfo,
|
|
1098
|
+
const std::string& method) {
|
|
1099
|
+
GIFunctionInfo* func = ResolveMethodByName(repo, gtype, objInfo, method.c_str());
|
|
1100
|
+
if (func == nullptr) {
|
|
1101
|
+
const std::string snake = CamelToSnake(method);
|
|
1102
|
+
if (snake != method) func = ResolveMethodByName(repo, gtype, objInfo, snake.c_str());
|
|
1103
|
+
}
|
|
1104
|
+
return func;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// callMethod(handle, methodName, args?: unknown[]) -> unknown
|
|
1108
|
+
// Resolve an instance method by walking the instance's GIObjectInfo (own +
|
|
1109
|
+
// implemented-interface methods at each level, then up the parent chain), then
|
|
1110
|
+
// invoke it with the instance prepended. The Node twin of `obj.method(...)`.
|
|
1111
|
+
// The LITERAL name is resolved first (GJS fidelity — GIR names are exposed
|
|
1112
|
+
// verbatim, incl. Vala camelCase); the snake_case alias is the fallback.
|
|
1113
|
+
Napi::Value CallMethod(const Napi::CallbackInfo& info) {
|
|
1114
|
+
Napi::Env env = info.Env();
|
|
1115
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
1116
|
+
Napi::TypeError::New(env, "callMethod(handle, methodName: string, args?: unknown[])")
|
|
1117
|
+
.ThrowAsJavaScriptException();
|
|
1118
|
+
return env.Null();
|
|
1119
|
+
}
|
|
1120
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
1121
|
+
if (obj == nullptr) return env.Null();
|
|
1122
|
+
std::string method = info[1].As<Napi::String>().Utf8Value();
|
|
1123
|
+
Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
|
|
1124
|
+
: Napi::Array::New(env, 0);
|
|
1125
|
+
|
|
1126
|
+
GType gtype = G_OBJECT_TYPE(obj);
|
|
1127
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1128
|
+
|
|
1129
|
+
GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
|
|
1130
|
+
if (objInfo == nullptr) {
|
|
1131
|
+
g_object_unref(repo);
|
|
1132
|
+
Napi::Error::New(env, std::string("no introspection info for ") + G_OBJECT_TYPE_NAME(obj))
|
|
1133
|
+
.ThrowAsJavaScriptException();
|
|
1134
|
+
return env.Null();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
|
|
1138
|
+
gi_base_info_unref(objInfo);
|
|
1139
|
+
g_object_unref(repo);
|
|
1140
|
+
|
|
1141
|
+
if (func == nullptr) {
|
|
1142
|
+
// On a terminating env the method-name read (info[1].Utf8Value) degrades to
|
|
1143
|
+
// "" (a swallowed napi failure), so resolution misses and we land here — but
|
|
1144
|
+
// building the Napi::Error on the dying env would abort via Error::New's
|
|
1145
|
+
// fatal funnel. Gate on NodeGiJsAvailable (false on a dying env AND on a live
|
|
1146
|
+
// env with a pending exception); a genuine unknown method on a live env still
|
|
1147
|
+
// throws.
|
|
1148
|
+
if (NodeGiJsAvailable(env)) {
|
|
1149
|
+
Napi::Error::New(env, std::string("no method '") + method + "' on " + G_OBJECT_TYPE_NAME(obj))
|
|
1150
|
+
.ThrowAsJavaScriptException();
|
|
1151
|
+
}
|
|
1152
|
+
return env.Null();
|
|
1153
|
+
}
|
|
1154
|
+
if (!gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
|
|
1155
|
+
gi_base_info_unref(func);
|
|
1156
|
+
Napi::TypeError::New(env, method + " is not an instance method").ThrowAsJavaScriptException();
|
|
1157
|
+
return env.Null();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
Napi::Value result =
|
|
1161
|
+
InvokeFunctionInfo(env, func, obj, args, std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
|
|
1162
|
+
gi_base_info_unref(func);
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// hasMethod(handle, methodName) -> boolean
|
|
1167
|
+
// TRUE iff callMethod(handle, methodName, …) would resolve an invocable
|
|
1168
|
+
// instance method (literal introspected name or snake_case alias, walking the
|
|
1169
|
+
// same own/interface/parent chain). Backs the L1 wrapper's GJS-parity property
|
|
1170
|
+
// access: `obj.nonexistentMethod` must be `undefined` — real consumers
|
|
1171
|
+
// feature-detect optional native methods (`typeof gl.clearBufferfv ===
|
|
1172
|
+
// 'function'` in `@gjsify/webgl`'s clearBuffer emulation, the exposing call
|
|
1173
|
+
// under Excalibur's RenderTarget.blitToScreen) and the old unconditional
|
|
1174
|
+
// throw-on-call thunk made that detection lie. Never throws for a merely
|
|
1175
|
+
// unknown name; a non-introspectable instance simply reports false.
|
|
1176
|
+
Napi::Value HasMethod(const Napi::CallbackInfo& info) {
|
|
1177
|
+
Napi::Env env = info.Env();
|
|
1178
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
1179
|
+
Napi::TypeError::New(env, "hasMethod(handle, methodName: string)").ThrowAsJavaScriptException();
|
|
1180
|
+
return env.Null();
|
|
1181
|
+
}
|
|
1182
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
1183
|
+
if (obj == nullptr) return env.Null();
|
|
1184
|
+
std::string method = info[1].As<Napi::String>().Utf8Value();
|
|
1185
|
+
|
|
1186
|
+
GType gtype = G_OBJECT_TYPE(obj);
|
|
1187
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1188
|
+
GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
|
|
1189
|
+
if (objInfo == nullptr) {
|
|
1190
|
+
g_object_unref(repo);
|
|
1191
|
+
return Napi::Boolean::New(env, false);
|
|
1192
|
+
}
|
|
1193
|
+
GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
|
|
1194
|
+
gi_base_info_unref(objInfo);
|
|
1195
|
+
g_object_unref(repo);
|
|
1196
|
+
if (func == nullptr) return Napi::Boolean::New(env, false);
|
|
1197
|
+
const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
|
|
1198
|
+
gi_base_info_unref(func);
|
|
1199
|
+
return Napi::Boolean::New(env, invocable);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// callStaticMethod(namespace, typeName, methodName, args?) -> unknown
|
|
1203
|
+
// Invoke a type-level constructor/static function (e.g. Gio.File.new_for_path,
|
|
1204
|
+
// Gtk.Label.new) — a function found ON a type but taking no instance. The Node
|
|
1205
|
+
// twin of `Ns.Class.method(...)`.
|
|
1206
|
+
Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
|
|
1207
|
+
Napi::Env env = info.Env();
|
|
1208
|
+
if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
|
|
1209
|
+
Napi::TypeError::New(
|
|
1210
|
+
env, "callStaticMethod(namespace: string, typeName: string, methodName: string, args?: unknown[])")
|
|
1211
|
+
.ThrowAsJavaScriptException();
|
|
1212
|
+
return env.Null();
|
|
1213
|
+
}
|
|
1214
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
1215
|
+
std::string tn = info[1].As<Napi::String>().Utf8Value();
|
|
1216
|
+
std::string method = info[2].As<Napi::String>().Utf8Value();
|
|
1217
|
+
Napi::Array args = (info.Length() >= 4 && info[3].IsArray()) ? info[3].As<Napi::Array>()
|
|
1218
|
+
: Napi::Array::New(env, 0);
|
|
1219
|
+
|
|
1220
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1221
|
+
GIBaseInfo* typeInfo = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
|
|
1222
|
+
if (typeInfo == nullptr) {
|
|
1223
|
+
g_object_unref(repo);
|
|
1224
|
+
Napi::Error::New(env, "no such type: " + ns + "." + tn).ThrowAsJavaScriptException();
|
|
1225
|
+
return env.Null();
|
|
1226
|
+
}
|
|
1227
|
+
GIFunctionInfo* func = nullptr;
|
|
1228
|
+
if (GI_IS_OBJECT_INFO(typeInfo)) {
|
|
1229
|
+
func = gi_object_info_find_method(reinterpret_cast<GIObjectInfo*>(typeInfo), method.c_str());
|
|
1230
|
+
} else if (GI_IS_INTERFACE_INFO(typeInfo)) {
|
|
1231
|
+
func = gi_interface_info_find_method(reinterpret_cast<GIInterfaceInfo*>(typeInfo), method.c_str());
|
|
1232
|
+
} else if (GI_IS_STRUCT_INFO(typeInfo)) {
|
|
1233
|
+
func = gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(typeInfo), method.c_str());
|
|
1234
|
+
}
|
|
1235
|
+
gi_base_info_unref(typeInfo);
|
|
1236
|
+
if (func == nullptr) {
|
|
1237
|
+
g_object_unref(repo);
|
|
1238
|
+
Napi::Error::New(env, "no static method '" + method + "' on " + ns + "." + tn)
|
|
1239
|
+
.ThrowAsJavaScriptException();
|
|
1240
|
+
return env.Null();
|
|
1241
|
+
}
|
|
1242
|
+
if (gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
|
|
1243
|
+
gi_base_info_unref(func);
|
|
1244
|
+
g_object_unref(repo);
|
|
1245
|
+
Napi::TypeError::New(env, ns + "." + tn + "." + method +
|
|
1246
|
+
" is an instance method — call it on an instance")
|
|
1247
|
+
.ThrowAsJavaScriptException();
|
|
1248
|
+
return env.Null();
|
|
1249
|
+
}
|
|
1250
|
+
Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + tn + "." + method);
|
|
1251
|
+
gi_base_info_unref(func);
|
|
1252
|
+
g_object_unref(repo);
|
|
1253
|
+
return result;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// constructStruct(namespace, typeName, args?) -> boxed handle
|
|
1257
|
+
// The `new <Struct>()` [[Construct]] path for boxed/plain structs — GJS
|
|
1258
|
+
// gi/boxed.cpp parity. Resolution order:
|
|
1259
|
+
// 1. the struct/union HAS a 'new' constructor → invoke it with the args (the
|
|
1260
|
+
// pre-existing `new GLib.MainLoop(null, false)` behavior, unchanged);
|
|
1261
|
+
// 2. no 'new', ZERO args, a struct with a known size → a ZERO-INITIALIZED
|
|
1262
|
+
// instance (GJS zero-allocates in boxed_new — `new Graphene.Rect()`,
|
|
1263
|
+
// `new Gdk.RGBA()`). For a registered BOXED GType the zero blob is
|
|
1264
|
+
// g_boxed_copy'd so the handle's g_boxed_free matches the type's real
|
|
1265
|
+
// allocator (graphene g_slice/malloc-allocates internally — handing a raw
|
|
1266
|
+
// g_malloc0 blob to g_boxed_free would corrupt that allocator); the same
|
|
1267
|
+
// pattern as NewGValue's g_boxed_copy(G_TYPE_VALUE, &zero) in object.cc,
|
|
1268
|
+
// generalized. A struct with NO registered boxed GType keeps the g_malloc0
|
|
1269
|
+
// blob itself, owned via the rawOwned/g_free finalizer arm (the
|
|
1270
|
+
// caller-allocates OUT pattern above).
|
|
1271
|
+
// 3. no 'new' + args → a clear error (GJS supports no positional field args
|
|
1272
|
+
// for boxed construction either);
|
|
1273
|
+
// 4. a union without 'new' keeps throwing — GJS's union.cpp requires a
|
|
1274
|
+
// zero-args constructor and never zero-allocates a union.
|
|
1275
|
+
Napi::Value ConstructStruct(const Napi::CallbackInfo& info) {
|
|
1276
|
+
Napi::Env env = info.Env();
|
|
1277
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
1278
|
+
Napi::TypeError::New(env,
|
|
1279
|
+
"constructStruct(namespace: string, typeName: string, args?: unknown[])")
|
|
1280
|
+
.ThrowAsJavaScriptException();
|
|
1281
|
+
return env.Null();
|
|
1282
|
+
}
|
|
1283
|
+
std::string ns = info[0].As<Napi::String>().Utf8Value();
|
|
1284
|
+
std::string tn = info[1].As<Napi::String>().Utf8Value();
|
|
1285
|
+
Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
|
|
1286
|
+
: Napi::Array::New(env, 0);
|
|
1287
|
+
|
|
1288
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1289
|
+
GIBaseInfo* typeInfo = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
|
|
1290
|
+
if (typeInfo == nullptr) {
|
|
1291
|
+
g_object_unref(repo);
|
|
1292
|
+
Napi::Error::New(env, "no such type: " + ns + "." + tn).ThrowAsJavaScriptException();
|
|
1293
|
+
return env.Null();
|
|
1294
|
+
}
|
|
1295
|
+
const bool isStruct = GI_IS_STRUCT_INFO(typeInfo);
|
|
1296
|
+
const bool isUnion = GI_IS_UNION_INFO(typeInfo);
|
|
1297
|
+
if (!isStruct && !isUnion) {
|
|
1298
|
+
gi_base_info_unref(typeInfo);
|
|
1299
|
+
g_object_unref(repo);
|
|
1300
|
+
Napi::TypeError::New(env, ns + "." + tn + " is not a struct/union type")
|
|
1301
|
+
.ThrowAsJavaScriptException();
|
|
1302
|
+
return env.Null();
|
|
1303
|
+
}
|
|
1304
|
+
GIFunctionInfo* func =
|
|
1305
|
+
isStruct ? gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(typeInfo), "new")
|
|
1306
|
+
: gi_union_info_find_method(reinterpret_cast<GIUnionInfo*>(typeInfo), "new");
|
|
1307
|
+
if (func != nullptr) {
|
|
1308
|
+
// Has a 'new' constructor → the exact pre-existing static-call path.
|
|
1309
|
+
gi_base_info_unref(typeInfo);
|
|
1310
|
+
Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + tn + ".new");
|
|
1311
|
+
gi_base_info_unref(func);
|
|
1312
|
+
g_object_unref(repo);
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
if (isUnion) {
|
|
1316
|
+
gi_base_info_unref(typeInfo);
|
|
1317
|
+
g_object_unref(repo);
|
|
1318
|
+
Napi::Error::New(env, "unable to construct union " + ns + "." + tn +
|
|
1319
|
+
": it has no 'new' constructor")
|
|
1320
|
+
.ThrowAsJavaScriptException();
|
|
1321
|
+
return env.Null();
|
|
1322
|
+
}
|
|
1323
|
+
if (args.Length() > 0) {
|
|
1324
|
+
gi_base_info_unref(typeInfo);
|
|
1325
|
+
g_object_unref(repo);
|
|
1326
|
+
Napi::Error::New(env, ns + "." + tn +
|
|
1327
|
+
": constructor takes no arguments (boxed struct has no 'new')")
|
|
1328
|
+
.ThrowAsJavaScriptException();
|
|
1329
|
+
return env.Null();
|
|
1330
|
+
}
|
|
1331
|
+
const size_t size = gi_struct_info_get_size(reinterpret_cast<GIStructInfo*>(typeInfo));
|
|
1332
|
+
if (size == 0) {
|
|
1333
|
+
// Opaque struct (size unknown to GI) — nothing to zero-allocate; matches
|
|
1334
|
+
// GJS, which cannot allocate a boxed of unknown size either.
|
|
1335
|
+
gi_base_info_unref(typeInfo);
|
|
1336
|
+
g_object_unref(repo);
|
|
1337
|
+
Napi::Error::New(env, "unable to construct " + ns + "." + tn +
|
|
1338
|
+
": no 'new' constructor and unknown size (opaque struct)")
|
|
1339
|
+
.ThrowAsJavaScriptException();
|
|
1340
|
+
return env.Null();
|
|
1341
|
+
}
|
|
1342
|
+
const GType gt =
|
|
1343
|
+
gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(typeInfo));
|
|
1344
|
+
Napi::Value result;
|
|
1345
|
+
if (gt != G_TYPE_INVALID && gt != G_TYPE_NONE && G_TYPE_IS_BOXED(gt)) {
|
|
1346
|
+
gpointer zero = g_malloc0(size);
|
|
1347
|
+
gpointer owned = g_boxed_copy(gt, zero);
|
|
1348
|
+
g_free(zero);
|
|
1349
|
+
result = MakeBoxedHandle(env, owned, gt, /* owns */ true, typeInfo);
|
|
1350
|
+
} else {
|
|
1351
|
+
// Plain (or registered-but-non-boxed) C struct: the handle owns the zeroed
|
|
1352
|
+
// block directly (g_free on finalize); a registered GType is kept for
|
|
1353
|
+
// method/field resolution, `owns` stays false so g_boxed_free never runs.
|
|
1354
|
+
const GType handleGType = (gt != G_TYPE_INVALID && gt != G_TYPE_NONE) ? gt : G_TYPE_INVALID;
|
|
1355
|
+
gpointer blob = g_malloc0(size);
|
|
1356
|
+
result = MakeBoxedHandle(env, blob, handleGType, /* owns */ false, typeInfo,
|
|
1357
|
+
/* rawOwned */ true);
|
|
1358
|
+
}
|
|
1359
|
+
gi_base_info_unref(typeInfo);
|
|
1360
|
+
g_object_unref(repo);
|
|
1361
|
+
return result;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// callBoxedMethod(handle, methodName, args?) -> unknown
|
|
1365
|
+
// Invoke an instance method on a boxed/struct handle (e.g. mainLoop.run() /
|
|
1366
|
+
// mainLoop.quit()). Resolves the method against the boxed GType's GIStructInfo
|
|
1367
|
+
// (or union info) and invokes it with the boxed pointer prepended as instance.
|
|
1368
|
+
Napi::Value CallBoxedMethod(const Napi::CallbackInfo& info) {
|
|
1369
|
+
Napi::Env env = info.Env();
|
|
1370
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
1371
|
+
Napi::TypeError::New(env, "callBoxedMethod(handle, methodName: string, args?: unknown[])")
|
|
1372
|
+
.ThrowAsJavaScriptException();
|
|
1373
|
+
return env.Null();
|
|
1374
|
+
}
|
|
1375
|
+
if (!info[0].IsExternal() ||
|
|
1376
|
+
!info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag)) {
|
|
1377
|
+
Napi::TypeError::New(env, "expected a node-gi boxed handle").ThrowAsJavaScriptException();
|
|
1378
|
+
return env.Null();
|
|
1379
|
+
}
|
|
1380
|
+
BoxedHandle* bh = info[0].As<Napi::External<BoxedHandle>>().Data();
|
|
1381
|
+
if (bh == nullptr || bh->ptr == nullptr) {
|
|
1382
|
+
Napi::TypeError::New(env, "invalid boxed handle").ThrowAsJavaScriptException();
|
|
1383
|
+
return env.Null();
|
|
1384
|
+
}
|
|
1385
|
+
std::string method = info[1].As<Napi::String>().Utf8Value();
|
|
1386
|
+
Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
|
|
1387
|
+
: Napi::Array::New(env, 0);
|
|
1388
|
+
if (bh->gtype == G_TYPE_INVALID) {
|
|
1389
|
+
Napi::Error::New(env, "boxed handle has no introspection GType for method resolution")
|
|
1390
|
+
.ThrowAsJavaScriptException();
|
|
1391
|
+
return env.Null();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
GIRepository* repo = DupDefaultRepository();
|
|
1395
|
+
GIBaseInfo* bi = gi_repository_find_by_gtype(repo, bh->gtype);
|
|
1396
|
+
GIFunctionInfo* func = nullptr;
|
|
1397
|
+
if (bi != nullptr && GI_IS_STRUCT_INFO(bi)) {
|
|
1398
|
+
func = gi_struct_info_find_method(reinterpret_cast<GIStructInfo*>(bi), method.c_str());
|
|
1399
|
+
} else if (bi != nullptr && GI_IS_UNION_INFO(bi)) {
|
|
1400
|
+
func = gi_union_info_find_method(reinterpret_cast<GIUnionInfo*>(bi), method.c_str());
|
|
1401
|
+
}
|
|
1402
|
+
if (bi != nullptr) gi_base_info_unref(bi);
|
|
1403
|
+
if (func == nullptr) {
|
|
1404
|
+
g_object_unref(repo);
|
|
1405
|
+
Napi::Error::New(env, std::string("no method '") + method + "' on " + g_type_name(bh->gtype))
|
|
1406
|
+
.ThrowAsJavaScriptException();
|
|
1407
|
+
return env.Null();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
Napi::Value result = InvokeFunctionInfo(env, func, bh->ptr, args,
|
|
1411
|
+
std::string(g_type_name(bh->gtype)) + "." + method);
|
|
1412
|
+
gi_base_info_unref(func);
|
|
1413
|
+
g_object_unref(repo);
|
|
1414
|
+
return result;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// isBoxedHandle(value) -> boolean (tag-checked; no dereference)
|
|
1418
|
+
Napi::Value IsBoxedHandle(const Napi::CallbackInfo& info) {
|
|
1419
|
+
Napi::Env env = info.Env();
|
|
1420
|
+
bool is = info.Length() >= 1 && info[0].IsExternal() &&
|
|
1421
|
+
info[0].As<Napi::External<BoxedHandle>>().CheckTypeTag(&kBoxedHandleTag);
|
|
1422
|
+
return Napi::Boolean::New(env, is);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
} // namespace nodegi
|