@gjsify/node-gi 0.50.0 → 0.51.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/binding.gyp +6 -1
- package/gettext.d.ts +18 -18
- package/gettext.js +141 -54
- package/gi.js +94 -29
- package/globals.d.ts +1 -1
- package/gtk-runtime.js +22 -0
- package/index.js +20 -0
- package/overrides/package.js +6 -6
- package/package.json +14 -13
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/scripts/mac-intl-gyp-flags.mjs +70 -0
- package/scripts/win-gi-gyp-flags.mjs +22 -4
- package/src/addon.cc +17 -0
- package/src/calls.cc +57 -0
- package/src/class.cc +135 -0
- package/src/common.h +35 -0
- package/src/private.cc +229 -0
- package/src/repo.cc +1 -0
- package/src/toggle.cc +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"binding.gyp",
|
|
63
63
|
"scripts/install.mjs",
|
|
64
64
|
"scripts/win-gi-gyp-flags.mjs",
|
|
65
|
+
"scripts/mac-intl-gyp-flags.mjs",
|
|
65
66
|
"prebuilds"
|
|
66
67
|
],
|
|
67
68
|
"gjsify": {
|
|
@@ -87,23 +88,23 @@
|
|
|
87
88
|
"build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
|
|
88
89
|
"rebuild": "node-gyp rebuild",
|
|
89
90
|
"clean": "node-gyp clean",
|
|
90
|
-
"test": "NODE_GI_NATIVE=build node --test",
|
|
91
|
-
"test:node": "NODE_GI_NATIVE=build node --test",
|
|
92
|
-
"test:gc": "NODE_GI_NATIVE=build node --test --expose-gc",
|
|
93
|
-
"test:dbus": "dbus-run-session --config-file=test/session.conf -- env NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
|
|
91
|
+
"test": "LC_ALL=C NODE_GI_NATIVE=build node --test",
|
|
92
|
+
"test:node": "LC_ALL=C NODE_GI_NATIVE=build node --test",
|
|
93
|
+
"test:gc": "LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc",
|
|
94
|
+
"test:dbus": "dbus-run-session --config-file=test/session.conf -- env LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
|
|
94
95
|
"test:bun": "node scripts/cross-runtime.mjs bun",
|
|
95
96
|
"test:conformance": "node scripts/conformance.mjs",
|
|
96
97
|
"test:conformance:gjs-napi": "node scripts/conformance.mjs --runtimes=gjs-napi",
|
|
97
98
|
"test:gimarshalling": "node scripts/gimarshalling.mjs",
|
|
98
99
|
"test:deno": "node scripts/cross-runtime.mjs deno",
|
|
99
|
-
"test:cairo": "NODE_GI_NATIVE=build node --test test/cairo.test.mjs",
|
|
100
|
-
"test:cairo-canvas2d": "NODE_GI_NATIVE=build node --test test/cairo-canvas2d.test.mjs",
|
|
101
|
-
"test:gtk": "NODE_GI_NATIVE=build node --test test/gtk-smoke.test.mjs",
|
|
102
|
-
"test:gtk-cairo": "NODE_GI_NATIVE=build node --test test/cairo-drawfunc.test.mjs",
|
|
103
|
-
"test:adw": "NODE_GI_NATIVE=build node --test test/adw-smoke.test.mjs",
|
|
104
|
-
"test:gtk-template": "NODE_GI_NATIVE=build node --test test/gtk-template.test.mjs",
|
|
105
|
-
"test:gtk-template-callbacks": "NODE_GI_NATIVE=build node --test test/gtk-template-callbacks.test.mjs",
|
|
106
|
-
"test:gtk-template-signals": "NODE_GI_NATIVE=build node --test test/gtk-template-signals.test.mjs"
|
|
100
|
+
"test:cairo": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo.test.mjs",
|
|
101
|
+
"test:cairo-canvas2d": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-canvas2d.test.mjs",
|
|
102
|
+
"test:gtk": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-smoke.test.mjs",
|
|
103
|
+
"test:gtk-cairo": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-drawfunc.test.mjs",
|
|
104
|
+
"test:adw": "LC_ALL=C NODE_GI_NATIVE=build node --test test/adw-smoke.test.mjs",
|
|
105
|
+
"test:gtk-template": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template.test.mjs",
|
|
106
|
+
"test:gtk-template-callbacks": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-callbacks.test.mjs",
|
|
107
|
+
"test:gtk-template-signals": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-signals.test.mjs"
|
|
107
108
|
},
|
|
108
109
|
"dependencies": {
|
|
109
110
|
"node-addon-api": "^8.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Emit the link flags the darwin addon needs for libintl, for binding.gyp's
|
|
3
|
+
// `OS=="mac"` branch.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/mac-intl-gyp-flags.mjs -> "-L<dir> -lintl", or nothing
|
|
6
|
+
//
|
|
7
|
+
// WHY ONLY DARWIN. `bindtextdomain` / `textdomain` / `bind_textdomain_codeset` are
|
|
8
|
+
// GNU gettext. glibc absorbs them into libc, so Linux needs no flag at all
|
|
9
|
+
// (`nm -D libc.so.6` lists both as weak symbols); Windows resolves them out of
|
|
10
|
+
// gvsbuild's `intl.dll` through the `--static` pkg-config closure the Windows
|
|
11
|
+
// branch already walks. macOS's libSystem has NO gettext — Homebrew's `gettext`
|
|
12
|
+
// formula supplies `libintl.dylib`, and GLib links it, which is how `g_dgettext`
|
|
13
|
+
// works there. But a Mach-O is two-level-namespaced: the addon must name the
|
|
14
|
+
// library it takes symbols from, so "GLib already loaded it" is not enough at LINK
|
|
15
|
+
// time, only at run time.
|
|
16
|
+
//
|
|
17
|
+
// Emitting NOTHING when no libintl is found is deliberate: the failure then is a
|
|
18
|
+
// named undefined-symbol error at link, which says more than a guessed `-L` that
|
|
19
|
+
// silently points somewhere wrong.
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
if (process.platform !== 'darwin') process.exit(0);
|
|
25
|
+
|
|
26
|
+
function brewPrefix() {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('brew', ['--prefix'], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
|
+
}).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
// No Homebrew on this host — the fixed paths below still cover the two
|
|
34
|
+
// standard prefixes, and MacPorts users get the honest link error.
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pkgConfigLibDirs() {
|
|
40
|
+
const pkgConfig = process.env.PKG_CONFIG || 'pkg-config';
|
|
41
|
+
try {
|
|
42
|
+
const out = execFileSync(pkgConfig, ['--libs-only-L', 'glib-2.0'], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
45
|
+
});
|
|
46
|
+
return out
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.filter((t) => t.startsWith('-L'))
|
|
49
|
+
.map((t) => t.slice(2));
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Homebrew symlinks every keg's lib/ into <prefix>/lib, so the gettext keg is
|
|
56
|
+
// reachable from the prefix even though glib's own .pc points at glib's cellar dir.
|
|
57
|
+
const prefix = brewPrefix();
|
|
58
|
+
const candidates = [
|
|
59
|
+
...pkgConfigLibDirs(),
|
|
60
|
+
...(prefix ? [join(prefix, 'lib')] : []),
|
|
61
|
+
'/opt/homebrew/lib',
|
|
62
|
+
'/usr/local/lib',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const dir of candidates) {
|
|
66
|
+
if (existsSync(join(dir, 'libintl.dylib')) || existsSync(join(dir, 'libintl.a'))) {
|
|
67
|
+
process.stdout.write(`-L${dir} -lintl`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -124,9 +124,22 @@ if (mode === '--includes') {
|
|
|
124
124
|
// glib/gobject/gio/gmodule-2.0 g_*/GObject/GValue/GSignal/GClosure/Gio
|
|
125
125
|
// ffi ffi_call / ffi_closure_alloc (girffi + the vfunc/callback trampolines)
|
|
126
126
|
// cairo the native cairo foreign-struct binding (src/cairo.cc)
|
|
127
|
+
// intl bindtextdomain / textdomain / bind_textdomain_codeset (src/private.cc);
|
|
128
|
+
// glibc absorbs these into libc so Linux names nothing, but the MSVC CRT
|
|
129
|
+
// has no gettext and gvsbuild ships GNU gettext as `intl.dll` — the same
|
|
130
|
+
// library GLib itself links for g_dgettext
|
|
127
131
|
// Pango/GdkPixbuf/Graphene are NOT linked — the conformance programs load their
|
|
128
132
|
// typelibs at RUNTIME (girepository dlopens the DLL by soname), not at link time.
|
|
129
|
-
const DIRECT_LIBS = [
|
|
133
|
+
const DIRECT_LIBS = [
|
|
134
|
+
'girepository-2.0',
|
|
135
|
+
'gio-2.0',
|
|
136
|
+
'gobject-2.0',
|
|
137
|
+
'gmodule-2.0',
|
|
138
|
+
'glib-2.0',
|
|
139
|
+
'ffi',
|
|
140
|
+
'cairo',
|
|
141
|
+
'intl',
|
|
142
|
+
];
|
|
130
143
|
// Locate the import-lib dir: GTK_PREFIX/lib, else pkg-config's -L.
|
|
131
144
|
const searchDirs = [];
|
|
132
145
|
if (process.env.GTK_PREFIX) searchDirs.push(join(process.env.GTK_PREFIX, 'lib'));
|
|
@@ -146,10 +159,15 @@ if (mode === '--includes') {
|
|
|
146
159
|
}
|
|
147
160
|
if (found) break;
|
|
148
161
|
}
|
|
162
|
+
// gmodule is optional (girepository may pull it internally). `intl` is too,
|
|
163
|
+
// and for a sharper reason: throwing here BLANKS the whole libraries list, so
|
|
164
|
+
// a gvsbuild that spells its gettext import lib differently would turn one
|
|
165
|
+
// missing library into "nothing links at all". Skipping leaves the linker to
|
|
166
|
+
// name the three undefined gettext symbols, which says what is actually wrong.
|
|
167
|
+
const OPTIONAL = new Set(['gmodule-2.0', 'intl']);
|
|
149
168
|
if (found) resolved.push(found);
|
|
150
|
-
else if (name
|
|
151
|
-
//
|
|
152
|
-
// else is required — surface a missing import lib loudly.
|
|
169
|
+
else if (!OPTIONAL.has(name)) {
|
|
170
|
+
// Everything else is required — surface a missing import lib loudly.
|
|
153
171
|
throw new Error(`win-gi-gyp-flags: import lib for '${name}' not found under ${searchDirs.join(', ')}`);
|
|
154
172
|
}
|
|
155
173
|
}
|
package/src/addon.cc
CHANGED
|
@@ -55,6 +55,15 @@ static void OnEnvShutdown(void* arg) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
58
|
+
// FIRST, before any GLib call: adopt the locale the environment names, the way
|
|
59
|
+
// gjs's entry point does (`setlocale(LC_ALL, "")`, refs/gjs/gjs/console.cpp).
|
|
60
|
+
// This is the seam EVERY node-gi application crosses — `gi://`, `requireGi`, the
|
|
61
|
+
// globals shim and a bare `import '@gjsify/node-gi'` all load the addon — whereas
|
|
62
|
+
// the globals module is injected only when a bundle still references the GJS
|
|
63
|
+
// ambient globals, so a locale set there would miss every program that does not.
|
|
64
|
+
// Without it the process stays in the C locale, where GNU gettext refuses to
|
|
65
|
+
// translate; the block in private.cc records what that cost.
|
|
66
|
+
NodeGiInitProcessLocale();
|
|
58
67
|
// The owner env + its JS/main thread are captured lazily at the first wrap
|
|
59
68
|
// (EnsureDrainAsync) — NOT here, since Init runs once PER env and a per-env
|
|
60
69
|
// overwrite would mis-identify the main thread under worker_threads. The cleanup
|
|
@@ -130,12 +139,20 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
130
139
|
exports.Set("setTemplateCallbackResolver",
|
|
131
140
|
Napi::Function::New(env, SetTemplateCallbackResolver));
|
|
132
141
|
exports.Set("setConstructCallback", Napi::Function::New(env, SetConstructCallback));
|
|
142
|
+
exports.Set("setPropertySetCallback", Napi::Function::New(env, SetPropertySetCallback));
|
|
143
|
+
exports.Set("storedPropertyNames", Napi::Function::New(env, StoredPropertyNames));
|
|
133
144
|
// GjsPrivate-mirroring helpers (private.cc): the structured-log writer func +
|
|
134
145
|
// the bind_property_full / BindingGroup.bind_full transform trampolines.
|
|
135
146
|
exports.Set("logSetWriterFunc", Napi::Function::New(env, LogSetWriterFunc));
|
|
136
147
|
exports.Set("logSetWriterDefault", Napi::Function::New(env, LogSetWriterDefault));
|
|
137
148
|
exports.Set("bindPropertyFull", Napi::Function::New(env, BindPropertyFull));
|
|
138
149
|
exports.Set("bindingGroupBindFull", Napi::Function::New(env, BindingGroupBindFull));
|
|
150
|
+
// The libintl half of GjsPrivate: the binders GLib's GIR does not publish, and
|
|
151
|
+
// the LC_* constants read from this platform's headers.
|
|
152
|
+
exports.Set("setThreadLocale", Napi::Function::New(env, ApplyThreadLocale));
|
|
153
|
+
exports.Set("textdomain", Napi::Function::New(env, Textdomain));
|
|
154
|
+
exports.Set("bindtextdomain", Napi::Function::New(env, Bindtextdomain));
|
|
155
|
+
exports.Set("localeCategories", Napi::Function::New(env, LocaleCategories));
|
|
139
156
|
// The native cairo binding + foreign-struct registration (the `__cairo` export).
|
|
140
157
|
InitCairo(env, exports);
|
|
141
158
|
return exports;
|
package/src/calls.cc
CHANGED
|
@@ -513,6 +513,34 @@ int JsInArgCount(GICallableInfo* callable) {
|
|
|
513
513
|
return count;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
// gjs's Gjs::Function::format_name (refs/gjs/gi/function.cpp:801) — the spelling its
|
|
517
|
+
// argument-count TypeError carries, so a refusal reads identically on both runtimes.
|
|
518
|
+
// Deliberately NOT the `displayName` the other refusals here use: that one names the
|
|
519
|
+
// RUNTIME type the call was made on (`GtkLabel.get_property`), which is the useful
|
|
520
|
+
// thing to say about a marshalling failure but not what gjs prints for arity. gjs
|
|
521
|
+
// names the DECLARING info — `method GObject.Object.get_property` — and consumers
|
|
522
|
+
// byte-compare these.
|
|
523
|
+
static std::string GjsCallableName(GICallableInfo* callable) {
|
|
524
|
+
GIBaseInfo* base = reinterpret_cast<GIBaseInfo*>(callable);
|
|
525
|
+
bool isMethod = gi_callable_info_is_method(callable);
|
|
526
|
+
std::string name = isMethod ? "method " : "function ";
|
|
527
|
+
const char* ns = gi_base_info_get_namespace(base);
|
|
528
|
+
if (ns != nullptr) name += ns;
|
|
529
|
+
name += '.';
|
|
530
|
+
if (isMethod) {
|
|
531
|
+
// Borrowed ref — no unref (same contract as the container read below).
|
|
532
|
+
GIBaseInfo* container = gi_base_info_get_container(base);
|
|
533
|
+
const char* containerName = container != nullptr ? gi_base_info_get_name(container) : nullptr;
|
|
534
|
+
if (containerName != nullptr) {
|
|
535
|
+
name += containerName;
|
|
536
|
+
name += '.';
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const char* fn = gi_base_info_get_name(base);
|
|
540
|
+
if (fn != nullptr) name += fn;
|
|
541
|
+
return name;
|
|
542
|
+
}
|
|
543
|
+
|
|
516
544
|
static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpointer instance,
|
|
517
545
|
Napi::Array args, const std::string& displayName) {
|
|
518
546
|
GICallableInfo* callable = reinterpret_cast<GICallableInfo*>(func);
|
|
@@ -574,6 +602,35 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
574
602
|
std::vector<bool> isLenArg(n_args, false);
|
|
575
603
|
ComputeSkippedArgs(callable, n_args, &skip, &isLenArg);
|
|
576
604
|
|
|
605
|
+
// Too FEW arguments is a REFUSAL — gjs does the same at
|
|
606
|
+
// refs/gjs/gi/function.cpp:891 (JS::CallArgs::reportMoreArgsNeeded). node-gi used
|
|
607
|
+
// to read a missing argument as `undefined` and marshal THAT, which is not leniency
|
|
608
|
+
// but a wrong call: on a GValue parameter `undefined` becomes gjs's null guess, a
|
|
609
|
+
// G_TYPE_POINTER GValue (JsToFreshGValue), so `label.get_property('label')` — one
|
|
610
|
+
// of the two arguments gjs demands — printed "g_object_get_property: can't retrieve
|
|
611
|
+
// property 'label' of type 'gchararray' as value of type 'gpointer'" and evaluated
|
|
612
|
+
// to `undefined`, on stock and custom classes alike, with the set_property twin
|
|
613
|
+
// mirroring it. Counted off the pre-scan JUST computed rather than by calling
|
|
614
|
+
// JsInArgCount (which would redo it on every call, on the hottest path in the
|
|
615
|
+
// binding) — same walk, so what is demanded is by construction what the loop below
|
|
616
|
+
// consumes, and stays equal to the `Function.length` the method reports. Placed
|
|
617
|
+
// here, before the marshalling loop: nothing above has taken ownership of anything,
|
|
618
|
+
// so the early return needs no unwinding. Too MANY stays permitted — gjs only warns
|
|
619
|
+
// there, through a JS warning reporter node-gi has no equivalent of.
|
|
620
|
+
int required = 0;
|
|
621
|
+
for (unsigned int i = 0; i < n_args; i++) {
|
|
622
|
+
if (skip[i]) continue;
|
|
623
|
+
if (dirs[i] == GI_DIRECTION_IN || dirs[i] == GI_DIRECTION_INOUT) required++;
|
|
624
|
+
}
|
|
625
|
+
if (required > 0 && args.Length() < static_cast<uint32_t>(required)) {
|
|
626
|
+
Napi::TypeError::New(env, GjsCallableName(callable) + ": At least " +
|
|
627
|
+
std::to_string(required) + " argument" +
|
|
628
|
+
(required == 1 ? "" : "s") + " required, but only " +
|
|
629
|
+
std::to_string(args.Length()) + " passed")
|
|
630
|
+
.ThrowAsJavaScriptException();
|
|
631
|
+
return env.Undefined();
|
|
632
|
+
}
|
|
633
|
+
|
|
577
634
|
// Caller-allocates OUT storage (fixed C array or boxed struct): blob != nullptr
|
|
578
635
|
// marks the arg; boxedGType != 0 selects the boxed copy/free path on read-back.
|
|
579
636
|
std::vector<gpointer> callerAllocBlob(n_args, nullptr);
|
package/src/class.cc
CHANGED
|
@@ -299,6 +299,17 @@ static GQuark NodeGiInstancePropsQuark() {
|
|
|
299
299
|
static GQuark q = g_quark_from_static_string("node-gi-instance-props");
|
|
300
300
|
return q;
|
|
301
301
|
}
|
|
302
|
+
// The ORDER in which this instance's custom properties were first set. A GHashTable
|
|
303
|
+
// has no order, and the construct-time replay is visible output: gjs runs the JS
|
|
304
|
+
// setters in the order g_object_new applied the values (CONSTRUCT properties, then
|
|
305
|
+
// the rest), so replaying in hash order prints the same setters in a different
|
|
306
|
+
// sequence. Recording what actually happened is cheaper than modelling GObject's
|
|
307
|
+
// application order, and it stays right for paths that do not go through
|
|
308
|
+
// g_object_new at all (GtkBuilder, a binding).
|
|
309
|
+
static GQuark NodeGiInstancePropOrderQuark() {
|
|
310
|
+
static GQuark q = g_quark_from_static_string("node-gi-instance-prop-order");
|
|
311
|
+
return q;
|
|
312
|
+
}
|
|
302
313
|
|
|
303
314
|
static void FreeStoredGValue(gpointer p) {
|
|
304
315
|
GValue* v = static_cast<GValue*>(p);
|
|
@@ -362,6 +373,72 @@ static void NodeGiGetProperty(GObject* obj, guint prop_id, GValue* value, GParam
|
|
|
362
373
|
if (cd != nullptr && cd->parentGet != nullptr) cd->parentGet(obj, prop_id, value, pspec);
|
|
363
374
|
}
|
|
364
375
|
|
|
376
|
+
// Push a just-set custom property through the class's own JS setter, the way gjs's
|
|
377
|
+
// set_property vfunc does (refs/gjs/gi/gobject.cpp gjs_object_set_gproperty →
|
|
378
|
+
// jsobj_set_gproperty → JS_SetProperty on the wrapper).
|
|
379
|
+
//
|
|
380
|
+
// THE GATE IS THE WRAPPER, AND THAT IS THE WHOLE DESIGN — do not replace it with a
|
|
381
|
+
// flag. gjs bails when `priv->wrapper()` is null and so do we, which means a set that
|
|
382
|
+
// lands DURING construction (g_object_new applying construct properties, before
|
|
383
|
+
// node-gi has built the wrapper) has nothing to call into and cannot run a setter
|
|
384
|
+
// early. That is not an accident to tidy up: running a plain READWRITE property's
|
|
385
|
+
// setter at construct time is what crashed Learn6502's SourceView (`selectable` →
|
|
386
|
+
// `_signalHandlers.forEach` against state the ctor body had not created yet). The
|
|
387
|
+
// construct-time values are replayed later, from the base ctor, once the wrapper
|
|
388
|
+
// exists — see gi.js flushPropertiesToJsSetters. A set that lands AFTER construction
|
|
389
|
+
// (GtkBuilder applying a non-construct template property via g_object_set, a
|
|
390
|
+
// property binding, set_property) is the case this branch exists for, and it is the
|
|
391
|
+
// one that left every Learn6502 tutorial code block empty on `--app node`.
|
|
392
|
+
static void MaybeRunJsPropertySetter(NodeGiClassData* cd, GObject* obj, const char* name) {
|
|
393
|
+
if (cd == nullptr || cd->env == nullptr) return;
|
|
394
|
+
napi_env env = cd->env;
|
|
395
|
+
// Never enter JS during GC / env teardown (same gate as the vfunc trampoline and
|
|
396
|
+
// RunJsConstructorForCObject); the property is already stored either way.
|
|
397
|
+
if (!NodeGiJsAvailable(env)) return;
|
|
398
|
+
Napi::Env napiEnv(env);
|
|
399
|
+
Napi::HandleScope scope(napiEnv);
|
|
400
|
+
NodeGiPumpJsDispatchScope pumpWindow;
|
|
401
|
+
|
|
402
|
+
Napi::Value handle = PeekGObjectHandle(napiEnv, obj);
|
|
403
|
+
if (handle.IsEmpty()) return; // construction has not reached the wrap yet
|
|
404
|
+
|
|
405
|
+
NodeGiEnvData* d = EnvData(env);
|
|
406
|
+
napi_value cb = nullptr;
|
|
407
|
+
if (d == nullptr || d->propertySetCallback == nullptr ||
|
|
408
|
+
napi_get_reference_value(env, d->propertySetCallback, &cb) != napi_ok || cb == nullptr) {
|
|
409
|
+
return; // L1 hasn't registered the callback (nothing to run)
|
|
410
|
+
}
|
|
411
|
+
napi_value nameVal = nullptr;
|
|
412
|
+
napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &nameVal);
|
|
413
|
+
napi_value args[2] = {handle, nameVal};
|
|
414
|
+
napi_value undef = nullptr;
|
|
415
|
+
napi_get_undefined(env, &undef);
|
|
416
|
+
napi_value ret = nullptr;
|
|
417
|
+
// A plain call, not napi_make_callback: g_object_set may be running inside GTK's
|
|
418
|
+
// own frame and must not get a microtask checkpoint in the middle of it — the same
|
|
419
|
+
// reasoning as RunJsConstructorForCObject, and gjs's JS_SetProperty takes no
|
|
420
|
+
// checkpoint either.
|
|
421
|
+
if (napi_call_function(env, undef, cb, 2, args, &ret) != napi_ok) {
|
|
422
|
+
// The setter threw. A pending JS exception must never cross back into GObject
|
|
423
|
+
// (g_object_set has no way to report one), so fold it into a g_warning — gjs logs
|
|
424
|
+
// the uncaught exception at the same point (gjs_log_exception_uncaught).
|
|
425
|
+
napi_value ex = nullptr;
|
|
426
|
+
if (napi_get_and_clear_last_exception(env, &ex) == napi_ok && ex != nullptr) {
|
|
427
|
+
napi_value msg = nullptr;
|
|
428
|
+
std::string detail;
|
|
429
|
+
if (napi_get_named_property(env, ex, "message", &msg) == napi_ok) {
|
|
430
|
+
size_t len = 0;
|
|
431
|
+
if (napi_get_value_string_utf8(env, msg, nullptr, 0, &len) == napi_ok) {
|
|
432
|
+
detail.resize(len);
|
|
433
|
+
napi_get_value_string_utf8(env, msg, detail.data(), len + 1, &len);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
g_warning("node-gi: JS setter for %s.%s threw: %s", g_type_name(G_OBJECT_TYPE(obj)), name,
|
|
437
|
+
detail.empty() ? "(no message)" : detail.c_str());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
365
442
|
static void NodeGiSetProperty(GObject* obj, guint prop_id, const GValue* value, GParamSpec* pspec) {
|
|
366
443
|
NodeGiClassData* ownerCd =
|
|
367
444
|
static_cast<NodeGiClassData*>(g_type_get_qdata(pspec->owner_type, NodeGiClassDataQuark()));
|
|
@@ -372,10 +449,26 @@ static void NodeGiSetProperty(GObject* obj, guint prop_id, const GValue* value,
|
|
|
372
449
|
g_object_set_qdata_full(obj, NodeGiInstancePropsQuark(), store,
|
|
373
450
|
reinterpret_cast<GDestroyNotify>(g_hash_table_destroy));
|
|
374
451
|
}
|
|
452
|
+
// Record first-set order alongside the value (see NodeGiInstancePropOrderQuark).
|
|
453
|
+
// This is the ONLY writer of either structure, so they cannot drift.
|
|
454
|
+
if (!g_hash_table_contains(store, pspec->name)) {
|
|
455
|
+
GPtrArray* order =
|
|
456
|
+
static_cast<GPtrArray*>(g_object_get_qdata(obj, NodeGiInstancePropOrderQuark()));
|
|
457
|
+
if (order == nullptr) {
|
|
458
|
+
order = g_ptr_array_new_with_free_func(g_free);
|
|
459
|
+
g_object_set_qdata_full(obj, NodeGiInstancePropOrderQuark(), order,
|
|
460
|
+
reinterpret_cast<GDestroyNotify>(g_ptr_array_unref));
|
|
461
|
+
}
|
|
462
|
+
g_ptr_array_add(order, g_strdup(pspec->name));
|
|
463
|
+
}
|
|
375
464
|
GValue* copy = g_new0(GValue, 1);
|
|
376
465
|
g_value_init(copy, G_VALUE_TYPE(value));
|
|
377
466
|
g_value_copy(value, copy);
|
|
378
467
|
g_hash_table_replace(store, g_strdup(pspec->name), copy);
|
|
468
|
+
// Before the notify, so a ::notify handler observes the JS-side state the setter
|
|
469
|
+
// wrote — GObject emits its own notify after this vfunc returns, which is the
|
|
470
|
+
// order gjs's handlers see.
|
|
471
|
+
MaybeRunJsPropertySetter(ownerCd, obj, pspec->name);
|
|
379
472
|
g_object_notify_by_pspec(obj, pspec);
|
|
380
473
|
return;
|
|
381
474
|
}
|
|
@@ -1502,4 +1595,46 @@ Napi::Value SetConstructCallback(const Napi::CallbackInfo& info) {
|
|
|
1502
1595
|
return env.Undefined();
|
|
1503
1596
|
}
|
|
1504
1597
|
|
|
1598
|
+
// setPropertySetCallback(cb) -> void. L1 registers the callback NodeGiSetProperty
|
|
1599
|
+
// invokes after storing a custom property: (instanceHandle, propertyName) → run the
|
|
1600
|
+
// class's own JS setter (see gi.js runJsPropertySetter). Mirrors
|
|
1601
|
+
// setConstructCallback.
|
|
1602
|
+
Napi::Value SetPropertySetCallback(const Napi::CallbackInfo& info) {
|
|
1603
|
+
Napi::Env env = info.Env();
|
|
1604
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
1605
|
+
Napi::TypeError::New(env, "setPropertySetCallback(cb: function)").ThrowAsJavaScriptException();
|
|
1606
|
+
return env.Undefined();
|
|
1607
|
+
}
|
|
1608
|
+
NodeGiEnvData* d = EnvData(env);
|
|
1609
|
+
if (d == nullptr) return env.Undefined();
|
|
1610
|
+
if (d->propertySetCallback != nullptr) {
|
|
1611
|
+
napi_delete_reference(env, d->propertySetCallback);
|
|
1612
|
+
d->propertySetCallback = nullptr;
|
|
1613
|
+
}
|
|
1614
|
+
napi_create_reference(env, info[0], 1, &d->propertySetCallback);
|
|
1615
|
+
return env.Undefined();
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// storedPropertyNames(handle) -> string[]. The custom properties that have actually
|
|
1619
|
+
// been SET on this instance, in the order they were first set — which is the only
|
|
1620
|
+
// honest record of it, since a property nobody set is absent (NodeGiGetProperty
|
|
1621
|
+
// answers such a read from the ParamSpec default instead). The base ctor reads it to
|
|
1622
|
+
// replay construct-time sets through the JS setters that did not exist yet, and
|
|
1623
|
+
// "actually set" is exactly the discriminator that keeps a declared-but-untouched
|
|
1624
|
+
// property's setter from firing early (the SourceView `selectable` crash). Order is
|
|
1625
|
+
// part of the contract, not a detail: the replay runs those setters, and gjs runs
|
|
1626
|
+
// them in the order g_object_new applied the values.
|
|
1627
|
+
Napi::Value StoredPropertyNames(const Napi::CallbackInfo& info) {
|
|
1628
|
+
Napi::Env env = info.Env();
|
|
1629
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
1630
|
+
if (obj == nullptr) return Napi::Array::New(env, 0);
|
|
1631
|
+
GPtrArray* order =
|
|
1632
|
+
static_cast<GPtrArray*>(g_object_get_qdata(obj, NodeGiInstancePropOrderQuark()));
|
|
1633
|
+
if (order == nullptr) return Napi::Array::New(env, 0);
|
|
1634
|
+
Napi::Array out = Napi::Array::New(env, order->len);
|
|
1635
|
+
for (guint i = 0; i < order->len; i++)
|
|
1636
|
+
out.Set(i, Napi::String::New(env, static_cast<const char*>(g_ptr_array_index(order, i))));
|
|
1637
|
+
return out;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1505
1640
|
} // namespace nodegi
|
package/src/common.h
CHANGED
|
@@ -95,6 +95,12 @@ struct NodeGiEnvData {
|
|
|
95
95
|
// Reflect.construct(class) in adopt mode (see gi.js runCtorForCObject). Per-env
|
|
96
96
|
// for the same reason as errorBuilder (a napi_ref is env-specific).
|
|
97
97
|
napi_ref constructCallback = nullptr;
|
|
98
|
+
// L1 callback that pushes a just-set custom GObject property through the class's
|
|
99
|
+
// own JS setter — invoked by NodeGiSetProperty once the instance HAS a wrapper
|
|
100
|
+
// (handle, propertyName) → see gi.js runJsPropertySetter. gjs reaches the same
|
|
101
|
+
// setter from its set_property vfunc (refs/gjs/gi/gobject.cpp jsobj_set_gproperty).
|
|
102
|
+
// Per-env for the same reason as errorBuilder (a napi_ref is env-specific).
|
|
103
|
+
napi_ref propertySetCallback = nullptr;
|
|
98
104
|
};
|
|
99
105
|
|
|
100
106
|
void NodeGiEnvDataFinalize(napi_env env, void* data, void* hint);
|
|
@@ -458,6 +464,9 @@ void NodeGiToggleDebugLog(const char* fmt, ...) G_GNUC_PRINTF(1, 2);
|
|
|
458
464
|
|
|
459
465
|
Napi::Value MakeGObjectHandle(Napi::Env env, GObject* obj);
|
|
460
466
|
Napi::Value WrapGObject(Napi::Env env, GObject* obj, GITransfer transfer);
|
|
467
|
+
// The wrapper `obj` already has in `env`, or an empty value — never creates one. See
|
|
468
|
+
// toggle.cc; the caller that needs it is the set_property vfunc (class.cc).
|
|
469
|
+
Napi::Value PeekGObjectHandle(Napi::Env env, GObject* obj);
|
|
461
470
|
|
|
462
471
|
extern int g_syncEmitDepth;
|
|
463
472
|
|
|
@@ -665,6 +674,8 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info);
|
|
|
665
674
|
Napi::Value HasClassVfunc(const Napi::CallbackInfo& info);
|
|
666
675
|
Napi::Value CallClassVfunc(const Napi::CallbackInfo& info);
|
|
667
676
|
Napi::Value SetConstructCallback(const Napi::CallbackInfo& info);
|
|
677
|
+
Napi::Value SetPropertySetCallback(const Napi::CallbackInfo& info);
|
|
678
|
+
Napi::Value StoredPropertyNames(const Napi::CallbackInfo& info);
|
|
668
679
|
|
|
669
680
|
// template.cc
|
|
670
681
|
Napi::Value GetTemplateChild(const Napi::CallbackInfo& info);
|
|
@@ -699,6 +710,30 @@ Napi::Value LogSetWriterDefault(const Napi::CallbackInfo& info);
|
|
|
699
710
|
Napi::Value BindPropertyFull(const Napi::CallbackInfo& info);
|
|
700
711
|
Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info);
|
|
701
712
|
|
|
713
|
+
// private.cc — the locale + gettext binders (gjs_set_thread_locale /
|
|
714
|
+
// gjs_textdomain / gjs_bindtextdomain, and the LC_* constants GjsPrivate exports
|
|
715
|
+
// as GjsLocaleCategory). These need C because libintl is not introspectable:
|
|
716
|
+
// GLib publishes the LOOKUP half (g_dgettext/g_dngettext/g_dpgettext2) in its GIR
|
|
717
|
+
// and nothing else, so `bindtextdomain` has no `gi://` spelling on any runtime.
|
|
718
|
+
// NOT named SetThreadLocale, though that is what it mirrors: `<windows.h>` (pulled
|
|
719
|
+
// in by uv.h) defines SetThreadLocale as an object-like macro for the kernel32 API,
|
|
720
|
+
// which rewrote the token at the `Napi::Function::New(env, SetThreadLocale)` call
|
|
721
|
+
// site and failed the MSVC build with C2665 — the same collision the `#undef
|
|
722
|
+
// RegisterClass` above exists for. Renaming the C++ symbol keeps that undef list at
|
|
723
|
+
// one entry; `SetThreadLocaleImpl` is a distinct token and needs nothing.
|
|
724
|
+
Napi::Value ApplyThreadLocale(const Napi::CallbackInfo& info);
|
|
725
|
+
Napi::Value Textdomain(const Napi::CallbackInfo& info);
|
|
726
|
+
Napi::Value Bindtextdomain(const Napi::CallbackInfo& info);
|
|
727
|
+
// The LC_* values THIS platform's <locale.h> defines, as a JS object. Read from
|
|
728
|
+
// the headers rather than written down: the numbers differ per C library
|
|
729
|
+
// (LC_MESSAGES is 5 on glibc, 6 on darwin, and gettext's own 1729 on MSVC, which
|
|
730
|
+
// has no such category), so a literal table is correct on one platform and
|
|
731
|
+
// silently addresses the wrong category on every other one.
|
|
732
|
+
Napi::Value LocaleCategories(const Napi::CallbackInfo& info);
|
|
733
|
+
// Put the process in the locale the environment names — the `setlocale(LC_ALL,
|
|
734
|
+
// "")` gjs's entry point runs before anything else. Idempotent, process-wide.
|
|
735
|
+
void NodeGiInitProcessLocale();
|
|
736
|
+
|
|
702
737
|
// loop.cc
|
|
703
738
|
Napi::Value StartMainLoop(const Napi::CallbackInfo& info);
|
|
704
739
|
Napi::Value IterateMainContext(const Napi::CallbackInfo& info);
|