@gjsify/node-gi 0.63.0 → 0.64.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
@@ -118,82 +118,265 @@ function unwrapProps(props) {
118
118
  return out;
119
119
  }
120
120
 
121
- // An enum object's GError-domain descriptor ({ name, quark }), so GLib.Error.matches
122
- // can resolve an error-enum (Gio.IOErrorEnum) to the domain it represents. Attached by
123
- // makeEnum when the enum is registered as a GError domain.
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');
127
-
128
- // GLib.Error, L1: the engine throws one of these on a failed sync GI invoke (via the
129
- // builder registered below) and `requireGi('GLib').Error` is this constructor, so a
130
- // caught error is `instanceof GLib.Error` exactly as under GJS.
121
+ // ---- GLib.Error (L1) ----
131
122
  //
132
- // Deliberate divergence from GJS: `.domain` is the quark NAME string (GJS's is the
133
- // numeric GQuark), with the numeric quark on `.domainQuark`. `matches()` accepts the
134
- // domain as an error-enum object, a name string or a numeric quark.
135
-
136
- // Resolve a `matches` domain argument to a { name?, quark? } descriptor.
137
- function errorDomainOf(domain) {
138
- if (domain === null || domain === undefined) return null;
139
- if (typeof domain === 'object' && domain[ERROR_DOMAIN] !== undefined) return domain[ERROR_DOMAIN];
140
- if (typeof domain === 'string') return { name: domain };
141
- if (typeof domain === 'number') return { quark: domain };
142
- return null;
143
- }
144
-
145
- class GLibError extends Error {
146
- // `new GLib.Error(domain, code, message)`, where `domain` may be an error-enum
147
- // object, a quark name string, or a numeric quark.
148
- constructor(domain, code, message) {
149
- super(typeof message === 'string' ? message : '');
150
- this.name = 'GLib.Error';
151
- const d = errorDomainOf(domain);
152
- this.domain = d !== null && d.name !== undefined ? d.name : typeof domain === 'string' ? domain : undefined;
153
- this.domainQuark =
154
- d !== null && d.quark !== undefined ? d.quark : typeof domain === 'number' ? domain : undefined;
155
- this.code = code;
123
+ // ONE object for every GError, as on GJS: a boxed handle over a real `GError*` whose
124
+ // CLASS is the error DOMAIN's class (`Gio.IOErrorEnum`) when the quark names a
125
+ // registered domain, and `GLib.Error` otherwise. There is no JS `Error` subclass
126
+ // behind it — `Error.isError()` is true while `instanceof Error` is FALSE, a
127
+ // combination only `Reflect.construct(Error, [], Ctor)` reaches (GJS gets it from
128
+ // `JSCLASS_IS_DOMJSCLASS` on ErrorBase::klass).
129
+ //
130
+ // Reference: refs/gjs/gi/gerror.cpp (ErrorBase/ErrorInstance/ErrorPrototype) and its
131
+ // `gjs_define_error_properties`, plus the GLib.js override's `Error.new_literal`
132
+ // guard and `Error.prototype.matches`. The old divergence — an `Error` subclass with
133
+ // `.domain` the quark NAME and the number on a node-gi-only `.domainQuark`, beside a
134
+ // SECOND plain-boxed shape for a marshalled one — is gone with no compat shim;
135
+ // test/gerror-identity.test.mjs is the contract.
136
+
137
+ // quark → the class makeErrorDomainClass built for it. A cached `null` means "asked
138
+ // the repository, this quark has no domain class".
139
+ const errorClassByQuark = new Map();
140
+
141
+ // GJS defines the error methods enumerable and non-configurable, which is observable:
142
+ // a formatter listing a prototype's methods sees them, and nothing can redefine them.
143
+ const errorMethod = (value) => ({ value, enumerable: true, writable: true, configurable: false });
144
+
145
+ // The class for a GError's domain quark, or null when no typelib registers one (a
146
+ // private quark from `GLib.quark_from_string`), in which case the error is a plain
147
+ // `GLib.Error`.
148
+ function errorClassForQuark(quark) {
149
+ const cached = errorClassByQuark.get(quark);
150
+ if (cached !== undefined) return cached;
151
+ // Nothing has touched the enum yet, so the map cannot know it: ask the repository
152
+ // which { namespace, name } registered the domain and READ that member — makeEnum
153
+ // registers the class it builds on the way out. Without this an error whose domain
154
+ // enum the application never named would fall back to the base class, which is
155
+ // exactly the `catch (e) { if (e instanceof Gio.IOErrorEnum) }` case.
156
+ // Guarded for the cross-version pairing the `native.setPropertySetCallback` guard
157
+ // below describes: an addon older than this JS does not export it. Degrading means
158
+ // falling back to what the map alone knows — a domain whose enum `makeEnum` has
159
+ // already realized is in it, one nothing has touched yet lands on the base class.
160
+ // Nothing is cached in that case, so a later `makeEnum` still registers.
161
+ if (typeof native.errorDomainInfo !== 'function') return null;
162
+ const info = native.errorDomainInfo(quark);
163
+ errorClassByQuark.set(quark, null); // also breaks the cycle while the namespace loads
164
+ if (info === null) return null;
165
+ // Namespace MEMBERS are lazy (createNamespace resolves on first read), so the
166
+ // namespace object alone builds nothing — the member read is what runs makeEnum.
167
+ // oxlint-disable-next-line no-unused-expressions -- the READ is the side effect: it runs makeEnum, which registers the class
168
+ requireGi(info.namespace)[info.name];
169
+ return errorClassByQuark.get(quark);
170
+ }
171
+
172
+ // The first parseable `file:line:column` in a V8 stack. The FRAMES stay V8's — only
173
+ // their position is lifted out, and the frame it lands on is node-gi's own dispatch
174
+ // site for a marshalled error (see docs/node-gi-gjs-surface.md).
175
+ function topFrame(stack) {
176
+ for (const line of String(stack).split('\n')) {
177
+ const match = /\(?([^()\s]+):(\d+):(\d+)\)?$/.exec(line.trim());
178
+ if (match !== null) {
179
+ return { fileName: match[1], lineNumber: Number(match[2]), columnNumber: Number(match[3]) };
180
+ }
156
181
  }
157
-
158
- // g_error_new_literal, which GJS exposes as a static on GLib.Error because it is in
159
- // `GLib-2.0.gir` — and which is the only spelling `@girs/glib-2.0` TYPES, its
160
- // `constructor(properties?)` signature rejecting the three positional arguments the
161
- // constructor above actually takes. So a cross-runtime source that has to build a GError
162
- // (a test asserting on an error a host cannot produce, say a CoreText font map's
163
- // G_IO_ERROR_NOT_SUPPORTED) type-checks only against this, and without it the gjs leg
164
- // passes and the node leg dies with `Error.new_literal is not a function`. Same arguments,
165
- // same result — the constructor is the implementation.
166
- static new_literal(domain, code, message) {
167
- return new GLibError(domain, code, message);
182
+ return { fileName: undefined, lineNumber: undefined, columnNumber: undefined };
183
+ }
184
+
185
+ // GJS's `gjs_define_error_properties`: four own ENUMERABLE value properties saying
186
+ // where the error came from. Enumerable is the load-bearing half — V8's own `stack`
187
+ // is not, so a `{...err}` spread or a structured log carried the frames on GJS and
188
+ // nothing here.
189
+ function definePositionProperties(error, hideAbove) {
190
+ Error.captureStackTrace(error, hideAbove);
191
+ const stack = error.stack;
192
+ const frame = topFrame(stack);
193
+ for (const [name, value] of [
194
+ ['stack', stack],
195
+ ['fileName', frame.fileName],
196
+ ['lineNumber', frame.lineNumber],
197
+ ['columnNumber', frame.columnNumber],
198
+ ]) {
199
+ Object.defineProperty(error, name, { value, enumerable: true, writable: true, configurable: true });
168
200
  }
201
+ }
202
+
203
+ // Wrap a `GError*` handle as the error object. `withPosition` is GJS's asymmetry: the
204
+ // position properties are defined when the error was THROWN or CONSTRUCTED, and
205
+ // otherwise only for an UNKNOWN domain — because GJS then has no domain metadata to
206
+ // build a class from and reaches the GError through its generic struct wrapper, which
207
+ // defines them (gi/struct.cpp). `hideAbove` is the frame the position should be
208
+ // measured at, so `new GLib.Error(…)` reports the caller and not this file.
209
+ function wrapGError(handle, withPosition = false, hideAbove = wrapGError) {
210
+ const cls = errorClassForQuark(native.getBoxedField(handle, 'domain'));
211
+ const error = Reflect.construct(Error, [], cls ?? GLibError);
212
+ error[HANDLE] = handle;
213
+ if (withPosition || cls === null) definePositionProperties(error, hideAbove);
214
+ // `Reflect.construct(Error, …)` always leaves V8's own `stack` behind, and GJS has
215
+ // none on this path at all.
216
+ else delete error.stack;
217
+ return error;
218
+ }
169
219
 
170
- // g_error_matches: true when the error's domain AND code both match.
171
- matches(domain, code) {
172
- const d = errorDomainOf(domain);
173
- if (d === null) return false;
174
- const domainMatch =
175
- (d.name !== undefined && d.name === this.domain) || (d.quark !== undefined && d.quark === this.domainQuark);
176
- return domainMatch && code === this.code;
220
+ // g_error_new_literal, behind the quark guard GJS's GLib.js puts in front of it: the
221
+ // domain must already name something, so `new GLib.Error()` is refused here rather
222
+ // than producing a GError no `matches()` can ever hit.
223
+ function newErrorLiteral(domain, code, message) {
224
+ const quark = toDomainQuark(domain);
225
+ if (native.callFunction('GLib', 'quark_to_string', [quark]) === null) {
226
+ throw new TypeError(`Error.new_literal: ${domain} is not a valid domain`);
177
227
  }
228
+ return native.callStaticMethod('GLib', 'Error', 'new_literal', [quark, code, message]);
229
+ }
230
+
231
+ // GJS's uint32 conversion for a `domain` argument (gi/js-value-inl.h,
232
+ // js_value_to_c_checked): `undefined` and a non-finite NUMBER become 0 silently,
233
+ // anything else goes through ToNumber and is REFUSED when it misses uint32 range.
234
+ // node-gi's own NodeGiToUint32 has no range check, so the node-gi-only spelling
235
+ // `err.matches('g-io-error-quark', code)` converted to NaN→0 and quietly matched
236
+ // nothing; GJS cannot run that line at all, so neither can this.
237
+ function toDomainQuark(domain) {
238
+ if (domain === undefined || (typeof domain === 'number' && !Number.isFinite(domain))) return 0;
239
+ const quark = Number(domain);
240
+ if (Number.isNaN(quark) || quark < 0 || quark > 0xffffffff) {
241
+ throw new Error('Argument domain: value is out of range for uint32');
242
+ }
243
+ return quark >>> 0;
244
+ }
178
245
 
179
- toString() {
180
- return `GLib.Error ${this.domain}: ${this.message}`;
246
+ // `GLib.Error`: a plain FUNCTION, not a `class … extends Error` — GJS's
247
+ // GLib.Error.prototype has Object.prototype on its chain, not Error.prototype.
248
+ function GLibError(domain, code, message) {
249
+ // The spelling `@girs/glib-2.0`'s `constructor(properties?)` signature invites,
250
+ // and which GJS runs into its generic boxed-struct constructor with: a GError
251
+ // field is not writable. Replicated verbatim so the failure reads the same.
252
+ if (arguments.length === 1 && typeof domain === 'object' && domain !== null) {
253
+ throw new Error('Writing field GLib.Error.message is not supported');
181
254
  }
255
+ return wrapGError(newErrorLiteral(domain, code, message), true, GLibError);
182
256
  }
257
+ Object.defineProperty(GLibError, 'name', { value: 'GLib_Error', configurable: true });
258
+ defineLazyGType(GLibError, 'GLib', 'Error');
183
259
 
184
- // Called by the engine on a failed GI invoke, with the GError's authoritative fields.
185
- function buildGError(domainName, domainQuark, code, message) {
186
- const error = new GLibError(domainName, code, message);
187
- error.domain = domainName;
188
- error.domainQuark = domainQuark;
189
- return error;
260
+ // The static GJS exposes because `g_error_new_literal` is in GLib-2.0.gir — and the
261
+ // only spelling `@girs/glib-2.0` TYPES, so a cross-runtime source that has to build a
262
+ // GError type-checks against this rather than the three positional constructor args.
263
+ GLibError.new_literal = function new_literal(domain, code, message) {
264
+ return wrapGError(newErrorLiteral(domain, code, message));
265
+ };
266
+
267
+ // `domain`/`code`/`message` read straight off the `GError*` through a PROTOTYPE
268
+ // accessor, as GJS does: getter-only (a consumer "normalising" an error by assigning
269
+ // `.domain` is a TypeError, not a silent mutation) and own to each prototype, so the
270
+ // refusal below can name the right one.
271
+ function defineErrorFields(proto, displayName, protoQuark) {
272
+ for (const field of ['domain', 'code', 'message']) {
273
+ Object.defineProperty(proto, field, {
274
+ enumerable: true,
275
+ configurable: false,
276
+ get() {
277
+ const handle = this[HANDLE];
278
+ if (handle !== undefined) return native.getBoxedField(handle, field);
279
+ // A PROTOTYPE has no GError* behind it, and refusing is what keeps a
280
+ // prototype walk (util.inspect, a formatter collecting accessors) from
281
+ // reading a wild pointer. GJS still answers a DOMAIN prototype's
282
+ // `domain` — that is where `matches(Gio.IOErrorEnum, …)` reads the
283
+ // quark from — and refuses the other two.
284
+ if (field === 'domain' && protoQuark !== undefined) return protoQuark;
285
+ throw new Error(`Can't get a field on ${displayName}.prototype; only on instances`);
286
+ },
287
+ });
288
+ }
190
289
  }
191
- // The CLASS is registered with the builder, because the engine needs it the other
192
- // way round too: a `GError`-typed IN argument recognises a caught (or `new`-ed)
193
- // GLib.Error by `instanceof` and rebuilds a real GError from its fields, so an
194
- // application can hand back the error it is holding (`GLib.propagate_error`,
195
- // `Gst.Message.new_error`). One call, so builder and class cannot drift apart.
196
- native.setErrorBuilder(buildGError, GLibError);
290
+
291
+ Object.defineProperties(GLibError.prototype, {
292
+ copy: errorMethod(function copy() {
293
+ return wrapGError(native.callBoxedMethod(this[HANDLE], 'copy', []));
294
+ }),
295
+ // g_error_free, for parity. It carries the same double-free footgun as on GJS: the
296
+ // handle still owns the GError* and frees it again on GC.
297
+ free: errorMethod(function free() {
298
+ return native.callBoxedMethod(this[HANDLE], 'free', []);
299
+ }),
300
+ matches: errorMethod(function matches(domain, code) {
301
+ if (arguments.length < 2) {
302
+ throw new TypeError(
303
+ `method GLib.Error.matches: At least 2 arguments required, but only ${arguments.length} passed`,
304
+ );
305
+ }
306
+ return native.callBoxedMethod(this[HANDLE], 'matches', [toDomainQuark(domain), code]);
307
+ }),
308
+ });
309
+ defineErrorFields(GLibError.prototype, 'GLib.Error', undefined);
310
+ // Defined SEPARATELY here and on each domain prototype, which is why on GJS
311
+ // `err.toString !== GLib.Error.prototype.toString` for a known domain: there is no
312
+ // sentinel branch, the class that knows its name owns the method.
313
+ Object.defineProperty(
314
+ GLibError.prototype,
315
+ 'toString',
316
+ errorMethod(function toString() {
317
+ return `GLib.Error ${native.callFunction('GLib', 'quark_to_string', [this.domain])}: ${this.message}`;
318
+ }),
319
+ );
320
+
321
+ // An error-domain enum (`Gio.IOErrorEnum`) is BOTH the enum and the CLASS of its
322
+ // errors on GJS — `instanceof` discriminates the domain and `new Gio.IOErrorEnum({
323
+ // message, code })` builds one — so it has to be a function with a prototype chained
324
+ // to GLib.Error.prototype. A frozen object, which is what makeEnum hands out
325
+ // otherwise, can be neither.
326
+ function makeErrorDomainClass(namespace, typeName, members, quark) {
327
+ const displayName = `${namespace}.${typeName}`;
328
+ const cls = function (props) {
329
+ // The wording is the contract — a cross-runtime test asserting on a
330
+ // constructor refusal compares the string — and `message` is checked BEFORE
331
+ // `code`, so a call naming neither names `message`.
332
+ if (arguments.length !== 1 || typeof props !== 'object' || props === null) {
333
+ throw new Error('Invalid parameters passed to GError constructor, expected one object');
334
+ }
335
+ if (typeof props.message !== 'string') {
336
+ throw new Error("No property 'message' in GError constructor (or it was not a string)");
337
+ }
338
+ if (!Number.isInteger(props.code) || props.code < -0x80000000 || props.code > 0x7fffffff) {
339
+ throw new Error("No property 'code' in GError constructor (or it was not a 32-bit integer)");
340
+ }
341
+ return wrapGError(newErrorLiteral(quark, props.code, props.message), true, cls);
342
+ };
343
+ Object.defineProperty(cls, 'name', { value: `${namespace}_${typeName}`, configurable: true });
344
+ Object.assign(cls, members);
345
+ // G_TYPE_ERROR, NOT the enum's own GIOErrorEnum: on GJS an error domain is an
346
+ // Error class before it is an enum (ErrorBase::to_gtype), and the enum GType it
347
+ // shadows is unreachable there, so nothing written for GJS can ask for it.
348
+ defineLazyGType(cls, 'GLib', 'Error');
349
+ // A STATIC on GJS (ErrorBase::value_of reads `this.constructor.prototype`'s
350
+ // domain), and what makes both `Number(Gio.IOErrorEnum)` and
351
+ // `err.matches(Gio.IOErrorEnum, code)` answer the quark with no registry lookup.
352
+ cls.valueOf = () => quark;
353
+ Object.setPrototypeOf(cls.prototype, GLibError.prototype);
354
+ defineErrorFields(cls.prototype, displayName, quark);
355
+ Object.defineProperty(
356
+ cls.prototype,
357
+ 'toString',
358
+ errorMethod(function toString() {
359
+ return `${displayName}: ${this.message}`;
360
+ }),
361
+ );
362
+ errorClassByQuark.set(quark, cls);
363
+ return cls;
364
+ }
365
+
366
+ // Called by the engine on a failed GI invoke, with ownership of the GError* already
367
+ // transferred to the boxed handle. Named, so the position properties report the
368
+ // application frame rather than this one.
369
+ native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
370
+ // Same cross-version pairing: an addon older than this JS calls the builder with
371
+ // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
372
+ // there is no second `errorClass` argument to give it. Rebuilding the GError from
373
+ // the fields keeps the ONE shape on that pairing too — it costs only the original
374
+ // GError's address, which nothing can read.
375
+ if (typeof handle === 'string') {
376
+ return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
377
+ }
378
+ return wrapGError(handle, true, buildThrownGError);
379
+ });
197
380
 
198
381
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
199
382
  // record WHICH introspected class a registration belongs to — matched against the
@@ -301,6 +484,12 @@ function wrapFundamental(handle) {
301
484
  // boxedMemberKind checks methods first — 1 = method, 2 = field, 0 = neither,
302
485
  // 3 = undecidable.
303
486
  function wrapBoxed(handle) {
487
+ // A GError gets the full error shape — domain class, Error brand, position
488
+ // properties — wherever it surfaces: an OUT param, a struct field, a container
489
+ // element. Routing HERE rather than in the GError-typed OUT arm in C++ is what
490
+ // makes those three agree; the old code only special-cased the thrown path, so a
491
+ // GError read out of a container was a bare method-routing proxy.
492
+ if (native.boxedTypeName(handle) === 'GError') return wrapGError(handle);
304
493
  const target = { [HANDLE]: handle };
305
494
  const methodDispatch =
306
495
  (prop) =>
@@ -612,6 +801,13 @@ function makeLogStructured(baseNs) {
612
801
  }
613
802
 
614
803
  function decorateGLibNamespace(baseNs) {
804
+ // GJS's GLib override installs this on the GLOBAL Error.prototype, which is what
805
+ // makes `catch (e) { if (e.matches(domain, code)) … }` safe to write without a
806
+ // type test in front of it. Plain assignment, so the descriptor is GJS's
807
+ // (writable, enumerable, configurable) rather than a hardened one.
808
+ Error.prototype.matches = function matches() {
809
+ return false;
810
+ };
615
811
  // Per-namespace cache of the overlay functions that need the introspected baseNs
616
812
  // (log_structured → log_variant; the *_once helpers → idle_add/timeout_add).
617
813
  const cache = new Map();
@@ -658,9 +854,9 @@ function decorateGLibNamespace(baseNs) {
658
854
  return new Proxy(baseNs, {
659
855
  get(t, prop) {
660
856
  if (prop === 'Variant') return variantClass;
661
- // GLib.Error is the L1 GError subclass (the engine throws instances of it,
662
- // and `new GLib.Error(domain, code, message)` constructs one), shadowing the
663
- // introspected boxed type so `instanceof GLib.Error` + `.matches()` work.
857
+ // GLib.Error is the L1 error base class — every GError reaching JS is an
858
+ // instance of it or of its domain's subclass — shadowing the introspected
859
+ // boxed type, which would hand out a bare method-routing proxy.
664
860
  if (prop === 'Error') return GLibError;
665
861
  if (typeof prop === 'string' && GLIB_OVERLAY_NAMES.has(prop)) return overlay(prop);
666
862
  return t[prop];
@@ -1954,13 +2150,11 @@ function makeEnum(namespace, typeName) {
1954
2150
  for (const key of Object.keys(raw)) {
1955
2151
  out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
1956
2152
  }
1957
- // If this enum is registered as a GError domain (Gio.IOErrorEnum, …), attach
1958
- // its domain descriptor (non-enumerable) so GLib.Error.matches(enum, code) can
1959
- // resolve the enum to its domain. A plain enum gets nothing (getErrorDomain → null).
2153
+ // An enum registered as a GError domain (Gio.IOErrorEnum, …) is the CLASS of its
2154
+ // errors as well, which a frozen object cannot be — it keeps its members on the
2155
+ // function form instead.
1960
2156
  const domain = native.getErrorDomain(namespace, typeName);
1961
- if (domain !== null) {
1962
- Object.defineProperty(out, ERROR_DOMAIN, { value: domain, enumerable: false });
1963
- }
2157
+ if (domain !== null) return makeErrorDomainClass(namespace, typeName, out, domain.quark);
1964
2158
  // `Ns.Enum.$gtype` — gjs answers a registered G_TYPE_ENUM/G_TYPE_FLAGS here, so
1965
2159
  // `GObject.type_name(Gio.BusType.$gtype)` is `GBusType` there and was `null` over
1966
2160
  // the bridge: an enum object carried no `$gtype` at all. EAGER rather than
package/index.d.ts CHANGED
@@ -56,17 +56,18 @@ export function getEnumValues(namespace: string, name: string): Record<string, n
56
56
  export function getErrorDomain(namespace: string, name: string): { name: string; quark: number } | null;
57
57
 
58
58
  /**
59
- * Register the L1 GLib.Error factory the engine calls when a GI invoke fails, so
60
- * a failed sync call throws a real `GLib.Error` (instanceof, with `.matches()`),
61
- * together with the CLASS it builds — which a `GError`-typed IN argument uses to
62
- * recognise such an error coming back and rebuild a real `GError` from its
63
- * fields. Without the class that direction accepts only a marshalled (boxed)
64
- * error, so a caught one cannot be handed on.
65
- */
66
- export function setErrorBuilder(
67
- builder: (domainName: string, domainQuark: number, code: number, message: string) => Error,
68
- errorClass?: new (domain: unknown, code: number, message: string) => Error,
69
- ): void;
59
+ * Resolve a domain quark back to the enum type registered for it — the reverse of
60
+ * {@link getErrorDomain}, so a thrown error reaches its domain class even when
61
+ * nothing has touched that enum yet. `null` for a quark no typelib claims.
62
+ */
63
+ export function errorDomainInfo(quark: number): { namespace: string; name: string } | null;
64
+
65
+ /**
66
+ * Register the L1 `GLib.Error` factory the engine calls when a GI invoke fails. It
67
+ * receives the failed call's `GError` as a boxed handle, having taken ownership of
68
+ * it, and returns the object to throw — the same shape a marshalled error gets.
69
+ */
70
+ export function setErrorBuilder(builder: (handle: BoxedHandle) => unknown): void;
70
71
 
71
72
  /** Prepend a directory to the GIRepository typelib search path. */
72
73
  export function prependSearchPath(path: string): void;
@@ -599,6 +600,7 @@ declare const native: {
599
600
  getConstantValue: typeof getConstantValue;
600
601
  getEnumValues: typeof getEnumValues;
601
602
  getErrorDomain: typeof getErrorDomain;
603
+ errorDomainInfo: typeof errorDomainInfo;
602
604
  setErrorBuilder: typeof setErrorBuilder;
603
605
  prependSearchPath: typeof prependSearchPath;
604
606
  prependLibraryPath: typeof prependLibraryPath;
package/index.js CHANGED
@@ -29,6 +29,7 @@ export const findInfo = native.findInfo;
29
29
  export const getConstantValue = native.getConstantValue;
30
30
  export const getEnumValues = native.getEnumValues;
31
31
  export const getErrorDomain = native.getErrorDomain;
32
+ export const errorDomainInfo = native.errorDomainInfo;
32
33
  export const setErrorBuilder = native.setErrorBuilder;
33
34
  export const prependSearchPath = native.prependSearchPath;
34
35
  export const prependLibraryPath = native.prependLibraryPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.63.0",
3
+ "version": "0.64.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/addon.cc CHANGED
@@ -88,6 +88,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
88
88
  exports.Set("getConstantValue", Napi::Function::New(env, GetConstantValue));
89
89
  exports.Set("getEnumValues", Napi::Function::New(env, GetEnumValues));
90
90
  exports.Set("getErrorDomain", Napi::Function::New(env, GetErrorDomain));
91
+ exports.Set("errorDomainInfo", Napi::Function::New(env, ErrorDomainInfo));
91
92
  exports.Set("setErrorBuilder", Napi::Function::New(env, SetErrorBuilder));
92
93
  exports.Set("prependSearchPath", Napi::Function::New(env, PrependSearchPath));
93
94
  exports.Set("prependLibraryPath", Napi::Function::New(env, PrependLibraryPath));
package/src/calls.cc CHANGED
@@ -380,44 +380,40 @@ bool IsSupportedOutType(GITypeInfo* type, std::string* why) {
380
380
 
381
381
  // ---- GError -> GLib.Error ----
382
382
  //
383
- // A failed GI invoke yields a GError (domain quark + code + message). GJS surfaces
384
- // it as a real `GLib.Error` (an Error subclass carrying `.domain`/`.code`/
385
- // `.message` + a `.matches(domain, code)` method). The GLib.Error CLASS itself is
386
- // owned by L1 (gi.js) so its `matches()` can resolve an error-enum object to its
387
- // domain; the engine just reports the GError's fields to an L1-registered builder
388
- // function (stored in this env's instance data — see NodeGiEnvData) and throws
389
- // what it returns.
383
+ // A failed GI invoke yields a GError (domain quark + code + message), which GJS
384
+ // throws as the error object of that DOMAIN's class. That object is L1's to build
385
+ // (gi.js wrapGError) — only JS can give it the Error brand, the domain class and
386
+ // the position properties — so the engine hands the builder registered in this
387
+ // env's instance data the one thing it needs: the `GError*` itself, as a boxed
388
+ // handle.
390
389
  //
391
- // Read a GError's domain (quark name + numeric quark), code and message, FREE it,
392
- // and throw the JS error the L1 builder produces (instanceof GLib.Error). Falls
393
- // back to a plain JS Error when no builder is registered for this env. `context`
394
- // is the "Ns.method" display name, used only in the fallback message (a real
395
- // GLib.Error carries the bare GError message, matching GJS).
390
+ // Passing the HANDLE and not the fields is the whole point. The fields used to be
391
+ // read out, the GError g_error_free'd here, and a JS Error subclass built from
392
+ // them — so a thrown error and a marshalled one were two different objects with
393
+ // two different `.domain` types. Ownership now transfers to the handle, which
394
+ // frees the GError when the JS error is collected, and both directions wrap the
395
+ // same pointer the same way.
396
+ //
397
+ // Falls back to a plain JS Error when no builder is registered for this env;
398
+ // `context` is the "Ns.method" display name, used only in that fallback message (a
399
+ // real GLib.Error carries the bare GError message, matching GJS).
396
400
  void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
397
- // Read everything out of the GError BEFORE g_error_free.
398
- GQuark domainQuark = error != nullptr ? error->domain : 0;
399
- const char* domainName = domainQuark != 0 ? g_quark_to_string(domainQuark) : nullptr;
400
- std::string domain = domainName != nullptr ? domainName : "";
401
- int code = error != nullptr ? error->code : 0;
402
401
  std::string message =
403
402
  (error != nullptr && error->message != nullptr) ? error->message : "invocation failed";
404
- if (error != nullptr) g_error_free(error);
405
403
 
406
- // Prefer this env's L1 GLib.Error builder (instanceof GLib.Error + matches()).
407
404
  NodeGiEnvData* d = EnvData(env);
408
- if (d != nullptr && d->errorBuilder != nullptr) {
405
+ if (error != nullptr && d != nullptr && d->errorBuilder != nullptr) {
409
406
  napi_value builder = nullptr;
410
407
  if (napi_get_reference_value(env, d->errorBuilder, &builder) == napi_ok &&
411
408
  builder != nullptr) {
412
409
  napi_value undef = nullptr;
413
410
  napi_get_undefined(env, &undef);
414
- napi_value bargs[4] = {nullptr, nullptr, nullptr, nullptr};
415
- napi_create_string_utf8(env, domain.c_str(), NAPI_AUTO_LENGTH, &bargs[0]);
416
- napi_create_uint32(env, static_cast<uint32_t>(domainQuark), &bargs[1]);
417
- napi_create_int32(env, code, &bargs[2]);
418
- napi_create_string_utf8(env, message.c_str(), NAPI_AUTO_LENGTH, &bargs[3]);
411
+ // GI_TRANSFER_EVERYTHING: the invoke's out-GError is ours, and the handle
412
+ // adopts it rather than copying — so nothing frees it here.
413
+ napi_value handle = MakeGErrorHandle(env, error, GI_TRANSFER_EVERYTHING);
414
+ error = nullptr; // adopted by the handle; no path below may free it again
419
415
  napi_value errObj = nullptr;
420
- napi_status st = napi_call_function(env, undef, builder, 4, bargs, &errObj);
416
+ napi_status st = napi_call_function(env, undef, builder, 1, &handle, &errObj);
421
417
  if (st == napi_ok && errObj != nullptr) {
422
418
  napi_throw(env, errObj);
423
419
  return;
@@ -427,6 +423,7 @@ void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
427
423
  if (napi_is_exception_pending(env, &pending) == napi_ok && pending) return;
428
424
  }
429
425
  }
426
+ if (error != nullptr) g_error_free(error);
430
427
  Napi::Error::New(env, "Calling " + context + ": " + message).ThrowAsJavaScriptException();
431
428
  }
432
429
 
@@ -636,7 +633,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
636
633
  // belong to the callee (dropped only when the callee never ran).
637
634
  CreatedBytes createdBytes;
638
635
  CreatedValues createdValues;
639
- CreatedErrors createdErrors;
640
636
  bool ok = true;
641
637
  size_t jsCursor = 0;
642
638
  std::vector<LengthWrite> lengthWrites;
@@ -815,7 +811,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
815
811
  jsCursor++;
816
812
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
817
813
  if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings, nullptr,
818
- nullptr, nullptr, &createdErrors,
814
+ nullptr, nullptr,
819
815
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)))) {
820
816
  in_args[inPos[i]].v_pointer = &slots[i];
821
817
  out_args[outPos[i]].v_pointer = &slots[i];
@@ -922,7 +918,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
922
918
  } else {
923
919
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
924
920
  ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
925
- &createdClosures, &createdBytes, &createdValues, &createdErrors,
921
+ &createdClosures, &createdBytes, &createdValues,
926
922
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
927
923
  }
928
924
  }
@@ -944,9 +940,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
944
940
  // Likewise every GValue boxed from a plain JS value — never adopted.
945
941
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
946
942
  for (GValue* gv : createdValues.transferFull) NodeGiFreeBoxedGValue(gv);
947
- // Likewise every GError built from an L1 GLib.Error — never adopted.
948
- for (GError* e : createdErrors.transferNone) g_error_free(e);
949
- for (GError* e : createdErrors.transferFull) g_error_free(e);
950
943
  // Likewise the transfer-full-instance ref: the callee never consumed it.
951
944
  if (instanceRefTaken) g_object_unref(static_cast<GObject*>(instance));
952
945
  for (gpointer s : ownedInStrings) g_free(s); // never reached the callee (#658)
@@ -1001,11 +994,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
1001
994
  // box is ours to free — success AND error paths, mirroring the GBytes rule.
1002
995
  // Transfer-full GValues were adopted by the callee — no release.
1003
996
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
1004
- // Transfer-none GError IN-args built from an L1 GLib.Error: the callee copied
1005
- // what it kept (gst_message_new_error copies the GError into the message), so
1006
- // the build is ours to free — success AND error paths, mirroring the two rules
1007
- // above. Transfer-full GErrors were adopted by the callee — no release.
1008
- for (GError* e : createdErrors.transferNone) g_error_free(e);
1009
997
  if (!success) {
1010
998
  for (const InContainer& c : inContainers) FreeInContainer(c);
1011
999
  // A failed invoke did not adopt the transfer-full IN/INOUT strings we g_strdup'd
package/src/class.cc CHANGED
@@ -907,7 +907,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
907
907
  jsCursor++;
908
908
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
909
909
  if (JsToGIArgument(env, v, ti, &slots[i], &holds[i], tr, &ownedInStrings, nullptr,
910
- nullptr, nullptr, nullptr,
910
+ nullptr, nullptr,
911
911
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai))))
912
912
  giArgs[1 + i].v_pointer = &slots[i];
913
913
  else
@@ -963,7 +963,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
963
963
  jsCursor++;
964
964
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
965
965
  ok = JsToGIArgument(env, v, ti, &giArgs[1 + i], &holds[i], tr, &ownedInStrings, nullptr,
966
- nullptr, nullptr, nullptr,
966
+ nullptr, nullptr,
967
967
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
968
968
  }
969
969
 
package/src/common.h CHANGED
@@ -83,11 +83,6 @@ GIRepository* DupDefaultRepository();
83
83
  // Init, freed by the instance-data finalizer at env teardown.
84
84
  struct NodeGiEnvData {
85
85
  napi_ref errorBuilder = nullptr;
86
- // The CLASS the builder above constructs — L1's `GLib.Error`. Registered with
87
- // the builder (one call, so the two cannot drift apart) and read the other way
88
- // round: a GError-typed IN arg uses it to recognise an L1 error and rebuild a
89
- // real GError from its fields. Per-env for the same reason as errorBuilder.
90
- napi_ref errorClass = nullptr;
91
86
  // L1-registered resolver for Gtk.Template `<signal handler="…">` callbacks: given
92
87
  // (instanceHandle, handlerName) it returns the instance's bound JS method (or
93
88
  // undefined). Stored per-env for the same reason as errorBuilder (a napi_ref is
@@ -160,6 +155,13 @@ extern const napi_type_tag kGObjectHandleTag;
160
155
  Napi::Value MakeBoxedHandle(Napi::Env env, gpointer ptr, GType gtype, bool owns,
161
156
  GIBaseInfo* info = nullptr, bool rawOwned = false);
162
157
  Napi::Value WrapVariant(Napi::Env env, GVariant* var, GITransfer transfer);
158
+ // Wrap a `GError*` as the boxed handle L1's `wrapGError` turns into the error
159
+ // object. ONE helper for both directions a GError reaches JS by — a GError-typed
160
+ // return/OUT and the error a failed invoke THREW — because those used to produce
161
+ // two different JS shapes, and sharing the wrap is what keeps them from drifting
162
+ // apart again. Transfer follows the boxed rule: transfer-everything is adopted,
163
+ // transfer-none is copied, so the handle always owns what it frees.
164
+ Napi::Value MakeGErrorHandle(Napi::Env env, GError* error, GITransfer transfer);
163
165
  BoxedHandle* TryGetBoxedHandle(Napi::Value v);
164
166
  bool TryGetBoxedPtr(Napi::Value v, gpointer* out);
165
167
  Napi::Value MakeGTypeHandle(Napi::Env env, GType gtype);
@@ -317,13 +319,13 @@ struct CreatedClosures {
317
319
 
318
320
  // ---- C objects this layer SYNTHESISES for one IN argument --------------------
319
321
  //
320
- // Three IN arms build a throw-away C object out of a plain JS value, because the
321
- // JS side has no handle to hand over: a Uint8Array for a `GLib.Bytes` arg, a
322
- // number/string for a `GObject.Value` arg, an L1 `GLib.Error` for a `GError` arg.
323
- // All three carry the SAME lifetime rule, which is why they share one type
324
- // instead of a third near-identical struct: transfer-none → the callee borrowed
325
- // (or copied) it, so we release it after the invoke; transfer-full → the callee
326
- // adopted it, so it is released only when the invoke never ran.
322
+ // Two IN arms build a throw-away C object out of a plain JS value, because the JS
323
+ // side has no handle to hand over: a Uint8Array for a `GLib.Bytes` arg, a
324
+ // number/string for a `GObject.Value` arg. Both carry the SAME lifetime rule,
325
+ // which is why they share one type instead of a second near-identical struct:
326
+ // transfer-none → the callee borrowed (or copied) it, so we release it after the
327
+ // invoke; transfer-full → the callee adopted it, so it is released only when the
328
+ // invoke never ran.
327
329
  template <typename T>
328
330
  struct CreatedIn {
329
331
  std::vector<T*> transferNone; // release after the invoke (callee borrowed/copied)
@@ -353,17 +355,6 @@ using CreatedBytes = CreatedIn<GBytes>;
353
355
  // boxed-handle path and is passed through untouched.
354
356
  using CreatedValues = CreatedIn<GValue>;
355
357
 
356
- // An L1 `GLib.Error` (what a `catch` hands you, and what `new GLib.Error(…)`
357
- // builds) has no `GError*` behind it — it is a JS class, not a boxed handle — so
358
- // a `GError`-typed IN arg gets a fresh `g_error_new_literal` built from its
359
- // domain/code/message. Without this, the hand-back direction the OUT direction
360
- // exists for is only open to errors that came OUT of GI in the first place:
361
- // `GLib.propagate_error(caught)` and `Gst.Message.new_error(src, caught, dbg)`
362
- // both refused where gjs accepts, which is the shape an application actually
363
- // writes. A marshalled GError (a boxed handle) still routes through
364
- // TransferBoxedIn and is not copied here.
365
- using CreatedErrors = CreatedIn<GError>;
366
-
367
358
  // Release a GValue this layer boxed: unset the held value (dropping the ref an
368
359
  // object/boxed/string GValue took) and free the struct. A plain `g_free` would
369
360
  // leak whatever the GValue owns.
@@ -390,9 +381,6 @@ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
390
381
  // `values` (optional): enables the JS-value→GValue IN-arg boxing above; nullptr
391
382
  // (the default) keeps the previous behaviour (plain value → TypeError) on paths
392
383
  // that cannot release a created GValue.
393
- // `errors` (optional): enables the L1-GLib.Error→GError IN-arg build above;
394
- // nullptr (the default) keeps a marshalled boxed handle working and refuses the
395
- // L1 class, on paths that cannot release a created GError.
396
384
  // `argName` (optional): the introspected argument name, used ONLY in error
397
385
  // messages so a refusal reads like gjs's ("Expected type string for argument
398
386
  // 'name' but got type number"); nullptr degrades to a message without the name.
@@ -403,7 +391,6 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
403
391
  CreatedClosures* closures = nullptr,
404
392
  CreatedBytes* bytes = nullptr,
405
393
  CreatedValues* values = nullptr,
406
- CreatedErrors* errors = nullptr,
407
394
  const char* argName = nullptr);
408
395
 
409
396
  // ---- IN container building -----------------------------------------
@@ -671,6 +658,7 @@ Napi::Value FindInfo(const Napi::CallbackInfo& info);
671
658
  Napi::Value GetConstantValue(const Napi::CallbackInfo& info);
672
659
  Napi::Value GetEnumValues(const Napi::CallbackInfo& info);
673
660
  Napi::Value GetErrorDomain(const Napi::CallbackInfo& info);
661
+ Napi::Value ErrorDomainInfo(const Napi::CallbackInfo& info);
674
662
  Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info);
675
663
  Napi::Value PrependSearchPath(const Napi::CallbackInfo& info);
676
664
  Napi::Value PrependLibraryPath(const Napi::CallbackInfo& info);
package/src/marshal.cc CHANGED
@@ -143,6 +143,22 @@ static Napi::Value WrapBoxed(Napi::Env env, gpointer ptr, GIBaseInfo* structInfo
143
143
  return MakeBoxedHandle(env, handlePtr, handleGType, owns, info);
144
144
  }
145
145
 
146
+ // See common.h. Wrapped through the GLib.Error STRUCT info rather than by GType
147
+ // alone, so `domain`/`code`/`message` resolve as fields — which is how L1 reads
148
+ // them off the GError*.
149
+ Napi::Value MakeGErrorHandle(Napi::Env env, GError* error, GITransfer transfer) {
150
+ if (error == nullptr) return env.Null();
151
+ GIRepository* repo = DupDefaultRepository();
152
+ // GLib is loaded by any namespace require; require defensively anyway so a bare
153
+ // engine call still resolves the struct info (idempotent, cheap).
154
+ gi_repository_require(repo, "GLib", "2.0", static_cast<GIRepositoryLoadFlags>(0), nullptr);
155
+ GIBaseInfo* errInfo = gi_repository_find_by_name(repo, "GLib", "Error");
156
+ Napi::Value result = WrapBoxed(env, error, errInfo, transfer);
157
+ if (errInfo != nullptr) gi_base_info_unref(errInfo);
158
+ g_object_unref(repo);
159
+ return result;
160
+ }
161
+
146
162
  // Read a boxed handle (ptr + boxed GType) if `v` is one (tag-checked; no deref of
147
163
  // ptr). Returns nullptr when `v` is not a node-gi boxed handle. The returned handle
148
164
  // is owned by the External — never free it. Callers that need the GType (boxed
@@ -472,47 +488,6 @@ static void ThrowExpectedGLibError(Napi::Env env, const char* argName, const std
472
488
  .ThrowAsJavaScriptException();
473
489
  }
474
490
 
475
- // Whether `v` is an instance of the L1 `GLib.Error` class — the shape a `catch`
476
- // around a failed GI invoke hands you, and what `new GLib.Error(domain, code,
477
- // message)` builds. Recognition is `instanceof` against the class L1 registered
478
- // alongside its error builder, never duck-typing: a plain object carrying a
479
- // `code` must still be refused.
480
- static bool IsL1GLibError(Napi::Env env, Napi::Value v) {
481
- if (!v.IsObject()) return false;
482
- NodeGiEnvData* d = EnvData(env);
483
- if (d == nullptr || d->errorClass == nullptr) return false;
484
- napi_value cls = nullptr;
485
- if (napi_get_reference_value(env, d->errorClass, &cls) != napi_ok || cls == nullptr) return false;
486
- Napi::Value clsValue(env, cls);
487
- return clsValue.IsFunction() && v.As<Napi::Object>().InstanceOf(clsValue.As<Napi::Function>());
488
- }
489
-
490
- // Build a real GError out of an L1 `GLib.Error`, which carries no `GError*` of
491
- // its own. Throws + returns nullptr when its domain is unusable.
492
- //
493
- // `.domainQuark` is the numeric GQuark the engine read off the original GError;
494
- // `.domain` is its NAME (the node-gi divergence documented in
495
- // docs/node-gi-gjs-surface.md), which is what an error constructed in JS from a
496
- // quark name carries. Either resolves to the same quark; a value with neither is
497
- // not a usable GError — quark 0 would produce an error no `matches()` can ever
498
- // answer for, so it is refused rather than marshalled into a silent mismatch.
499
- static GError* ErrorFromL1GLibError(Napi::Env env, Napi::Value v, const char* argName) {
500
- Napi::Object err = v.As<Napi::Object>();
501
- Napi::Value quark = err.Get("domainQuark");
502
- Napi::Value domain = err.Get("domain");
503
- GQuark gq = 0;
504
- if (quark.IsNumber()) gq = static_cast<GQuark>(NodeGiToUint32(quark));
505
- else if (domain.IsString())
506
- gq = g_quark_from_string(domain.As<Napi::String>().Utf8Value().c_str());
507
- if (gq == 0) {
508
- ThrowExpectedGLibError(env, argName, "a GLib.Error with no domain");
509
- return nullptr;
510
- }
511
- Napi::Value message = err.Get("message");
512
- std::string text = message.IsString() ? message.As<Napi::String>().Utf8Value() : std::string();
513
- return g_error_new_literal(gq, NodeGiToInt32(err.Get("code")), text.c_str());
514
- }
515
-
516
491
  // "Gtk.Box" for an introspected GType, g_type_name() otherwise (a registerClass
517
492
  // subtype has no GI info of its own; its C name is still exact). The gjs twin is
518
493
  // GIWrapperBase::format_name() (refs/gjs/gi/wrapperutils.h).
@@ -541,7 +516,6 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
541
516
  CreatedClosures* closures,
542
517
  CreatedBytes* bytes,
543
518
  CreatedValues* values,
544
- CreatedErrors* errors,
545
519
  const char* argName) {
546
520
  if (v.IsEmpty()) {
547
521
  // Residue of a swallowed napi failure (a fallible Get()/coercion upstream
@@ -829,43 +803,35 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
829
803
  out->v_pointer = nullptr;
830
804
  return true;
831
805
  }
832
- // Shape 1: the boxed handle a GError-typed return/OUT produced. Ownership is
833
- // the ordinary boxed one and TransferBoxedIn supplies it: transfer-none is a
834
- // borrow, transfer-full hands the callee an independent g_error_copy
835
- // (g_propagate_error ADOPTS and frees its `src`) so the JS handle's finalizer
836
- // cannot double-free it. Measured against gjs: after `GLib.propagate_error(e)`
837
- // the original `e` is still readable there too.
806
+ // There is exactly ONE shape to accept: the boxed handle every GError reaching
807
+ // JS carries, whether it was marshalled out, copied, constructed or thrown
808
+ // (L1's wrapGError puts the same handle on all four). The second arm this used
809
+ // to need — rebuilding a GError from an L1 Error subclass's JS fields — is gone
810
+ // with the subclass itself.
811
+ //
812
+ // Ownership is the ordinary boxed one and TransferBoxedIn supplies it:
813
+ // transfer-none is a borrow, transfer-full hands the callee an independent
814
+ // g_error_copy (g_propagate_error ADOPTS and frees its `src`) so the JS
815
+ // handle's finalizer cannot double-free it. Measured against gjs: after
816
+ // `GLib.propagate_error(e)` the original `e` is still readable there too.
838
817
  //
839
818
  // Type-safety is UNCONDITIONAL here, unlike the struct/boxed arm's "compare
840
819
  // only when both sides carry a registered GType": the expected type is known
841
820
  // (G_TYPE_ERROR, always), so a handle without it is a wrong pointer — and a
842
821
  // wrong pointer in a GError slot is a wild dereference inside GLib.
843
822
  BoxedHandle* h = TryGetBoxedHandle(v);
844
- if (h != nullptr) {
845
- if (h->gtype == G_TYPE_INVALID || !g_type_is_a(h->gtype, G_TYPE_ERROR)) {
846
- ThrowExpectedGLibError(env, argName,
847
- h->gtype != G_TYPE_INVALID ? g_type_name(h->gtype)
848
- : InformalValueTypeName(v));
849
- return false;
850
- }
851
- out->v_pointer = TransferBoxedIn(h, transfer);
852
- return true;
823
+ if (h == nullptr) {
824
+ ThrowExpectedGLibError(env, argName, InformalValueTypeName(v));
825
+ return false;
853
826
  }
854
- // Shape 2: the L1 `GLib.Error` JS class — a caught error, or one built with
855
- // `new GLib.Error(…)`. It carries no GError*, so build one and release it per
856
- // transfer (CreatedErrors, the rule GBytes and GValue already follow). gjs has
857
- // ONE object for both shapes and accepts either; refusing this one left the
858
- // hand-back the OUT direction exists for closed to exactly the errors an
859
- // application has in hand.
860
- if (errors != nullptr && IsL1GLibError(env, v)) {
861
- GError* built = ErrorFromL1GLibError(env, v, argName);
862
- if (built == nullptr) return false; // ErrorFromL1GLibError already threw
863
- out->v_pointer = built;
864
- errors->Record(built, transfer);
865
- return true;
827
+ if (h->gtype == G_TYPE_INVALID || !g_type_is_a(h->gtype, G_TYPE_ERROR)) {
828
+ ThrowExpectedGLibError(
829
+ env, argName,
830
+ h->gtype != G_TYPE_INVALID ? g_type_name(h->gtype) : InformalValueTypeName(v));
831
+ return false;
866
832
  }
867
- ThrowExpectedGLibError(env, argName, InformalValueTypeName(v));
868
- return false;
833
+ out->v_pointer = TransferBoxedIn(h, transfer);
834
+ return true;
869
835
  }
870
836
  default:
871
837
  Napi::TypeError::New(
@@ -995,23 +961,10 @@ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
995
961
  }
996
962
  case GI_TYPE_TAG_ERROR: {
997
963
  // A GError-typed value (a `GLib.Error` return like Gtk.GLArea.get_error(),
998
- // or a GLib.Error.new_literal construct): GJS surfaces it as a GLib.Error
999
- // boxed (G_TYPE_ERROR) with `.domain`/`.code`/`.message` field access +
1000
- // `.matches()`/`.copy()` methods. Wrap through the GLib.Error struct info
1001
- // so the boxed handle resolves fields and methods; a NULL GError (the
1002
- // no-error case) stays null. WrapBoxed handles transfer: a transfer-none
1003
- // borrow is g_boxed_copy'd into an owned copy, transfer-full is adopted.
1004
- GError* gerr = static_cast<GError*>(arg->v_pointer);
1005
- if (gerr == nullptr) return env.Null();
1006
- GIRepository* repo = DupDefaultRepository();
1007
- // GLib is loaded by any namespace require; require defensively anyway so a
1008
- // bare engine call still resolves the struct info (idempotent, cheap).
1009
- gi_repository_require(repo, "GLib", "2.0", static_cast<GIRepositoryLoadFlags>(0), nullptr);
1010
- GIBaseInfo* errInfo = gi_repository_find_by_name(repo, "GLib", "Error");
1011
- Napi::Value result = WrapBoxed(env, gerr, errInfo, transfer);
1012
- if (errInfo != nullptr) gi_base_info_unref(errInfo);
1013
- g_object_unref(repo);
1014
- return result;
964
+ // or a GLib.Error.new_literal construct). The same handle a THROWN error
965
+ // carries, so L1 builds the same object from both — a NULL GError (the
966
+ // no-error case) stays null.
967
+ return MakeGErrorHandle(env, static_cast<GError*>(arg->v_pointer), transfer);
1015
968
  }
1016
969
  default:
1017
970
  Napi::TypeError::New(env, "Unsupported return type tag " +
@@ -1861,7 +1814,7 @@ bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer
1861
1814
  }
1862
1815
  std::string held;
1863
1816
  return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
1864
- nullptr, argName);
1817
+ argName);
1865
1818
  }
1866
1819
 
1867
1820
  } // namespace
package/src/repo.cc CHANGED
@@ -13,7 +13,6 @@ void NodeGiEnvDataFinalize(napi_env env, void* data, void* /*hint*/) {
13
13
  NodeGiEnvData* d = static_cast<NodeGiEnvData*>(data);
14
14
  if (d == nullptr) return;
15
15
  if (d->errorBuilder != nullptr) napi_delete_reference(env, d->errorBuilder);
16
- if (d->errorClass != nullptr) napi_delete_reference(env, d->errorClass);
17
16
  if (d->templateCallbackResolver != nullptr)
18
17
  napi_delete_reference(env, d->templateCallbackResolver);
19
18
  if (d->cairoWrappers != nullptr) napi_delete_reference(env, d->cairoWrappers);
@@ -260,17 +259,52 @@ Napi::Value GetErrorDomain(const Napi::CallbackInfo& info) {
260
259
  return result;
261
260
  }
262
261
 
263
- // setErrorBuilder(builder: (domainName, domainQuark, code, message) => Error,
264
- // errorClass?: typeof GLib.Error) -> void
265
- // Register the L1 GLib.Error factory the engine calls when a GI invoke fails, so a
266
- // failed sync call throws a real GLib.Error, together with the class it builds —
267
- // which the GError IN-arg path needs to recognise one coming back. Both stored
268
- // per-env (a napi_ref is env-specific; both paths are gated on the same env), in
269
- // ONE call so a registration cannot supply the builder and forget the class.
262
+ // errorDomainInfo(quark: number) -> { namespace: string, name: string } | null
263
+ // Which introspected enum registered a GError domain quark, so L1 can resolve a
264
+ // thrown error to its DOMAIN CLASS (Gio.IOErrorEnum) by reading that member.
265
+ // getErrorDomain below answers the same relation the other way round, and L1's
266
+ // quark→class map is only populated by it — which is not enough on its own: an
267
+ // application that never named `Gio.IOErrorEnum` has nothing in the map, and the
268
+ // error it catches would land on the base class, breaking exactly the
269
+ // `catch (e) { if (e instanceof Gio.IOErrorEnum) }` idiom. Null for a quark no
270
+ // typelib registers (a private `GLib.quark_from_string` domain).
271
+ Napi::Value ErrorDomainInfo(const Napi::CallbackInfo& info) {
272
+ Napi::Env env = info.Env();
273
+ if (info.Length() < 1 || !info[0].IsNumber()) {
274
+ Napi::TypeError::New(env, "errorDomainInfo(quark: number)").ThrowAsJavaScriptException();
275
+ return env.Null();
276
+ }
277
+ GQuark quark = static_cast<GQuark>(NodeGiToUint32(info[0]));
278
+ if (quark == 0) return env.Null();
279
+ GIRepository* repo = DupDefaultRepository();
280
+ GIEnumInfo* enumInfo = gi_repository_find_by_error_domain(repo, quark);
281
+ Napi::Value result = env.Null();
282
+ if (enumInfo != nullptr) {
283
+ GIBaseInfo* base = reinterpret_cast<GIBaseInfo*>(enumInfo);
284
+ const char* ns = gi_base_info_get_namespace(base);
285
+ const char* name = gi_base_info_get_name(base);
286
+ if (ns != nullptr && name != nullptr) {
287
+ Napi::Object obj = Napi::Object::New(env);
288
+ obj.Set("namespace", Napi::String::New(env, ns));
289
+ obj.Set("name", Napi::String::New(env, name));
290
+ result = obj;
291
+ }
292
+ gi_base_info_unref(base);
293
+ }
294
+ g_object_unref(repo);
295
+ return result;
296
+ }
297
+
298
+ // setErrorBuilder(builder: (handle) => GLib.Error) -> void
299
+ // Register the L1 error factory the engine calls when a GI invoke fails, so a
300
+ // failed sync call throws the error object of its domain's class. Stored per-env
301
+ // (a napi_ref is env-specific). It takes the GError's boxed HANDLE — the L1 error
302
+ // IS that handle, which is why nothing else needs registering here: the GError
303
+ // IN-arg direction reads the same handle back off the object.
270
304
  Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info) {
271
305
  Napi::Env env = info.Env();
272
306
  if (info.Length() < 1 || !info[0].IsFunction()) {
273
- Napi::TypeError::New(env, "setErrorBuilder(builder: function, errorClass?: function)")
307
+ Napi::TypeError::New(env, "setErrorBuilder(builder: function)")
274
308
  .ThrowAsJavaScriptException();
275
309
  return env.Undefined();
276
310
  }
@@ -281,12 +315,6 @@ Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info) {
281
315
  d->errorBuilder = nullptr;
282
316
  }
283
317
  napi_create_reference(env, info[0], 1, &d->errorBuilder);
284
- if (d->errorClass != nullptr) {
285
- napi_delete_reference(env, d->errorClass);
286
- d->errorClass = nullptr;
287
- }
288
- if (info.Length() >= 2 && info[1].IsFunction())
289
- napi_create_reference(env, info[1], 1, &d->errorClass);
290
318
  return env.Undefined();
291
319
  }
292
320