@gjsify/node-gi 0.64.1 → 0.64.2

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/cairo.d.ts CHANGED
@@ -36,6 +36,16 @@ export interface FontExtents {
36
36
  maxYAdvance: number;
37
37
  }
38
38
 
39
+ /** `cairo_matrix_t` as the plain object GJS passes around (no matrix wrapper). */
40
+ export interface Matrix {
41
+ xx: number;
42
+ yx: number;
43
+ xy: number;
44
+ yy: number;
45
+ x0: number;
46
+ y0: number;
47
+ }
48
+
39
49
  /** cairo drawing context (`cairo_t`). */
40
50
  export class Context {
41
51
  constructor(surface: Surface);
@@ -53,10 +63,17 @@ export class Context {
53
63
  textPath(utf8: string): void;
54
64
  textExtents(utf8: string): TextExtents;
55
65
  fontExtents(): FontExtents;
66
+ /**
67
+ * The current font matrix. GJS has no cairo matrix wrapper
68
+ * (`getMatrix`/`setMatrix` stay unbound), so it travels as a plain object.
69
+ */
70
+ getFontMatrix(): Matrix;
71
+ setFontMatrix(matrix: Matrix): void;
56
72
  rectangle(x: number, y: number, width: number, height: number): void;
57
73
  arc(xc: number, yc: number, radius: number, angle1: number, angle2: number): void;
58
74
  arcNegative(xc: number, yc: number, radius: number, angle1: number, angle2: number): void;
59
75
  curveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): void;
76
+ relCurveTo(dx1: number, dy1: number, dx2: number, dy2: number, dx3: number, dy3: number): void;
60
77
  fill(): void;
61
78
  fillPreserve(): void;
62
79
  stroke(): void;
@@ -65,6 +82,19 @@ export class Context {
65
82
  paintWithAlpha(alpha: number): void;
66
83
  clip(): void;
67
84
  clipPreserve(): void;
85
+ resetClip(): void;
86
+ /** Redirect drawing to an intermediate group surface. */
87
+ pushGroup(): void;
88
+ /** Like {@link pushGroup} but with an explicit {@link Content} for the group surface. */
89
+ pushGroupWithContent(content: number): void;
90
+ /** End the current group and return it as a pattern (concrete subclass). */
91
+ popGroup(): Pattern;
92
+ /** End the current group and install it as this context's source. */
93
+ popGroupToSource(): void;
94
+ /** Paint the current source through `pattern`'s alpha. */
95
+ mask(pattern: Pattern): void;
96
+ /** Paint the current source through `surface`'s alpha, offset to `(x, y)`. */
97
+ maskSurface(surface: Surface, x: number, y: number): void;
68
98
  translate(tx: number, ty: number): void;
69
99
  scale(sx: number, sy: number): void;
70
100
  rotate(angle: number): void;
@@ -74,6 +104,10 @@ export class Context {
74
104
  setSourceSurface(surface: Surface, x: number, y: number): void;
75
105
  /** The context's current source pattern (own reference; concrete subclass). */
76
106
  getSource(): Pattern;
107
+ /** The surface this context draws to (own reference; concrete subclass). */
108
+ getTarget(): Surface;
109
+ /** The target of the current group, or {@link getTarget} outside a group. */
110
+ getGroupTarget(): Surface;
77
111
  identityMatrix(): void;
78
112
  newSubPath(): void;
79
113
  /** User → device coordinates as `[x, y]` (GJS return shape). */
@@ -111,6 +145,10 @@ export class Context {
111
145
  setOperator(op: number): void;
112
146
  setMiterLimit(limit: number): void;
113
147
  setTolerance(tolerance: number): void;
148
+ /** Emit the current page and clear it (paginated surfaces; a no-op elsewhere). */
149
+ copyPage(): void;
150
+ /** Like {@link copyPage} but also clears the current page's contents. */
151
+ showPage(): void;
114
152
  status(): number;
115
153
  $dispose(): void;
116
154
  }
package/cairo.js CHANGED
@@ -23,16 +23,25 @@
23
23
  // (the `Object.assign({}, imports._cairo, cairoNative)` merge shape).
24
24
  // Copyright (c) 2010 litl, LLC / GJS contributors. MIT OR LGPL-2.0-or-later.
25
25
 
26
- import native from './index.js';
26
+ import native, { ensureNative, onNativeLoaded } from './index.js';
27
27
 
28
- const c = native.__cairo;
29
- if (c === undefined) {
30
- throw new Error(
28
+ // Bound when the addon loads: at import on desktop, on first use on a lazy host (NativeScript).
29
+ // Every call that can run before any cairo object exists goes through `engine()`.
30
+ let c;
31
+
32
+ function missingBinding() {
33
+ return new Error(
31
34
  '@gjsify/node-gi/cairo: the native cairo binding is missing from the addon. ' +
32
35
  'Rebuild @gjsify/node-gi (node-gyp rebuild) — the addon must link libcairo.',
33
36
  );
34
37
  }
35
38
 
39
+ function engine() {
40
+ ensureNative();
41
+ if (c === undefined) throw missingBinding();
42
+ return c;
43
+ }
44
+
36
45
  // ---- enums (ported verbatim from refs/gjs/modules/core/_cairo.js) -----------
37
46
 
38
47
  export const Antialias = { DEFAULT: 0, NONE: 1, GRAY: 2, SUBPIXEL: 3 };
@@ -141,7 +150,7 @@ function handleOf(x, what) {
141
150
  /** cairo drawing context. `new cairo.Context(surface)` wraps `cairo_create`. */
142
151
  export class Context {
143
152
  constructor(surface) {
144
- setHandle(this, c.contextCreate(handleOf(surface, 'cairo.Surface')));
153
+ setHandle(this, engine().contextCreate(handleOf(surface, 'cairo.Surface')));
145
154
  }
146
155
 
147
156
  save() {
@@ -181,6 +190,9 @@ export class Context {
181
190
  curveTo(x1, y1, x2, y2, x3, y3) {
182
191
  c.curveTo(this[H], x1, y1, x2, y2, x3, y3);
183
192
  }
193
+ relCurveTo(dx1, dy1, dx2, dy2, dx3, dy3) {
194
+ c.relCurveTo(this[H], dx1, dy1, dx2, dy2, dx3, dy3);
195
+ }
184
196
 
185
197
  fill() {
186
198
  c.fill(this[H]);
@@ -206,6 +218,33 @@ export class Context {
206
218
  clipPreserve() {
207
219
  c.clipPreserve(this[H]);
208
220
  }
221
+ resetClip() {
222
+ c.resetClip(this[H]);
223
+ }
224
+ /** Redirect drawing to an intermediate group surface. */
225
+ pushGroup() {
226
+ c.pushGroup(this[H]);
227
+ }
228
+ /** Like {@link pushGroup} but with an explicit {@link Content} for the group surface. */
229
+ pushGroupWithContent(content) {
230
+ c.pushGroupWithContent(this[H], content);
231
+ }
232
+ /** End the current group and return it as a pattern (concrete subclass). */
233
+ popGroup() {
234
+ return wrapPatternHandle(c.popGroup(this[H]));
235
+ }
236
+ /** End the current group and install it as this context's source. */
237
+ popGroupToSource() {
238
+ c.popGroupToSource(this[H]);
239
+ }
240
+ /** Paint the current source through `pattern`'s alpha. */
241
+ mask(pattern) {
242
+ c.mask(this[H], handleOf(pattern, 'cairo.Pattern'));
243
+ }
244
+ /** Paint the current source through `surface`'s alpha, offset to `(x, y)`. */
245
+ maskSurface(surface, x, y) {
246
+ c.maskSurface(this[H], handleOf(surface, 'cairo.Surface'), x, y);
247
+ }
209
248
 
210
249
  translate(tx, ty) {
211
250
  c.translate(this[H], tx, ty);
@@ -289,6 +328,14 @@ export class Context {
289
328
  getSource() {
290
329
  return wrapPatternHandle(c.getSource(this[H]));
291
330
  }
331
+ /** The surface this context draws to (a ref of its own — safe to keep). */
332
+ getTarget() {
333
+ return wrapSurfaceHandle(c.getTarget(this[H]));
334
+ }
335
+ /** The target of the current group, or {@link getTarget} outside a group. */
336
+ getGroupTarget() {
337
+ return wrapSurfaceHandle(c.getGroupTarget(this[H]));
338
+ }
292
339
 
293
340
  setLineWidth(width) {
294
341
  c.setLineWidth(this[H], width);
@@ -355,6 +402,18 @@ export class Context {
355
402
  fontExtents() {
356
403
  return c.fontExtents(this[H]);
357
404
  }
405
+ /**
406
+ * The current font matrix as `{ xx, yx, xy, yy, x0, y0}`. GJS has no cairo
407
+ * matrix wrapper (`getMatrix`/`setMatrix` stay unbound), so it travels as
408
+ * a plain object of the six doubles.
409
+ */
410
+ getFontMatrix() {
411
+ return c.getFontMatrix(this[H]);
412
+ }
413
+ /** Set the font matrix from `{ xx, yx, xy, yy, x0, y0}`. */
414
+ setFontMatrix(matrix) {
415
+ c.setFontMatrix(this[H], matrix);
416
+ }
358
417
  /** Stroke bounding box as `[x1, y1, x2, y2]`. */
359
418
  strokeExtents() {
360
419
  return c.strokeExtents(this[H]);
@@ -385,6 +444,15 @@ export class Context {
385
444
  c.setTolerance(this[H], tolerance);
386
445
  }
387
446
 
447
+ /** Emit the current page and clear it (paginated surfaces; a no-op elsewhere). */
448
+ copyPage() {
449
+ c.copyPage(this[H]);
450
+ }
451
+ /** Like {@link copyPage} but also clears the current page's contents. */
452
+ showPage() {
453
+ c.showPage(this[H]);
454
+ }
455
+
388
456
  status() {
389
457
  return c.status(this[H]);
390
458
  }
@@ -422,14 +490,14 @@ export class Surface {
422
490
  export class ImageSurface extends Surface {
423
491
  constructor(format, width, height) {
424
492
  super();
425
- setHandle(this, c.imageSurfaceCreate(format, width, height));
493
+ setHandle(this, engine().imageSurfaceCreate(format, width, height));
426
494
  }
427
495
 
428
496
  static create(format, width, height) {
429
497
  return new ImageSurface(format, width, height);
430
498
  }
431
499
  static createFromPNG(filename) {
432
- return wrapRaw(ImageSurface, c.imageSurfaceCreateFromPNG(filename));
500
+ return wrapRaw(ImageSurface, engine().imageSurfaceCreateFromPNG(filename));
433
501
  }
434
502
 
435
503
  getWidth() {
@@ -471,10 +539,10 @@ export class Pattern {
471
539
  /** A solid-color pattern (`cairo_pattern_create_rgb[a]`). */
472
540
  export class SolidPattern extends Pattern {
473
541
  static createRGB(red, green, blue) {
474
- return wrapRaw(SolidPattern, c.solidPatternCreateRGB(red, green, blue));
542
+ return wrapRaw(SolidPattern, engine().solidPatternCreateRGB(red, green, blue));
475
543
  }
476
544
  static createRGBA(red, green, blue, alpha) {
477
- return wrapRaw(SolidPattern, c.solidPatternCreateRGBA(red, green, blue, alpha));
545
+ return wrapRaw(SolidPattern, engine().solidPatternCreateRGBA(red, green, blue, alpha));
478
546
  }
479
547
  }
480
548
 
@@ -492,7 +560,7 @@ export class Gradient extends Pattern {
492
560
  export class LinearGradient extends Gradient {
493
561
  constructor(x0, y0, x1, y1) {
494
562
  super();
495
- setHandle(this, c.linearGradientCreate(x0, y0, x1, y1));
563
+ setHandle(this, engine().linearGradientCreate(x0, y0, x1, y1));
496
564
  }
497
565
  }
498
566
 
@@ -500,7 +568,7 @@ export class LinearGradient extends Gradient {
500
568
  export class RadialGradient extends Gradient {
501
569
  constructor(cx0, cy0, radius0, cx1, cy1, radius1) {
502
570
  super();
503
- setHandle(this, c.radialGradientCreate(cx0, cy0, radius0, cx1, cy1, radius1));
571
+ setHandle(this, engine().radialGradientCreate(cx0, cy0, radius0, cx1, cy1, radius1));
504
572
  }
505
573
  }
506
574
 
@@ -508,7 +576,7 @@ export class RadialGradient extends Gradient {
508
576
  export class SurfacePattern extends Pattern {
509
577
  constructor(surface) {
510
578
  super();
511
- setHandle(this, c.surfacePatternCreate(handleOf(surface, 'cairo.Surface')));
579
+ setHandle(this, engine().surfacePatternCreate(handleOf(surface, 'cairo.Surface')));
512
580
  }
513
581
 
514
582
  setExtend(extend) {
@@ -532,6 +600,12 @@ export class SurfacePattern extends Pattern {
532
600
  */
533
601
  export class Path {}
534
602
 
603
+ // Fan a surface handle out to the concrete subclass by cairo runtime type —
604
+ // mirrors CairoSurface::from_c_ptr (only the image surface has a subclass here).
605
+ function wrapSurfaceHandle(handle) {
606
+ return wrapRaw(c.surfaceGetType(handle) === SurfaceType.IMAGE ? ImageSurface : Surface, handle);
607
+ }
608
+
535
609
  // Fan a pattern handle out to the concrete subclass by cairo runtime type —
536
610
  // mirrors gjs_cairo_pattern_from_pattern (MESH/RASTER_SOURCE fall back to the
537
611
  // base Pattern; gjs throws for those, but they are unreachable from this module).
@@ -554,11 +628,15 @@ function wrapPatternHandle(handle) {
554
628
  // a returned/callback cairo pointer into the right JS instance. Surface + Pattern
555
629
  // fan out to the concrete subclass by cairo runtime type (mirrors
556
630
  // CairoSurface::from_c_ptr / gjs_cairo_pattern_from_pattern).
557
- c.setup({
558
- context: (handle) => wrapRaw(Context, handle),
559
- surface: (handle) => wrapRaw(c.surfaceGetType(handle) === SurfaceType.IMAGE ? ImageSurface : Surface, handle),
560
- pattern: (handle) => wrapPatternHandle(handle),
561
- path: (handle) => wrapRaw(Path, handle),
631
+ onNativeLoaded(() => {
632
+ if (native.__cairo === undefined) throw missingBinding();
633
+ c = native.__cairo;
634
+ c.setup({
635
+ context: (handle) => wrapRaw(Context, handle),
636
+ surface: (handle) => wrapSurfaceHandle(handle),
637
+ pattern: (handle) => wrapPatternHandle(handle),
638
+ path: (handle) => wrapRaw(Path, handle),
639
+ });
562
640
  });
563
641
 
564
642
  // The default export mirrors GJS's ESM `cairo` object (enums + constructors), the
package/gettext.js CHANGED
@@ -23,6 +23,7 @@
23
23
  // default export. The gjsify `--app node` build aliases the bare `gettext`
24
24
  // specifier to this module (kept external — `ALIASES_GJS_FOR_NODE`).
25
25
  import * as native from './index.js';
26
+ import { ensureNative } from './index.js';
26
27
  import { requireGi } from './gi.js';
27
28
 
28
29
  // GLib is resolved on first lookup, not at import: loading a namespace walks the
@@ -48,7 +49,10 @@ function GLib() {
48
49
  // lookups still work (they go through GLib, which every addon version has), and
49
50
  // the binders are the no-ops they used to be. It is announced once, because a
50
51
  // silently untranslated UI is the defect this file exists to close.
51
- const haveLocaleBinders = typeof native.localeCategories === 'function';
52
+ function haveLocaleBinders() {
53
+ ensureNative();
54
+ return typeof native.localeCategories === 'function';
55
+ }
52
56
 
53
57
  let announcedStaleAddon = false;
54
58
  function noLocaleBinders(fn) {
@@ -78,6 +82,18 @@ const FALLBACK_CATEGORIES = {
78
82
  win32: { ALL: 0, COLLATE: 1, CTYPE: 2, MONETARY: 3, NUMERIC: 4, TIME: 5, MESSAGES: 1729 },
79
83
  };
80
84
 
85
+ // Resolved on first read, not at import, so a lazy host (NativeScript) that only imports this
86
+ // module has not loaded the addon yet; a failed load throws here, there is no fallback table.
87
+ let categories = null;
88
+ function localeCategories() {
89
+ if (categories === null) {
90
+ categories = haveLocaleBinders()
91
+ ? native.localeCategories()
92
+ : (FALLBACK_CATEGORIES[globalThis.process?.platform] ?? FALLBACK_CATEGORIES.linux);
93
+ }
94
+ return categories;
95
+ }
96
+
81
97
  /** Look up `msgid` in the default domain (the one `textdomain()` last named). */
82
98
  export function gettext(msgid) {
83
99
  return GLib().dgettext(null, msgid);
@@ -145,13 +161,13 @@ export function domain(domainName) {
145
161
  * @returns {string|null} the prior locale name, or null if it could not be set
146
162
  */
147
163
  export function setlocale(category, locale) {
148
- if (!haveLocaleBinders) return noLocaleBinders('setlocale');
164
+ if (!haveLocaleBinders()) return noLocaleBinders('setlocale');
149
165
  return native.setThreadLocale(category, locale ?? null);
150
166
  }
151
167
 
152
168
  /** Set the default text domain, or report it when `domainName` is null. */
153
169
  export function textdomain(domainName) {
154
- if (!haveLocaleBinders) return noLocaleBinders('textdomain');
170
+ if (!haveLocaleBinders()) return noLocaleBinders('textdomain');
155
171
  return native.textdomain(domainName ?? null);
156
172
  }
157
173
 
@@ -162,7 +178,7 @@ export function textdomain(domainName) {
162
178
  * `gjs_bindtextdomain` does — see the note in private.cc.
163
179
  */
164
180
  export function bindtextdomain(domainName, dirName) {
165
- if (!haveLocaleBinders) return noLocaleBinders('bindtextdomain');
181
+ if (!haveLocaleBinders()) return noLocaleBinders('bindtextdomain');
166
182
  return native.bindtextdomain(domainName, dirName ?? null);
167
183
  }
168
184
 
@@ -188,9 +204,15 @@ export function bindtextdomainCodeset(_domainName, _codeset) {
188
204
  * claiming to address LC_MESSAGES — on two of the three platforms the translation
189
205
  * blocker was reported from.
190
206
  */
191
- export const LocaleCategory = haveLocaleBinders
192
- ? native.localeCategories()
193
- : (FALLBACK_CATEGORIES[globalThis.process?.platform] ?? FALLBACK_CATEGORIES.linux);
207
+ export const LocaleCategory = Object.defineProperties(
208
+ {},
209
+ Object.fromEntries(
210
+ Object.keys(FALLBACK_CATEGORIES.linux).map((name) => [
211
+ name,
212
+ { enumerable: true, get: () => localeCategories()[name] },
213
+ ]),
214
+ ),
215
+ );
194
216
 
195
217
  /**
196
218
  * The GJS `Gettext` module as a default export — the object shape
package/gi.js CHANGED
@@ -7,6 +7,7 @@
7
7
  // Reference: gjs/modules/esm/gi.js for the require shape; node-gtk (romgrk, MIT) for the
8
8
  // binding lineage. Hand-authored JS — the package ships no build step.
9
9
  import * as native from './index.js';
10
+ import { ensureNative, onNativeLoaded } from './index.js';
10
11
  // Timers, `process` and the windowing env record come from the host seam (host.node.js
11
12
  // explains why they are the runtime's own, not the globals; host.nativescript.js is the
12
13
  // NativeScript Android variant).
@@ -360,17 +361,19 @@ function makeErrorDomainClass(namespace, typeName, members, quark) {
360
361
  // Called by the engine on a failed GI invoke, with ownership of the GError* already
361
362
  // transferred to the boxed handle. Named, so the position properties report the
362
363
  // application frame rather than this one.
363
- native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
364
- // Same cross-version pairing: an addon older than this JS calls the builder with
365
- // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
366
- // there is no second `errorClass` argument to give it. Rebuilding the GError from
367
- // the fields keeps the ONE shape on that pairing too — it costs only the original
368
- // GError's address, which nothing can read.
369
- if (typeof handle === 'string') {
370
- return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
371
- }
372
- return wrapGError(handle, true, buildThrownGError);
373
- });
364
+ onNativeLoaded(() =>
365
+ native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
366
+ // Same cross-version pairing: an addon older than this JS calls the builder with
367
+ // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
368
+ // there is no second `errorClass` argument to give it. Rebuilding the GError from
369
+ // the fields keeps the ONE shape on that pairing too — it costs only the original
370
+ // GError's address, which nothing can read.
371
+ if (typeof handle === 'string') {
372
+ return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
373
+ }
374
+ return wrapGError(handle, true, buildThrownGError);
375
+ }),
376
+ );
374
377
 
375
378
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
376
379
  // record WHICH introspected class a registration belongs to — matched against the
@@ -443,7 +446,7 @@ function resolveTemplateCallback(handle, handlerName) {
443
446
  return desc.value.apply(proxy, args.map(wrapReturn));
444
447
  };
445
448
  }
446
- native.setTemplateCallbackResolver(resolveTemplateCallback);
449
+ onNativeLoaded(() => native.setTemplateCallbackResolver(resolveTemplateCallback));
447
450
 
448
451
  // Run a registered class's JS constructor for a GObject the ENGINE instantiated from C
449
452
  // — typically a GtkBuilder composite-template InternalChild, which has no JS-`new` to
@@ -466,7 +469,7 @@ function runCtorForCObject(handle, gtypeName) {
466
469
  adoptHandle = prev;
467
470
  }
468
471
  }
469
- native.setConstructCallback(runCtorForCObject);
472
+ onNativeLoaded(() => native.setConstructCallback(runCtorForCObject));
470
473
 
471
474
  // A non-GObject GObject-fundamental (GskRenderNode, GdkEvent) as an OPAQUE,
472
475
  // round-trippable handle: the native tagged External already carries the pointer and
@@ -927,13 +930,50 @@ function decorateGioNamespace(baseNs) {
927
930
  });
928
931
  }
929
932
 
933
+ // The internals of the PROXY links on an introspected prototype chain: the Proxy
934
+ // itself -> its target plus the materializer its getOwnPropertyDescriptor trap runs
935
+ // (`null` for a Proxy that traps no descriptor read). Registered by
936
+ // makeClassPrototype and vfuncChainProto; read only by findProtoDescriptor.
937
+ //
938
+ // Why: a descriptor read THROUGH a Proxy costs ~110 ns of V8 invariant machinery on
939
+ // top of the handler, and an introspected chain is mostly Proxies. Measured on the
940
+ // bench's depth-3 subject, a full MISS walk took 328 ns — 154 ns on the
941
+ // makeClassPrototype link, 65 ns on vfuncChainProto, 12 ns on the one plain link.
942
+ // With the target in hand the same answer costs 27 ns, because the trap body is
943
+ // exactly `materialize(prop); return Reflect.getOwnPropertyDescriptor(target, prop)`
944
+ // and running it directly skips only the machinery, never a step of the computation.
945
+ //
946
+ // This is NOT a cache of the ANSWER: nothing is remembered between calls. The target
947
+ // is the object every mutation of the prototype lands on — none of the handlers trap
948
+ // `set`, `defineProperty`, `deleteProperty` or `setPrototypeOf`, so
949
+ // `Cls.prototype.m = f`, `Object.defineProperty(…)`, `delete` and a spliced chain
950
+ // write through to it — and the descriptor still comes from a fresh read of it. That
951
+ // has to hold, because GJS pins nothing: on gjs 1.88.1 every one of those mutations
952
+ // is observable on an already-wrapped instance (measured), so an answer cache would
953
+ // need an invalidation signal JS does not offer. test/proto-descriptor-liveness.test.mjs
954
+ // is the proof obligation.
955
+ const protoInternals = new WeakMap();
956
+
930
957
  // Walk a JS prototype chain (excluding Object.prototype) for an own property
931
958
  // descriptor of `prop` — used to surface a registerClass subclass's own methods
932
- // /getters/setters on the GObject wrapper.
959
+ // /getters/setters on the GObject wrapper. One full walk runs per property GET, per
960
+ // SET and per `emit`, so the Proxy bypass above is on the hottest path there is.
933
961
  function findProtoDescriptor(proto, prop) {
934
- for (let p = proto; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
935
- const d = Object.getOwnPropertyDescriptor(p, prop);
962
+ for (let p = proto; p !== null && p !== Object.prototype;) {
963
+ const internals = protoInternals.get(p);
964
+ if (internals === undefined) {
965
+ const d = Object.getOwnPropertyDescriptor(p, prop);
966
+ if (d !== undefined) return d;
967
+ p = Object.getPrototypeOf(p);
968
+ continue;
969
+ }
970
+ const { target, materialize } = internals;
971
+ if (materialize !== null) materialize(prop);
972
+ const d = Object.getOwnPropertyDescriptor(target, prop);
936
973
  if (d !== undefined) return d;
974
+ // The target's prototype, not the Proxy's: neither handler traps
975
+ // getPrototypeOf, so these are the same link — one fewer trap to pay.
976
+ p = Object.getPrototypeOf(target);
937
977
  }
938
978
  return undefined;
939
979
  }
@@ -1131,9 +1171,9 @@ function runJsPropertySetter(handle, propertyName) {
1131
1171
  // function` until the next release republishes the binary. Every native call added
1132
1172
  // here degrades instead; without the callback, a post-construction set simply does
1133
1173
  // not reach the JS setter, which is what 0.50.0 did.
1134
- if (typeof native.setPropertySetCallback === 'function') {
1135
- native.setPropertySetCallback(runJsPropertySetter);
1136
- }
1174
+ onNativeLoaded(() => {
1175
+ if (typeof native.setPropertySetCallback === 'function') native.setPropertySetCallback(runJsPropertySetter);
1176
+ });
1137
1177
 
1138
1178
  // Which properties the construct-time replay must run.
1139
1179
  //
@@ -1342,6 +1382,7 @@ export function startMainContextPump() {
1342
1382
  // GLib's default main context — a JS-timer pump would only iterate the
1343
1383
  // context recursively from within its own dispatch.
1344
1384
  if (hostPumpsItself()) return () => {};
1385
+ ensureNative();
1345
1386
  pumpRefCount++;
1346
1387
  if (pumpTimer === null) {
1347
1388
  if (pendingView === null) pendingView = native.makePumpPendingCount();
@@ -1812,7 +1853,8 @@ function wrapInstance(handle, userProto) {
1812
1853
  // the introspected parent's `vfunc_x` is a thunk into the actual C vtable entry.
1813
1854
  // Non-`vfunc_` lookups fall through to Object.prototype (so the base prototype
1814
1855
  // keeps its normal object behaviour).
1815
- const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1856
+ const vfuncChainTarget = Object.create(Object.prototype);
1857
+ const vfuncChainProto = new Proxy(vfuncChainTarget, {
1816
1858
  get(target, prop, receiver) {
1817
1859
  if (typeof prop === 'string' && prop.startsWith('vfunc_')) {
1818
1860
  const vfuncName = prop.slice('vfunc_'.length);
@@ -1827,6 +1869,10 @@ const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1827
1869
  return Reflect.get(target, prop, receiver);
1828
1870
  },
1829
1871
  });
1872
+ // No descriptor trap here: a `vfunc_*` access is synthesized by `get` alone and was
1873
+ // never reported as a descriptor, so the walk's answer for this link always came from
1874
+ // the target. `null` materializer = "read the target, that is the whole trap".
1875
+ protoInternals.set(vfuncChainProto, { target: vfuncChainTarget, materialize: null });
1830
1876
 
1831
1877
  // Surface a lazy `$gtype` getter on an introspected class/struct ctor: the real
1832
1878
  // GObject GType (a node-gi GType handle), resolved on first read via the engine
@@ -1994,7 +2040,7 @@ function makeClassPrototype(namespace, typeName) {
1994
2040
  }
1995
2041
  return arity >= 0 && define(prop, makeGiMethod(where, prop, arity));
1996
2042
  };
1997
- return new Proxy(target, {
2043
+ const proto = new Proxy(target, {
1998
2044
  get(t, prop, receiver) {
1999
2045
  materialize(prop);
2000
2046
  return Reflect.get(t, prop, receiver);
@@ -2007,6 +2053,10 @@ function makeClassPrototype(namespace, typeName) {
2007
2053
  return materialize(prop) || Reflect.has(t, prop);
2008
2054
  },
2009
2055
  });
2056
+ // Let findProtoDescriptor run the descriptor trap's body directly on the target —
2057
+ // same two steps, without V8's per-trap invariant checks (see protoInternals).
2058
+ protoInternals.set(proto, { target, materialize });
2059
+ return proto;
2010
2060
  }
2011
2061
 
2012
2062
  function makeClass(namespace, typeName) {
@@ -2526,6 +2576,8 @@ function paramSpecGTyped(type) {
2526
2576
  const ParamSpec = Object.freeze({
2527
2577
  string: paramSpecPlain('string'),
2528
2578
  boolean: paramSpecPlain('boolean'),
2579
+ char: paramSpecRanged('char'),
2580
+ uchar: paramSpecRanged('uchar'),
2529
2581
  int: paramSpecRanged('int'),
2530
2582
  uint: paramSpecRanged('uint'),
2531
2583
  int64: paramSpecRanged('int64'),
@@ -3190,6 +3242,7 @@ function mirrorWindowingEnvIntoCrt() {
3190
3242
  * @returns {Record<string, unknown>}
3191
3243
  */
3192
3244
  export function requireGi(namespace, version) {
3245
+ ensureNative();
3193
3246
  native.requireNamespace(namespace, version);
3194
3247
  // The one moment the win32 CRT-environment mirror can run: the addon is loaded, so there
3195
3248
  // is a GLib to call, and nothing has built a font map or loaded a fontconfig
@@ -44,6 +44,12 @@
44
44
  /** @type {'nativescript'} */
45
45
  export const RUNTIME = 'nativescript';
46
46
 
47
+ /**
48
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
49
+ * @type {boolean}
50
+ */
51
+ export const LAZY_NATIVE = true;
52
+
47
53
  // Timers from the global scope: NativeScript has no node:timers, and there is no
48
54
  // GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
49
55
  export const setImmediate =
package/host.node.js CHANGED
@@ -55,6 +55,12 @@ export const RUNTIME =
55
55
  ? 'deno'
56
56
  : 'node';
57
57
 
58
+ /**
59
+ * Whether the addon may be loaded on first use instead of at import — see index.js.
60
+ * @type {boolean}
61
+ */
62
+ export const LAZY_NATIVE = false;
63
+
58
64
  // GJS host mode: `GjsifyNapi.init()` installs the loader as
59
65
  // `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
60
66
  // @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
package/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // a native package's JS entry is a loader, not a tsc artifact, and the repo
6
6
  // ignores `lib/`. The addon is Node-API (ABI-stable), so the same binary loads on
7
7
  // Node, Bun and Deno; runtime-specific behaviour is gated off RUNTIME below.
8
- import { RUNTIME, loadNativeHost } from '#host';
8
+ import { RUNTIME, loadNativeHost, LAZY_NATIVE } from '#host';
9
9
 
10
10
  /**
11
11
  * Which JS runtime we are on. Detection — and why each runtime gets the main-loop
@@ -17,121 +17,237 @@ export { RUNTIME };
17
17
  /** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
18
18
  export const isNodeRuntime = RUNTIME === 'node';
19
19
 
20
- const native = loadNativeHost();
20
+ /**
21
+ * The native engine. Desktop runtimes load it HERE, while this module evaluates, exactly as
22
+ * before. A host that sets `LAZY_NATIVE` (NativeScript: dlopen of `libnode_gi.so` plus its
23
+ * closure costs real cold-start time) defers the load to the first `ensureNative()`, so an app
24
+ * that imports this package but never asks for GObject pays nothing. A failed load throws from
25
+ * `ensureNative()` every time it is asked — a selected real engine still fails loudly, there
26
+ * is no fallback here (ADR 0105 § 4).
27
+ * @type {Record<string, any>}
28
+ */
29
+ let native;
30
+ let loaded = false;
31
+ /** @type {Array<() => void>} */
32
+ const loadHooks = [];
33
+
34
+ /**
35
+ * Load the addon if that has not happened yet, and return it. The L1 layers call this before
36
+ * they touch any binding below.
37
+ * @returns {Record<string, any>}
38
+ */
39
+ export function ensureNative() {
40
+ if (loaded) return native;
41
+ const addon = loadNativeHost();
42
+ native = addon;
43
+ requireNamespace = addon.requireNamespace;
44
+ listInfoNames = addon.listInfoNames;
45
+ findInfo = addon.findInfo;
46
+ getConstantValue = addon.getConstantValue;
47
+ getEnumValues = addon.getEnumValues;
48
+ getErrorDomain = addon.getErrorDomain;
49
+ errorDomainInfo = addon.errorDomainInfo;
50
+ setErrorBuilder = addon.setErrorBuilder;
51
+ prependSearchPath = addon.prependSearchPath;
52
+ prependLibraryPath = addon.prependLibraryPath;
53
+ callFunction = addon.callFunction;
54
+ callMethod = addon.callMethod;
55
+ hasMethod = addon.hasMethod;
56
+ hasClassMethod = addon.hasClassMethod;
57
+ classMethodArity = addon.classMethodArity;
58
+ callStaticMethod = addon.callStaticMethod;
59
+ constructStruct = addon.constructStruct;
60
+ newObject = addon.newObject;
61
+ registerClass = addon.registerClass;
62
+ registerClassFromGType = addon.registerClassFromGType;
63
+ constructType = addon.constructType;
64
+ callParentVfunc = addon.callParentVfunc;
65
+ hasClassVfunc = addon.hasClassVfunc;
66
+ callClassVfunc = addon.callClassVfunc;
67
+ getTemplateChild = addon.getTemplateChild;
68
+ getProperty = addon.getProperty;
69
+ setProperty = addon.setProperty;
70
+ hasProperty = addon.hasProperty;
71
+ getTypeName = addon.getTypeName;
72
+ classInfoForTypeName = addon.classInfoForTypeName;
73
+ getGType = addon.getGType;
74
+ isInstanceOf = addon.isInstanceOf;
75
+ isGObjectHandle = addon.isGObjectHandle;
76
+ newGValue = addon.newGValue;
77
+ callBoxedMethod = addon.callBoxedMethod;
78
+ isBoxedHandle = addon.isBoxedHandle;
79
+ boxedMemberKind = addon.boxedMemberKind;
80
+ getBoxedField = addon.getBoxedField;
81
+ setBoxedField = addon.setBoxedField;
82
+ boxedTypeName = addon.boxedTypeName;
83
+ __boxedAddress = addon.__boxedAddress;
84
+ isParamSpecHandle = addon.isParamSpecHandle;
85
+ isFundamentalHandle = addon.isFundamentalHandle;
86
+ paramSpecProp = addon.paramSpecProp;
87
+ variantNew = addon.variantNew;
88
+ variantUnpack = addon.variantUnpack;
89
+ variantGetTypeString = addon.variantGetTypeString;
90
+ isVariantHandle = addon.isVariantHandle;
91
+ startMainLoop = addon.startMainLoop;
92
+ iterateMainContext = addon.iterateMainContext;
93
+ mainContextHasPending = addon.mainContextHasPending;
94
+ makePumpPendingCount = addon.makePumpPendingCount;
95
+ pumpKick = addon.pumpKick;
96
+ setMicrotaskDrain = addon.setMicrotaskDrain;
97
+ connectSignal = addon.connectSignal;
98
+ emitSignal = addon.emitSignal;
99
+ disconnectSignal = addon.disconnectSignal;
100
+ setTemplateCallbackResolver = addon.setTemplateCallbackResolver;
101
+ setConstructCallback = addon.setConstructCallback;
102
+ setPropertySetCallback = addon.setPropertySetCallback;
103
+ storedPropertyNames = addon.storedPropertyNames;
104
+ logSetWriterFunc = addon.logSetWriterFunc;
105
+ logSetWriterDefault = addon.logSetWriterDefault;
106
+ bindPropertyFull = addon.bindPropertyFull;
107
+ bindingGroupBindFull = addon.bindingGroupBindFull;
108
+ setThreadLocale = addon.setThreadLocale;
109
+ textdomain = addon.textdomain;
110
+ bindtextdomain = addon.bindtextdomain;
111
+ localeCategories = addon.localeCategories;
112
+ probeHostOpenGL = addon.probeHostOpenGL;
113
+ currentGLStrings = addon.currentGLStrings;
114
+ loaded = true;
115
+ for (const hook of loadHooks.splice(0)) hook();
116
+ return native;
117
+ }
118
+
119
+ /** Whether the addon is loaded now — what the lazy host makes observable. */
120
+ export function isNativeLoaded() {
121
+ return loaded;
122
+ }
123
+
124
+ /**
125
+ * Run `hook` once the addon is loaded: now if it is, else straight after the first load. The
126
+ * L1 module-scope registrations (error builder, template resolver, …) go through this, so they
127
+ * run at import on desktop and at first use on a lazy host.
128
+ * @param {() => void} hook
129
+ */
130
+ export function onNativeLoaded(hook) {
131
+ if (loaded) hook();
132
+ else loadHooks.push(hook);
133
+ }
21
134
 
22
135
  // Straight re-exports of the native engine surface. Each contract — arguments,
23
136
  // ownership, the OUT/INOUT return-tuple convention, the keep-alive rules of the
24
137
  // main-loop group — is documented once in index.d.ts, the published type surface;
25
138
  // only what index.d.ts does not carry is noted here.
26
- export const requireNamespace = native.requireNamespace;
27
- export const listInfoNames = native.listInfoNames;
28
- export const findInfo = native.findInfo;
29
- export const getConstantValue = native.getConstantValue;
30
- export const getEnumValues = native.getEnumValues;
31
- export const getErrorDomain = native.getErrorDomain;
32
- export const errorDomainInfo = native.errorDomainInfo;
33
- export const setErrorBuilder = native.setErrorBuilder;
34
- export const prependSearchPath = native.prependSearchPath;
35
- export const prependLibraryPath = native.prependLibraryPath;
36
- export const callFunction = native.callFunction;
37
- export const callMethod = native.callMethod;
38
- export const hasMethod = native.hasMethod;
39
- export const hasClassMethod = native.hasClassMethod;
40
- export const classMethodArity = native.classMethodArity;
41
- export const callStaticMethod = native.callStaticMethod;
42
- export const constructStruct = native.constructStruct;
43
- export const newObject = native.newObject;
44
- export const registerClass = native.registerClass;
139
+ export let requireNamespace;
140
+ export let listInfoNames;
141
+ export let findInfo;
142
+ export let getConstantValue;
143
+ export let getEnumValues;
144
+ export let getErrorDomain;
145
+ export let errorDomainInfo;
146
+ export let setErrorBuilder;
147
+ export let prependSearchPath;
148
+ export let prependLibraryPath;
149
+ export let callFunction;
150
+ export let callMethod;
151
+ export let hasMethod;
152
+ export let hasClassMethod;
153
+ export let classMethodArity;
154
+ export let callStaticMethod;
155
+ export let constructStruct;
156
+ export let newObject;
157
+ export let registerClass;
45
158
 
46
159
  // Subclass an ALREADY-REGISTERED parent by its GType handle instead of a
47
160
  // `namespace.typeName`: a registered (dynamic) parent has no introspection entry, so it
48
161
  // cannot be resolved by name. Custom properties, signals and vfunc slots of registered
49
162
  // ancestors compose through ordinary GObject inheritance. The L1 `GObject.registerClass`
50
163
  // picks this variant when the nearest base is itself a registered class.
51
- export const registerClassFromGType = native.registerClassFromGType;
52
-
53
- export const constructType = native.constructType;
54
- export const callParentVfunc = native.callParentVfunc;
55
- export const hasClassVfunc = native.hasClassVfunc;
56
- export const callClassVfunc = native.callClassVfunc;
57
- export const getTemplateChild = native.getTemplateChild;
58
- export const getProperty = native.getProperty;
59
- export const setProperty = native.setProperty;
60
- export const hasProperty = native.hasProperty;
61
- export const getTypeName = native.getTypeName;
62
- export const classInfoForTypeName = native.classInfoForTypeName;
63
- export const getGType = native.getGType;
64
- export const isInstanceOf = native.isInstanceOf;
65
- export const isGObjectHandle = native.isGObjectHandle;
66
- export const newGValue = native.newGValue;
67
- export const callBoxedMethod = native.callBoxedMethod;
68
- export const isBoxedHandle = native.isBoxedHandle;
69
- export const boxedMemberKind = native.boxedMemberKind;
70
- export const getBoxedField = native.getBoxedField;
71
- export const setBoxedField = native.setBoxedField;
72
- export const boxedTypeName = native.boxedTypeName;
164
+ export let registerClassFromGType;
165
+
166
+ export let constructType;
167
+ export let callParentVfunc;
168
+ export let hasClassVfunc;
169
+ export let callClassVfunc;
170
+ export let getTemplateChild;
171
+ export let getProperty;
172
+ export let setProperty;
173
+ export let hasProperty;
174
+ export let getTypeName;
175
+ export let classInfoForTypeName;
176
+ export let getGType;
177
+ export let isInstanceOf;
178
+ export let isGObjectHandle;
179
+ export let newGValue;
180
+ export let callBoxedMethod;
181
+ export let isBoxedHandle;
182
+ export let boxedMemberKind;
183
+ export let getBoxedField;
184
+ export let setBoxedField;
185
+ export let boxedTypeName;
73
186
  /** TEST-ONLY: the address a boxed handle wraps, so an ownership rule about pointer
74
187
  * IDENTITY can be asserted instead of waited on (see marshal.cc BoxedAddress). */
75
- export const __boxedAddress = native.__boxedAddress;
76
- export const isParamSpecHandle = native.isParamSpecHandle;
188
+ export let __boxedAddress;
189
+ export let isParamSpecHandle;
77
190
 
78
191
  // True for a non-GObject GObject-fundamental handle (a GskRenderNode from
79
192
  // Gtk.Snapshot.to_node, a GdkEvent): introspected as object info, but ref-counted
80
193
  // through its own ref/unref funcs, NOT g_object_ref. L1 surfaces it as an opaque,
81
194
  // round-trippable pass-through handle.
82
- export const isFundamentalHandle = native.isFundamentalHandle;
83
-
84
- export const paramSpecProp = native.paramSpecProp;
85
- export const variantNew = native.variantNew;
86
- export const variantUnpack = native.variantUnpack;
87
- export const variantGetTypeString = native.variantGetTypeString;
88
- export const isVariantHandle = native.isVariantHandle;
89
- export const startMainLoop = native.startMainLoop;
90
- export const iterateMainContext = native.iterateMainContext;
91
- export const mainContextHasPending = native.mainContextHasPending;
92
- export const makePumpPendingCount = native.makePumpPendingCount;
93
- export const pumpKick = native.pumpKick;
94
- export const setMicrotaskDrain = native.setMicrotaskDrain;
95
- export const connectSignal = native.connectSignal;
96
- export const emitSignal = native.emitSignal;
97
- export const disconnectSignal = native.disconnectSignal;
98
- export const setTemplateCallbackResolver = native.setTemplateCallbackResolver;
195
+ export let isFundamentalHandle;
196
+
197
+ export let paramSpecProp;
198
+ export let variantNew;
199
+ export let variantUnpack;
200
+ export let variantGetTypeString;
201
+ export let isVariantHandle;
202
+ export let startMainLoop;
203
+ export let iterateMainContext;
204
+ export let mainContextHasPending;
205
+ export let makePumpPendingCount;
206
+ export let pumpKick;
207
+ export let setMicrotaskDrain;
208
+ export let connectSignal;
209
+ export let emitSignal;
210
+ export let disconnectSignal;
211
+ export let setTemplateCallbackResolver;
99
212
 
100
213
  // Registers the L1 callback the engine's overridden `constructor` vfunc invokes to run a
101
214
  // registered class's JS constructor for a GObject that C instantiated (a GtkBuilder
102
215
  // composite-template InternalChild): given (instanceHandle, gtypeName) it
103
216
  // Reflect.constructs the class in adopt mode — see gi.js runCtorForCObject.
104
- export const setConstructCallback = native.setConstructCallback;
217
+ export let setConstructCallback;
105
218
 
106
219
  // Registers the L1 callback the engine's set_property vfunc invokes after storing a
107
220
  // custom property: given (instanceHandle, propertyName) it runs the class's own JS
108
221
  // setter — see gi.js runJsPropertySetter. The engine calls it only once the instance
109
222
  // HAS a wrapper, so a construct-time set never reaches it; those are replayed from
110
223
  // the base ctor over storedPropertyNames.
111
- export const setPropertySetCallback = native.setPropertySetCallback;
224
+ export let setPropertySetCallback;
112
225
 
113
226
  // The custom properties actually SET on an instance (the per-instance store's keys) —
114
227
  // what that construct-time replay has to push through the class's JS setters.
115
- export const storedPropertyNames = native.storedPropertyNames;
228
+ export let storedPropertyNames;
116
229
 
117
- export const logSetWriterFunc = native.logSetWriterFunc;
118
- export const logSetWriterDefault = native.logSetWriterDefault;
119
- export const bindPropertyFull = native.bindPropertyFull;
120
- export const bindingGroupBindFull = native.bindingGroupBindFull;
230
+ export let logSetWriterFunc;
231
+ export let logSetWriterDefault;
232
+ export let bindPropertyFull;
233
+ export let bindingGroupBindFull;
121
234
 
122
235
  // The libintl half of GjsPrivate — the binders GLib's GIR does not publish, plus
123
236
  // the LC_* constants read from the host's own <locale.h>. Consumed by
124
237
  // `./gettext.js`; loading THIS module is also what puts the process in the
125
238
  // environment's locale (the addon's Init does it, as gjs's entry point does).
126
- export const setThreadLocale = native.setThreadLocale;
127
- export const textdomain = native.textdomain;
128
- export const bindtextdomain = native.bindtextdomain;
129
- export const localeCategories = native.localeCategories;
239
+ export let setThreadLocale;
240
+ export let textdomain;
241
+ export let bindtextdomain;
242
+ export let localeCategories;
130
243
 
131
244
  // win32 only (undefined elsewhere): which OpenGL the process got — see gtk-runtime.js
132
245
  // activateBundledOpenGL. currentGLStrings asks the implementation behind the context
133
246
  // current on this thread, so a caller can tell Mesa from a vendor driver.
134
- export const probeHostOpenGL = native.probeHostOpenGL;
135
- export const currentGLStrings = native.currentGLStrings;
247
+ export let probeHostOpenGL;
248
+ export let currentGLStrings;
249
+
250
+ export { native as default };
136
251
 
137
- export default native;
252
+ // Last, so every `export let` above is initialised before the eager load assigns it.
253
+ if (!LAZY_NATIVE) ensureNative();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.64.1",
3
+ "version": "0.64.2",
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/cairo.cc CHANGED
@@ -621,6 +621,11 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
621
621
  c.Set("paint", Napi::Function::New(env, CTX_0(cairo_paint)));
622
622
  c.Set("clip", Napi::Function::New(env, CTX_0(cairo_clip)));
623
623
  c.Set("clipPreserve", Napi::Function::New(env, CTX_0(cairo_clip_preserve)));
624
+ c.Set("resetClip", Napi::Function::New(env, CTX_0(cairo_reset_clip)));
625
+ c.Set("pushGroup", Napi::Function::New(env, CTX_0(cairo_push_group)));
626
+ c.Set("popGroupToSource", Napi::Function::New(env, CTX_0(cairo_pop_group_to_source)));
627
+ c.Set("copyPage", Napi::Function::New(env, CTX_0(cairo_copy_page)));
628
+ c.Set("showPage", Napi::Function::New(env, CTX_0(cairo_show_page)));
624
629
 
625
630
  c.Set("setLineWidth", Napi::Function::New(env, CTX_1D(cairo_set_line_width)));
626
631
  c.Set("paintWithAlpha", Napi::Function::New(env, CTX_1D(cairo_paint_with_alpha)));
@@ -641,7 +646,10 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
641
646
  c.Set("arc", Napi::Function::New(env, CTX_5D(cairo_arc)));
642
647
  c.Set("arcNegative", Napi::Function::New(env, CTX_5D(cairo_arc_negative)));
643
648
  c.Set("curveTo", Napi::Function::New(env, CTX_6D(cairo_curve_to)));
649
+ c.Set("relCurveTo", Napi::Function::New(env, CTX_6D(cairo_rel_curve_to)));
644
650
 
651
+ c.Set("pushGroupWithContent",
652
+ Napi::Function::New(env, CTX_ENUM(cairo_push_group_with_content, cairo_content_t)));
645
653
  c.Set("setOperator", Napi::Function::New(env, CTX_ENUM(cairo_set_operator, cairo_operator_t)));
646
654
  c.Set("setLineCap", Napi::Function::New(env, CTX_ENUM(cairo_set_line_cap, cairo_line_cap_t)));
647
655
  c.Set("setLineJoin", Napi::Function::New(env, CTX_ENUM(cairo_set_line_join, cairo_line_join_t)));
@@ -798,6 +806,45 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
798
806
  o.Set("maxYAdvance", Napi::Number::New(env, e.max_y_advance));
799
807
  return o;
800
808
  }));
809
+ // The font matrix is a plain `{xx, yx, xy, yy, x0, y0}` object in GJS — cairo's
810
+ // matrix type has no wrapper there (getMatrix/setMatrix stay unbound for that
811
+ // reason), so the font matrix travels as the six doubles.
812
+ c.Set("getFontMatrix", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
813
+ Napi::Env env = info.Env();
814
+ cairo_t* cr = CtxArg(env, info);
815
+ if (cr == nullptr) return env.Undefined();
816
+ cairo_matrix_t m;
817
+ cairo_get_font_matrix(cr, &m);
818
+ if (!CheckStatus(env, cairo_status(cr), "context")) return env.Undefined();
819
+ Napi::Object o = Napi::Object::New(env);
820
+ o.Set("xx", Napi::Number::New(env, m.xx));
821
+ o.Set("yx", Napi::Number::New(env, m.yx));
822
+ o.Set("xy", Napi::Number::New(env, m.xy));
823
+ o.Set("yy", Napi::Number::New(env, m.yy));
824
+ o.Set("x0", Napi::Number::New(env, m.x0));
825
+ o.Set("y0", Napi::Number::New(env, m.y0));
826
+ return o;
827
+ }));
828
+ c.Set("setFontMatrix", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
829
+ Napi::Env env = info.Env();
830
+ cairo_t* cr = CtxArg(env, info);
831
+ if (cr == nullptr) return env.Undefined();
832
+ if (!info[1].IsObject()) {
833
+ Napi::TypeError::New(env, "matrix must be an object").ThrowAsJavaScriptException();
834
+ return env.Undefined();
835
+ }
836
+ Napi::Object o = info[1].As<Napi::Object>();
837
+ cairo_matrix_t m;
838
+ m.xx = o.Get("xx").ToNumber().DoubleValue();
839
+ m.yx = o.Get("yx").ToNumber().DoubleValue();
840
+ m.xy = o.Get("xy").ToNumber().DoubleValue();
841
+ m.yy = o.Get("yy").ToNumber().DoubleValue();
842
+ m.x0 = o.Get("x0").ToNumber().DoubleValue();
843
+ m.y0 = o.Get("y0").ToNumber().DoubleValue();
844
+ cairo_set_font_matrix(cr, &m);
845
+ CheckStatus(env, cairo_status(cr), "context");
846
+ return env.Undefined();
847
+ }));
801
848
  c.Set("status", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
802
849
  Napi::Env env = info.Env();
803
850
  cairo_t* cr = CtxArg(env, info);
@@ -824,6 +871,26 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
824
871
  CheckStatus(env, cairo_status(cr), "context");
825
872
  return env.Undefined();
826
873
  }));
874
+ c.Set("mask", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
875
+ Napi::Env env = info.Env();
876
+ cairo_t* cr = CtxArg(env, info);
877
+ if (cr == nullptr) return env.Undefined();
878
+ cairo_pattern_t* p = PatternFromExt(env, info[1]);
879
+ if (p == nullptr) return env.Undefined();
880
+ cairo_mask(cr, p);
881
+ CheckStatus(env, cairo_status(cr), "context");
882
+ return env.Undefined();
883
+ }));
884
+ c.Set("maskSurface", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
885
+ Napi::Env env = info.Env();
886
+ cairo_t* cr = CtxArg(env, info);
887
+ if (cr == nullptr) return env.Undefined();
888
+ cairo_surface_t* s = SurfaceFromExt(env, info[1]);
889
+ if (s == nullptr) return env.Undefined();
890
+ cairo_mask_surface(cr, s, Num(info, 2), Num(info, 3));
891
+ CheckStatus(env, cairo_status(cr), "context");
892
+ return env.Undefined();
893
+ }));
827
894
 
828
895
  c.Set("identityMatrix", Napi::Function::New(env, CTX_0(cairo_identity_matrix)));
829
896
  c.Set("newSubPath", Napi::Function::New(env, CTX_0(cairo_new_sub_path)));
@@ -970,6 +1037,35 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
970
1037
  return MakeCairoHandle(env, p, CairoKind::Pattern);
971
1038
  }));
972
1039
 
1040
+ // popGroup: cairo_pop_group returns a NEW pattern (transfer-full) — the handle
1041
+ // adopts that ref directly, no extra reference (gjs wraps and then drops its own).
1042
+ c.Set("popGroup", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
1043
+ Napi::Env env = info.Env();
1044
+ cairo_t* cr = CtxArg(env, info);
1045
+ if (cr == nullptr) return env.Undefined();
1046
+ cairo_pattern_t* p = cairo_pop_group(cr);
1047
+ if (!CheckStatus(env, cairo_status(cr), "context")) {
1048
+ cairo_pattern_destroy(p);
1049
+ return env.Undefined();
1050
+ }
1051
+ return MakeCairoHandle(env, p, CairoKind::Pattern);
1052
+ }));
1053
+
1054
+ // getTarget/getGroupTarget: the surface belongs to the context — ref it so the
1055
+ // JS wrapper owns one (mirrors CairoSurface::from_c_ptr's kept reference).
1056
+ #define CTX_SURFACE_GETTER(NAME, CFN) \
1057
+ c.Set(NAME, Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value { \
1058
+ Napi::Env env = info.Env(); \
1059
+ cairo_t* cr = CtxArg(env, info); \
1060
+ if (cr == nullptr) return env.Undefined(); \
1061
+ cairo_surface_t* s = CFN(cr); \
1062
+ if (!CheckStatus(env, cairo_status(cr), "context")) return env.Undefined(); \
1063
+ cairo_surface_reference(s); \
1064
+ return MakeCairoHandle(env, s, CairoKind::Surface); \
1065
+ }));
1066
+ CTX_SURFACE_GETTER("getTarget", cairo_get_target)
1067
+ CTX_SURFACE_GETTER("getGroupTarget", cairo_get_group_target)
1068
+
973
1069
  // ---- $dispose (Context / Surface / Pattern / Path) ----
974
1070
  // Eagerly destroy the underlying cairo object + mark the handle so the finalizer
975
1071
  // won't double-free and any further method call throws (a live-handle check).
package/src/class.cc CHANGED
@@ -44,6 +44,8 @@ static GType UnwrapGType(Napi::Env env, Napi::Value v) {
44
44
  static GType TypeNameToGType(const std::string& t) {
45
45
  if (t == "string" || t == "utf8") return G_TYPE_STRING;
46
46
  if (t == "boolean" || t == "bool") return G_TYPE_BOOLEAN;
47
+ if (t == "char") return G_TYPE_CHAR;
48
+ if (t == "uchar") return G_TYPE_UCHAR;
47
49
  if (t == "int") return G_TYPE_INT;
48
50
  if (t == "uint") return G_TYPE_UINT;
49
51
  if (t == "int64") return G_TYPE_INT64;
@@ -95,6 +97,19 @@ static GParamSpec* BuildParamSpec(Napi::Env env, Napi::Object spec, std::string*
95
97
  : (def.IsNumber() ? NodeGiToBool(def) : FALSE);
96
98
  return g_param_spec_boolean(nm, nm, nm, d, flags);
97
99
  }
100
+ if (type == "char") {
101
+ return g_param_spec_char(nm, nm, nm, hasMin ? static_cast<gint8>(mn) : G_MININT8,
102
+ hasMax ? static_cast<gint8>(mx) : G_MAXINT8,
103
+ def.IsNumber() ? static_cast<gint8>(def.As<Napi::Number>().Int32Value()) : 0,
104
+ flags);
105
+ }
106
+ if (type == "uchar") {
107
+ return g_param_spec_uchar(nm, nm, nm, hasMin ? static_cast<guint8>(mn) : 0,
108
+ hasMax ? static_cast<guint8>(mx) : G_MAXUINT8,
109
+ def.IsNumber() ? static_cast<guint8>(def.As<Napi::Number>().Uint32Value())
110
+ : 0,
111
+ flags);
112
+ }
98
113
  if (type == "int") {
99
114
  return g_param_spec_int(nm, nm, nm, hasMin ? static_cast<gint>(mn) : G_MININT,
100
115
  hasMax ? static_cast<gint>(mx) : G_MAXINT,
package/src/common.h CHANGED
@@ -363,7 +363,7 @@ inline NodeGiIntResult NodeGiJsToInteger(Napi::Value v, GITypeTag tag, GIArgumen
363
363
  }
364
364
  }
365
365
 
366
- enum class NodeGiIntContext { kArgument, kField, kReturnValue };
366
+ enum class NodeGiIntContext { kArgument, kField, kReturnValue, kListElement, kHashElement };
367
367
  inline const char* NodeGiIntTagName(GITypeTag tag) {
368
368
  switch (tag) {
369
369
  case GI_TYPE_TAG_INT8: return "int8";
@@ -385,13 +385,31 @@ inline void NodeGiThrowIntOutOfRange(Napi::Env env, GITypeTag tag, NodeGiIntCont
385
385
  msg = std::string("Argument ") + (name != nullptr ? name : "") + ": value is out of range for " + t;
386
386
  } else {
387
387
  msg = "value " + shown + " is out of range for " +
388
- (ctx == NodeGiIntContext::kField ? std::string("Field '") + (name ? name : "") + "'"
389
- : std::string("Return value")) +
388
+ (ctx == NodeGiIntContext::kField
389
+ ? std::string("Field '") + (name ? name : "") + "'"
390
+ : ctx == NodeGiIntContext::kListElement ? std::string("List element")
391
+ : ctx == NodeGiIntContext::kHashElement ? std::string("Hash element")
392
+ : std::string("Return value")) +
390
393
  " (type " + t + ")";
391
394
  }
392
395
  Napi::Error::New(env, msg).ThrowAsJavaScriptException();
393
396
  }
394
397
 
398
+ // The text gjs_debug_value (refs/gjs/gi/../jsapi-util-string.cpp) prints for the offending
399
+ // value in a list/hash element error: a Number goes through C++ ostream (%g, 6 digits), a
400
+ // BigInt is shown as its signed 64-bit value with a "(modulo 2^64)" suffix.
401
+ inline std::string NodeGiDebugValue(Napi::Value v) {
402
+ if (v.IsBigInt()) {
403
+ bool lossless = false;
404
+ return std::to_string(v.As<Napi::BigInt>().Int64Value(&lossless)) + "n (modulo 2^64)";
405
+ }
406
+ Napi::Number n = v.ToNumber();
407
+ if (n.IsEmpty()) return std::string();
408
+ char buf[G_ASCII_DTOSTR_BUF_SIZE];
409
+ g_ascii_formatd(buf, sizeof(buf), "%g", n.DoubleValue()); // locale-independent, like ostream
410
+ return buf;
411
+ }
412
+
395
413
  // IN array elements and GValue int/uint, GJS-exact: these WRAP instead of refusing.
396
414
  // An array element goes through js_value_to_c_strict (refs/gjs/gi/arg.cpp:543), which
397
415
  // is ToInt32/ToUint32 truncated to the tag for 8/16/32 bits and ToInt64/ToUint64 —
package/src/marshal.cc CHANGED
@@ -1060,6 +1060,8 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byVa
1060
1060
  case GI_TYPE_TAG_UTF8:
1061
1061
  case GI_TYPE_TAG_FILENAME:
1062
1062
  return true;
1063
+ // A gunichar is a 4-byte cell; only a C array writes one (no list/hash node).
1064
+ case GI_TYPE_TAG_UNICHAR: return cArrayCell;
1063
1065
  case GI_TYPE_TAG_INTERFACE: {
1064
1066
  GIBaseInfo* iface = gi_type_info_get_interface(elem);
1065
1067
  bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface));
@@ -1178,7 +1180,8 @@ size_t CElementSize(GITypeInfo* elem) {
1178
1180
  case GI_TYPE_TAG_INT16:
1179
1181
  case GI_TYPE_TAG_UINT16: return 2;
1180
1182
  case GI_TYPE_TAG_INT32:
1181
- case GI_TYPE_TAG_UINT32: return 4;
1183
+ case GI_TYPE_TAG_UINT32:
1184
+ case GI_TYPE_TAG_UNICHAR: return 4;
1182
1185
  case GI_TYPE_TAG_INT64:
1183
1186
  case GI_TYPE_TAG_UINT64: return 8;
1184
1187
  case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
@@ -1601,16 +1604,16 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
1601
1604
  // the element, so it gets a ref (objects) or an independent copy (boxed) instead.
1602
1605
  // Throws + returns false on an unsupported element.
1603
1606
  static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
1604
- GITransfer elemTransfer, bool wrapIntegers = false) {
1607
+ GITransfer elemTransfer, NodeGiIntContext intContext = NodeGiIntContext::kListElement,
1608
+ bool wrapIntegers = false) {
1605
1609
  memset(a, 0, sizeof(*a));
1606
1610
  switch (gi_type_info_get_tag(elem)) {
1607
1611
  // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
1608
1612
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
1609
1613
  // into Error::New(nullptr)'s fatal sites.
1610
1614
  case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
1611
- // Integers WRAP here, in an array (arg.cpp gjs_array_to_auto_array) — see
1612
- // NodeGiJsToIntegerWrap (common.h). A GList/GHashTable element keeps the older
1613
- // coercion: gjs's rule for those is not probed yet.
1615
+ // Integers WRAP in an array (arg.cpp gjs_array_to_auto_array, NodeGiJsToIntegerWrap in
1616
+ // common.h) and are REFUSED out of range in a GList/GHashTable node.
1614
1617
  case GI_TYPE_TAG_INT8:
1615
1618
  case GI_TYPE_TAG_UINT8:
1616
1619
  case GI_TYPE_TAG_INT16:
@@ -1621,16 +1624,20 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
1621
1624
  case GI_TYPE_TAG_UINT64:
1622
1625
  if (wrapIntegers)
1623
1626
  return NodeGiJsToIntegerWrap(v, gi_type_info_get_tag(elem), a) == NodeGiIntResult::kOk;
1624
- switch (gi_type_info_get_tag(elem)) {
1625
- case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
1626
- case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
1627
- case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
1628
- case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
1629
- case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
1630
- case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
1631
- case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
1632
- default: a->v_uint64 = JsValueToUint64(v); return true;
1627
+ // A list/hash node is checked, not wrapped (arg.cpp gjs_value_to_basic_gi_argument
1628
+ // -> gjs_arg_set_from_js_value): out of range is a plain Error.
1629
+ switch (NodeGiJsToInteger(v, gi_type_info_get_tag(elem), a, nullptr)) {
1630
+ case NodeGiIntResult::kPending: return false;
1631
+ case NodeGiIntResult::kOutOfRange:
1632
+ NodeGiThrowIntOutOfRange(env, gi_type_info_get_tag(elem), intContext, nullptr,
1633
+ NodeGiDebugValue(v));
1634
+ return false;
1635
+ case NodeGiIntResult::kOk: return true;
1633
1636
  }
1637
+ return true;
1638
+ // gjs_array_to_auto_array<char32_t> wraps like uint32 (arg.cpp js_value_to_c_strict).
1639
+ case GI_TYPE_TAG_UNICHAR:
1640
+ return NodeGiJsToIntegerWrap(v, GI_TYPE_TAG_UINT32, a) == NodeGiIntResult::kOk;
1634
1641
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1635
1642
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1636
1643
  case GI_TYPE_TAG_GTYPE: {
@@ -2267,6 +2274,20 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2267
2274
  return true;
2268
2275
  }
2269
2276
 
2277
+ // A JS string as a gunichar C array → its code points (arg.cpp:584, gjs_string_to_ucs4).
2278
+ if (at == GI_ARRAY_TYPE_C && etag == GI_TYPE_TAG_UNICHAR && v.IsString()) {
2279
+ std::string s = v.As<Napi::String>().Utf8Value();
2280
+ glong n = 0;
2281
+ gunichar* ucs = g_utf8_to_ucs4_fast(s.c_str(), static_cast<glong>(s.size()), &n);
2282
+ void* buf = g_malloc0(elemSize * (n + (zt ? 1 : 0)));
2283
+ if (n > 0) memcpy(buf, ucs, elemSize * n);
2284
+ g_free(ucs);
2285
+ *outPtr = buf;
2286
+ *outCount = static_cast<long>(n);
2287
+ gi_base_info_unref(elem);
2288
+ return true;
2289
+ }
2290
+
2270
2291
  if (!v.IsArray()) {
2271
2292
  gi_base_info_unref(elem);
2272
2293
  Napi::TypeError::New(env, "expected an array for the array argument")
@@ -2301,7 +2322,7 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2301
2322
  }
2302
2323
  GIArgument a;
2303
2324
  ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer,
2304
- /*wrapIntegers=*/true);
2325
+ NodeGiIntContext::kListElement, /*wrapIntegers=*/true);
2305
2326
  if (!ok) break;
2306
2327
  // Copy the element's storage bytes (LE: the low elemSize bytes of the union
2307
2328
  // alias the active field — v_string/v_pointer for pointers, the scalar
@@ -2448,7 +2469,8 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2448
2469
  } else {
2449
2470
  GIArgument ka;
2450
2471
  Napi::Value keyNum = Napi::Number::New(env, static_cast<double>(g_ascii_strtoll(ks.c_str(), nullptr, 10)));
2451
- ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING);
2472
+ ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING,
2473
+ NodeGiIntContext::kHashElement, /*wrapIntegers=*/true);
2452
2474
  if (!ok) break;
2453
2475
  kp = gi_type_info_hash_pointer_from_argument(kt, &ka);
2454
2476
  }
@@ -2459,7 +2481,7 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2459
2481
  Napi::Value val = obj.Get(key);
2460
2482
  gpointer vp = nullptr;
2461
2483
  GIArgument va;
2462
- ok = ElementToGIArgument(env, vt, val, &va, elemTransfer);
2484
+ ok = ElementToGIArgument(env, vt, val, &va, elemTransfer, NodeGiIntContext::kHashElement);
2463
2485
  if (!ok) {
2464
2486
  g_free(kp);
2465
2487
  break;