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