@gjsify/node-gi 0.61.0 → 0.63.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 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
- const ERROR_DOMAIN = Symbol('nodeGiErrorDomain');
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
- return wrapReturn(native.callMethod(prototypeMethodHandle(this, where), method, unwrapArgs(args)));
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
- function makePromisified(asyncName, finishName) {
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
- const wrapper = makePromisified(asyncName, finish);
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.61.0",
3
+ "version": "0.63.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);