@gjsify/node-gi 0.60.0 → 0.62.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/gi.js +61 -4
- package/overrides/gio-settings.js +156 -0
- package/package.json +1 -1
- 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-arm64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/calls.cc +15 -0
package/gi.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as native from './index.js';
|
|
|
12
12
|
// NativeScript Android variant).
|
|
13
13
|
import { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites } from '#host';
|
|
14
14
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
15
|
+
import { createGioSettings } from './overrides/gio-settings.js';
|
|
15
16
|
|
|
16
17
|
// The raw native GObject handle on a wrapped instance, so it can be unwrapped again
|
|
17
18
|
// when passed back into the engine as a GI argument.
|
|
@@ -120,7 +121,9 @@ function unwrapProps(props) {
|
|
|
120
121
|
// An enum object's GError-domain descriptor ({ name, quark }), so GLib.Error.matches
|
|
121
122
|
// can resolve an error-enum (Gio.IOErrorEnum) to the domain it represents. Attached by
|
|
122
123
|
// makeEnum when the enum is registered as a GError domain.
|
|
123
|
-
|
|
124
|
+
// A registry symbol, so a host-side stand-in for an enum the typelib lacks (Gtk.DialogError on
|
|
125
|
+
// Android) can declare itself an error domain without importing this module.
|
|
126
|
+
const ERROR_DOMAIN = Symbol.for('nodeGiErrorDomain');
|
|
124
127
|
|
|
125
128
|
// GLib.Error, L1: the engine throws one of these on a failed sync GI invoke (via the
|
|
126
129
|
// builder registered below) and `requireGi('GLib').Error` is this constructor, so a
|
|
@@ -204,6 +207,10 @@ const CLASS_INFO = Symbol('nodeGiClassInfo');
|
|
|
204
207
|
// Gio.File.prototype) reaches the wrapper only through this registry; when two classes
|
|
205
208
|
// promisify the SAME method name it picks the registration whose class the instance is-a.
|
|
206
209
|
const promisifiedMethods = new Map();
|
|
210
|
+
// Wrappers `_promisify` made, so a second `_promisify` of the same name does not take one for an override.
|
|
211
|
+
const promisifiedWrappers = new WeakSet();
|
|
212
|
+
// The introspected method thunks of makeGiMethod: present on a prototype, but never a user override.
|
|
213
|
+
const nativeThunks = new WeakSet();
|
|
207
214
|
|
|
208
215
|
function wrapReturn(value) {
|
|
209
216
|
if (native.isGObjectHandle(value)) return wrapInstance(value);
|
|
@@ -685,9 +692,12 @@ function decorateGioNamespace(baseNs) {
|
|
|
685
692
|
}
|
|
686
693
|
return dbus;
|
|
687
694
|
};
|
|
695
|
+
// Same laziness, for the same reason: patching Gio.Settings touches its prototype.
|
|
696
|
+
let settings;
|
|
688
697
|
return new Proxy(baseNs, {
|
|
689
698
|
get(t, prop) {
|
|
690
699
|
if (prop === '_promisify') return promisify;
|
|
700
|
+
if (prop === 'Settings') return (settings ??= createGioSettings(t));
|
|
691
701
|
if (prop === 'DBus') return getDBus().DBus;
|
|
692
702
|
if (prop === 'DBusProxy') return getDBus().DBusProxy;
|
|
693
703
|
if (prop === 'DBusExportedObject') return getDBus().DBusExportedObject;
|
|
@@ -1624,8 +1634,15 @@ function prototypeMethodHandle(self, where) {
|
|
|
1624
1634
|
// GtkStack's takes 0` was this thunk, not the typelib.
|
|
1625
1635
|
function makeGiMethod(where, method, arity) {
|
|
1626
1636
|
const fn = function (...args) {
|
|
1627
|
-
|
|
1637
|
+
const handle = prototypeMethodHandle(this, where);
|
|
1638
|
+
// Each class prototype materialises its INHERITED methods as its own thunks, so a
|
|
1639
|
+
// `Gio._promisify(Gio.OutputStream.prototype, …)` is not on a FileOutputStream's chain;
|
|
1640
|
+
// the registration is honoured here, where the instance is known.
|
|
1641
|
+
const promisified = promisifiedMethods.size === 0 ? undefined : resolveRegistered(handle, method);
|
|
1642
|
+
if (promisified !== undefined) return promisified.apply(this, args);
|
|
1643
|
+
return wrapReturn(native.callMethod(handle, method, unwrapArgs(args)));
|
|
1628
1644
|
};
|
|
1645
|
+
nativeThunks.add(fn);
|
|
1629
1646
|
Object.defineProperty(fn, 'name', { value: method, configurable: true });
|
|
1630
1647
|
Object.defineProperty(fn, 'length', { value: arity, configurable: true });
|
|
1631
1648
|
return fn;
|
|
@@ -1984,9 +2001,28 @@ function deriveFinishName(asyncName) {
|
|
|
1984
2001
|
|
|
1985
2002
|
// Build the promisified wrapper for `asyncName`. Uses `this` (the instance proxy)
|
|
1986
2003
|
// so it works both bound from the registry and as a prototype method.
|
|
1987
|
-
|
|
2004
|
+
//
|
|
2005
|
+
// `override` is a plain JS function already installed on the prototype under `asyncName`
|
|
2006
|
+
// (a test double, a subclass hook): GJS's _promisify wraps whatever the prototype holds at
|
|
2007
|
+
// that moment, so the wrapper drives the override instead of the native method.
|
|
2008
|
+
function makePromisified(asyncName, finishName, override = undefined) {
|
|
1988
2009
|
const giAsync = camelToSnake(asyncName);
|
|
1989
2010
|
return function promisified(...args) {
|
|
2011
|
+
if (override !== undefined) {
|
|
2012
|
+
if (args.length > 0 && typeof args[args.length - 1] === 'function') return override.apply(this, args);
|
|
2013
|
+
const self = this;
|
|
2014
|
+
return new Promise((resolve, reject) => {
|
|
2015
|
+
override.call(self, ...args, (source, res) => {
|
|
2016
|
+
try {
|
|
2017
|
+
const value = (source ?? self)[finishName](res);
|
|
2018
|
+
if (Array.isArray(value) && value.length > 1 && value[0] === true) value.shift();
|
|
2019
|
+
resolve(value);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
reject(error);
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
1990
2026
|
const handle = this[HANDLE];
|
|
1991
2027
|
// A trailing callback → plain async method (the caller drives the callback).
|
|
1992
2028
|
// Wrap it so its (source, result) args are marshalled to wrapped instances —
|
|
@@ -2044,7 +2080,16 @@ function promisify(proto, asyncName, finishName = undefined) {
|
|
|
2044
2080
|
throw new TypeError('Gio._promisify: asyncName must be a string');
|
|
2045
2081
|
}
|
|
2046
2082
|
const finish = finishName === undefined ? deriveFinishName(asyncName) : finishName;
|
|
2047
|
-
|
|
2083
|
+
// GJS refuses a missing method at the call, not at first use.
|
|
2084
|
+
if (typeof proto[asyncName] !== 'function') throw new Error(`${proto} has no method named ${asyncName}`);
|
|
2085
|
+
if (typeof proto[finish] !== 'function') throw new Error(`${proto} has no method named ${finish}`);
|
|
2086
|
+
const present = Object.getOwnPropertyDescriptor(proto, asyncName)?.value;
|
|
2087
|
+
const override =
|
|
2088
|
+
typeof present === 'function' && !promisifiedWrappers.has(present) && !nativeThunks.has(present)
|
|
2089
|
+
? present
|
|
2090
|
+
: undefined;
|
|
2091
|
+
const wrapper = makePromisified(asyncName, finish, override);
|
|
2092
|
+
promisifiedWrappers.add(wrapper);
|
|
2048
2093
|
// Install on the prototype (so proto[asyncName] IS the promisified method, and a
|
|
2049
2094
|
// registerClass subclass — which resolves its userProto — picks it up) AND in
|
|
2050
2095
|
// the global registry (introspected instances resolve methods dynamically, with
|
|
@@ -2077,6 +2122,18 @@ function promisify(proto, asyncName, finishName = undefined) {
|
|
|
2077
2122
|
}
|
|
2078
2123
|
}
|
|
2079
2124
|
|
|
2125
|
+
// The registered wrapper for `methodName` only when the instance IS-A the class it was registered
|
|
2126
|
+
// on; unlike resolvePromisified there is no fallback, because a native thunk is already the
|
|
2127
|
+
// right call for every other class.
|
|
2128
|
+
function resolveRegistered(handle, methodName) {
|
|
2129
|
+
const registrations = promisifiedMethods.get(methodName);
|
|
2130
|
+
if (registrations === undefined) return undefined;
|
|
2131
|
+
for (const r of registrations) {
|
|
2132
|
+
if (r.namespace !== undefined && native.isInstanceOf(handle, r.namespace, r.typeName)) return r.wrapper;
|
|
2133
|
+
}
|
|
2134
|
+
return undefined;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2080
2137
|
// Resolve the promisified wrapper for `methodName` on an instance handle. The
|
|
2081
2138
|
// common case is a single registration (fast path); when several classes
|
|
2082
2139
|
// promisified the same method name, pick the one whose class the instance is-a.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
|
|
2
|
+
//
|
|
3
|
+
// Adapted from GJS (refs/gjs/modules/core/overrides/Gio.js, "Override Gio.Settings and
|
|
4
|
+
// Gio.SettingsSchema"). Copyright (c) GJS contributors. MIT OR LGPL-2.0-or-later.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: the C API of GSettings does not fail, it ABORTS. An unknown schema id or an
|
|
7
|
+
// unknown key is a `g_error` (G_LOG_LEVEL_ERROR, fatal), so on a bare binding one typo kills the
|
|
8
|
+
// process with no catchable error — measured on node-gi, GLib 2.88: `new Gio.Settings({schema_id:
|
|
9
|
+
// 'no.such'})` and `s.get_int('nope')` both exit the process. GJS wraps both in a JS check that
|
|
10
|
+
// throws; this is the same wrapper, so `try { … } catch` means the same on either runtime
|
|
11
|
+
// (ADR 0099 conformance row `errors`).
|
|
12
|
+
//
|
|
13
|
+
// Two deliberate differences from the GJS override:
|
|
14
|
+
// • the constructor check is a Proxy `construct` trap — a node-gi class has no `_init` to wrap;
|
|
15
|
+
// • `set_int` / `set_uint` range-check their value, which in GJS is the argument marshaller's
|
|
16
|
+
// job. node-gi's marshaller wraps modulo 2^32 (`set_uint('k', -1)` stores 4294967295), a
|
|
17
|
+
// general divergence recorded in status/open-todos/node-gi.md; this keeps the settings
|
|
18
|
+
// surface honest without touching the addon, so it also holds in an already-built payload.
|
|
19
|
+
|
|
20
|
+
const INT32_MIN = -(2 ** 31);
|
|
21
|
+
const INT32_MAX = 2 ** 31 - 1;
|
|
22
|
+
const UINT32_MAX = 2 ** 32 - 1;
|
|
23
|
+
|
|
24
|
+
const CHECKED = [
|
|
25
|
+
'get_boolean',
|
|
26
|
+
'set_boolean',
|
|
27
|
+
'get_double',
|
|
28
|
+
'set_double',
|
|
29
|
+
'get_enum',
|
|
30
|
+
'set_enum',
|
|
31
|
+
'get_flags',
|
|
32
|
+
'set_flags',
|
|
33
|
+
'get_int',
|
|
34
|
+
'set_int',
|
|
35
|
+
'get_int64',
|
|
36
|
+
'set_int64',
|
|
37
|
+
'get_string',
|
|
38
|
+
'set_string',
|
|
39
|
+
'get_strv',
|
|
40
|
+
'set_strv',
|
|
41
|
+
'get_uint',
|
|
42
|
+
'set_uint',
|
|
43
|
+
'get_uint64',
|
|
44
|
+
'set_uint64',
|
|
45
|
+
'get_value',
|
|
46
|
+
'set_value',
|
|
47
|
+
'bind',
|
|
48
|
+
'bind_writable',
|
|
49
|
+
'create_action',
|
|
50
|
+
'get_default_value',
|
|
51
|
+
'get_user_value',
|
|
52
|
+
'is_writable',
|
|
53
|
+
'reset',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** The range a setter's JS number must be in; GJS's marshaller throws this exact message. */
|
|
57
|
+
const RANGES = {
|
|
58
|
+
set_int: [INT32_MIN, INT32_MAX, 'int32'],
|
|
59
|
+
set_uint: [0, UINT32_MAX, 'uint32'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Patch `Gio.Settings` / `Gio.SettingsSchema` like GJS does, once, and return the class to hand
|
|
64
|
+
* out (a Proxy whose `new` refuses a schema that is not installed instead of aborting).
|
|
65
|
+
* @param {{ Settings: any, SettingsSchema: any, SettingsSchemaSource: any }} Gio the introspected namespace
|
|
66
|
+
*/
|
|
67
|
+
export function createGioSettings(Gio) {
|
|
68
|
+
const Settings = Gio.Settings;
|
|
69
|
+
const schemaProto = Gio.SettingsSchema.prototype;
|
|
70
|
+
const proto = Settings.prototype;
|
|
71
|
+
|
|
72
|
+
const realGetKey = schemaProto.get_key;
|
|
73
|
+
schemaProto.get_key = function get_key(key) {
|
|
74
|
+
if (!this.has_key(key)) throw new Error(`GSettings key ${key} not found in schema ${this.get_id()}`);
|
|
75
|
+
return realGetKey.call(this, key);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The key and child lists are per instance and a schema's keys never change, so one read each.
|
|
79
|
+
const keysOf = new WeakMap();
|
|
80
|
+
const childrenOf = new WeakMap();
|
|
81
|
+
const checkKey = (self, key) => {
|
|
82
|
+
let keys = keysOf.get(self);
|
|
83
|
+
if (keys === undefined) keysOf.set(self, (keys = self.settings_schema.list_keys()));
|
|
84
|
+
if (!keys.includes(key)) throw new Error(`GSettings key ${key} not found in schema ${self.schema_id}`);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const name of CHECKED) {
|
|
88
|
+
const real = proto[name];
|
|
89
|
+
if (typeof real !== 'function') continue;
|
|
90
|
+
const range = RANGES[name];
|
|
91
|
+
proto[name] = function (key, ...args) {
|
|
92
|
+
checkKey(this, key);
|
|
93
|
+
if (range !== undefined) {
|
|
94
|
+
const [min, max, type] = range;
|
|
95
|
+
const value = args[0];
|
|
96
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
97
|
+
throw new Error(`Argument value: value is out of range for ${type}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return real.call(this, key, ...args);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const realGetChild = proto.get_child;
|
|
105
|
+
proto.get_child = function get_child(name) {
|
|
106
|
+
let children = childrenOf.get(this);
|
|
107
|
+
if (children === undefined) childrenOf.set(this, (children = this.list_children()));
|
|
108
|
+
if (!children.includes(name)) {
|
|
109
|
+
throw new Error(`Child ${name} not found in GSettings schema ${this.schema_id}`);
|
|
110
|
+
}
|
|
111
|
+
return realGetChild.call(this, name);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// A boxed instance is a Proxy over a handle with no prototype link to its class, so
|
|
115
|
+
// `instanceof Gio.SettingsSchema` is false for the very schema `lookup` returned. Recognise it
|
|
116
|
+
// by the methods the constructor needs instead.
|
|
117
|
+
const isSchema = (value) =>
|
|
118
|
+
value instanceof Gio.SettingsSchema ||
|
|
119
|
+
(value !== null &&
|
|
120
|
+
typeof value === 'object' &&
|
|
121
|
+
typeof value.get_id === 'function' &&
|
|
122
|
+
typeof value.list_keys === 'function');
|
|
123
|
+
|
|
124
|
+
return new Proxy(Settings, {
|
|
125
|
+
// Read with the class itself as receiver: node-gi's class Proxy hides `$gtype` from any
|
|
126
|
+
// other receiver (so a subclass does not inherit its base's), which would include this one.
|
|
127
|
+
get(target, prop) {
|
|
128
|
+
return Reflect.get(target, prop);
|
|
129
|
+
},
|
|
130
|
+
construct(target, args) {
|
|
131
|
+
const props = args[0] ?? {};
|
|
132
|
+
const schemaIdProp = ['schema', 'schema-id', 'schema_id', 'schemaId'].find((p) => p in props);
|
|
133
|
+
const schemaProp = ['settings-schema', 'settings_schema', 'settingsSchema'].find((p) => p in props);
|
|
134
|
+
if (!schemaIdProp && !schemaProp) {
|
|
135
|
+
throw new Error("One of property 'schema-id' or 'settings-schema' are required for Gio.Settings");
|
|
136
|
+
}
|
|
137
|
+
if (schemaProp && !isSchema(props[schemaProp])) {
|
|
138
|
+
throw new Error(`Value of property '${schemaProp}' is not of type Gio.SettingsSchema`);
|
|
139
|
+
}
|
|
140
|
+
const schema = schemaProp
|
|
141
|
+
? props[schemaProp]
|
|
142
|
+
: Gio.SettingsSchemaSource.get_default()?.lookup(props[schemaIdProp], true);
|
|
143
|
+
if (!schema) throw new Error(`GSettings schema ${props[schemaIdProp]} not found`);
|
|
144
|
+
const schemaPath = schema.get_path();
|
|
145
|
+
if (props.path === undefined && !schemaPath) {
|
|
146
|
+
throw new Error(`Attempting to create schema '${schema.get_id()}' without a path`);
|
|
147
|
+
}
|
|
148
|
+
if (props.path !== undefined && schemaPath && props.path !== schemaPath) {
|
|
149
|
+
throw new Error(`GSettings created for path '${props.path}', but schema specifies '${schemaPath}'`);
|
|
150
|
+
}
|
|
151
|
+
// newTarget stays the real class: a node-gi class is an exotic constructor, and the
|
|
152
|
+
// Proxy must not become the `new.target` its wrapper builds the instance from.
|
|
153
|
+
return Reflect.construct(target, args);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/calls.cc
CHANGED
|
@@ -46,6 +46,10 @@ struct NodeGiCallback {
|
|
|
46
46
|
// immortal: GDK leaves a ~1 s repeating timeout armed, so the context reports
|
|
47
47
|
// work forever while nothing is ever ready to dispatch.
|
|
48
48
|
bool pumpCounted = false;
|
|
49
|
+
// IN containers (typed-array `contents`, string arrays) of the call that armed a scope=async
|
|
50
|
+
// callback. The callee reads them until the operation completes, so they outlive the invoke and
|
|
51
|
+
// are released with the callback — `g_file_replace_contents_async` wrote freed memory otherwise.
|
|
52
|
+
std::vector<InContainer> heldContainers;
|
|
49
53
|
};
|
|
50
54
|
|
|
51
55
|
static void NodeGiCallbackPumpRelease(NodeGiCallback* cb) {
|
|
@@ -58,6 +62,7 @@ static void NodeGiCallbackPumpRelease(NodeGiCallback* cb) {
|
|
|
58
62
|
static void NodeGiCallbackFree(NodeGiCallback* cb) {
|
|
59
63
|
if (cb == nullptr) return;
|
|
60
64
|
NodeGiCallbackPumpRelease(cb); // never fired (error/teardown path)
|
|
65
|
+
for (const InContainer& c : cb->heldContainers) FreeInContainer(c);
|
|
61
66
|
if (cb->jsFn != nullptr) napi_delete_reference(cb->env, cb->jsFn);
|
|
62
67
|
if (cb->closure != nullptr) gi_callable_info_destroy_closure(cb->info, cb->closure);
|
|
63
68
|
if (cb->info != nullptr) gi_base_info_unref(cb->info);
|
|
@@ -965,6 +970,16 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
965
970
|
if (!success) {
|
|
966
971
|
for (NodeGiCallback* cb : created) NodeGiCallbackPumpRelease(cb);
|
|
967
972
|
}
|
|
973
|
+
// The IN containers of an async call stay alive until its callback is released.
|
|
974
|
+
if (success) {
|
|
975
|
+
for (NodeGiCallback* cb : created) {
|
|
976
|
+
if (cb->scope == GI_SCOPE_TYPE_ASYNC) {
|
|
977
|
+
cb->heldContainers = std::move(inContainers);
|
|
978
|
+
inContainers.clear();
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
968
983
|
// Call-scope closures are only valid for the duration of the invoke; free them
|
|
969
984
|
// now (notified/async closures are owned by the callee / self-freeing).
|
|
970
985
|
for (NodeGiCallback* cb : callScope) NodeGiCallbackFree(cb);
|