@gjsify/node-gi 0.64.0 → 0.64.1

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
@@ -17,6 +17,25 @@ export const Operator: CairoEnum;
17
17
  export const PatternType: CairoEnum;
18
18
  export const SurfaceType: CairoEnum;
19
19
 
20
+ /** `cairo_text_extents_t`, in the shape GJS returns. */
21
+ export interface TextExtents {
22
+ xBearing: number;
23
+ yBearing: number;
24
+ width: number;
25
+ height: number;
26
+ xAdvance: number;
27
+ yAdvance: number;
28
+ }
29
+
30
+ /** `cairo_font_extents_t`, in the shape GJS returns. */
31
+ export interface FontExtents {
32
+ ascent: number;
33
+ descent: number;
34
+ height: number;
35
+ maxXAdvance: number;
36
+ maxYAdvance: number;
37
+ }
38
+
20
39
  /** cairo drawing context (`cairo_t`). */
21
40
  export class Context {
22
41
  constructor(surface: Surface);
@@ -28,6 +47,12 @@ export class Context {
28
47
  lineTo(x: number, y: number): void;
29
48
  relMoveTo(dx: number, dy: number): void;
30
49
  relLineTo(dx: number, dy: number): void;
50
+ selectFontFace(family: string, slant: number, weight: number): void;
51
+ setFontSize(size: number): void;
52
+ showText(utf8: string): void;
53
+ textPath(utf8: string): void;
54
+ textExtents(utf8: string): TextExtents;
55
+ fontExtents(): FontExtents;
31
56
  rectangle(x: number, y: number, width: number, height: number): void;
32
57
  arc(xc: number, yc: number, radius: number, angle1: number, angle2: number): void;
33
58
  arcNegative(xc: number, yc: number, radius: number, angle1: number, angle2: number): void;
package/cairo.js CHANGED
@@ -332,6 +332,29 @@ export class Context {
332
332
  fillExtents() {
333
333
  return c.fillExtents(this[H]);
334
334
  }
335
+ /** Select the toy-API font: `family` with a {@link FontSlant} and {@link FontWeight}. */
336
+ selectFontFace(family, slant, weight) {
337
+ c.selectFontFace(this[H], family, slant, weight);
338
+ }
339
+ setFontSize(size) {
340
+ c.setFontSize(this[H], size);
341
+ }
342
+ /** Draw `utf8` at the current point with the current source. */
343
+ showText(utf8) {
344
+ c.showText(this[H], utf8);
345
+ }
346
+ /** Append the outline of `utf8` to the current path. */
347
+ textPath(utf8) {
348
+ c.textPath(this[H], utf8);
349
+ }
350
+ /** `{ xBearing, yBearing, width, height, xAdvance, yAdvance }` of `utf8` in the current font. */
351
+ textExtents(utf8) {
352
+ return c.textExtents(this[H], utf8);
353
+ }
354
+ /** `{ ascent, descent, height, maxXAdvance, maxYAdvance }` of the current font. */
355
+ fontExtents() {
356
+ return c.fontExtents(this[H]);
357
+ }
335
358
  /** Stroke bounding box as `[x1, y1, x2, y2]`. */
336
359
  strokeExtents() {
337
360
  return c.strokeExtents(this[H]);
package/gi.js CHANGED
@@ -48,11 +48,21 @@ function camelToSnake(name) {
48
48
  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
49
49
  }
50
50
 
51
+ // Memoized: the wrapper's get/set/has traps convert on EVERY property access, the
52
+ // answer depends on nothing but the name, and the set of names a program uses is
53
+ // bounded by its source text — three regex passes per access buy nothing.
54
+ const kebabNames = new Map();
55
+
51
56
  function toKebab(name) {
52
- return name
53
- .replace(/([A-Z])/g, '-$1')
54
- .replace(/_/g, '-')
55
- .toLowerCase();
57
+ let kebab = kebabNames.get(name);
58
+ if (kebab === undefined) {
59
+ kebab = name
60
+ .replace(/([A-Z])/g, '-$1')
61
+ .replace(/_/g, '-')
62
+ .toLowerCase();
63
+ kebabNames.set(name, kebab);
64
+ }
65
+ return kebab;
56
66
  }
57
67
 
58
68
  // Repeated passes of the same callback must marshal to the SAME native-facing function:
@@ -221,26 +231,10 @@ function wrapGError(handle, withPosition = false, hideAbove = wrapGError) {
221
231
  // domain must already name something, so `new GLib.Error()` is refused here rather
222
232
  // than producing a GError no `matches()` can ever hit.
223
233
  function newErrorLiteral(domain, code, message) {
224
- const quark = toDomainQuark(domain);
225
- if (native.callFunction('GLib', 'quark_to_string', [quark]) === null) {
234
+ if (native.callFunction('GLib', 'quark_to_string', [domain]) === null) {
226
235
  throw new TypeError(`Error.new_literal: ${domain} is not a valid domain`);
227
236
  }
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;
237
+ return native.callStaticMethod('GLib', 'Error', 'new_literal', [domain, code, message]);
244
238
  }
245
239
 
246
240
  // `GLib.Error`: a plain FUNCTION, not a `class … extends Error` — GJS's
@@ -303,7 +297,7 @@ Object.defineProperties(GLibError.prototype, {
303
297
  `method GLib.Error.matches: At least 2 arguments required, but only ${arguments.length} passed`,
304
298
  );
305
299
  }
306
- return native.callBoxedMethod(this[HANDLE], 'matches', [toDomainQuark(domain), code]);
300
+ return native.callBoxedMethod(this[HANDLE], 'matches', [domain, code]);
307
301
  }),
308
302
  });
309
303
  defineErrorFields(GLibError.prototype, 'GLib.Error', undefined);
@@ -396,6 +390,13 @@ const promisifiedWrappers = new WeakSet();
396
390
  const nativeThunks = new WeakSet();
397
391
 
398
392
  function wrapReturn(value) {
393
+ // EVERY engine value comes through here — each return, each OUT tuple element and
394
+ // each signal argument (wrapSignalCallback) — and the five tag probes below are
395
+ // five N-API crossings. A handle is always an OBJECT (a tagged napi External), so a
396
+ // primitive can answer for itself: a `notify::label` handler's `(emitter, pspec)`
397
+ // then costs two probes for the emitter instead of ten for the pair, and the string
398
+ // argument of an ordinary `emit('changed', 'x')` costs none.
399
+ if (value === null || typeof value !== 'object') return value;
399
400
  if (native.isGObjectHandle(value)) return wrapInstance(value);
400
401
  // A GLib.Variant is ALSO a boxed handle by tag, so it must be checked first to get
401
402
  // the Variant ergonomics rather than a plain method-routing proxy.
@@ -543,36 +544,51 @@ function wrapBoxed(handle) {
543
544
  // GParamSpec-typed value surface as. Carries [HANDLE] so it round-trips back into the
544
545
  // engine and `instanceof GObject.ParamSpec` recognises it. value_type/owner_type are
545
546
  // native GType handles, like gjs's GType objects.
547
+ // The accessors live HERE, on one shared prototype, and read the handle off `this`.
548
+ // Built per instance they were eleven fresh closures plus a twelve-slot object on
549
+ // EVERY notify emission — the engine hands the pspec to the handler whether or not
550
+ // the handler looks at it, so that allocation landed on every property write of the
551
+ // real-GObject engine. On gjs these same members live on GObject.ParamSpec.prototype,
552
+ // so prototype-held is also the closer shape.
553
+ const paramSpecProto = {
554
+ get name() {
555
+ return native.paramSpecProp(this[HANDLE], 'name');
556
+ },
557
+ get nick() {
558
+ return native.paramSpecProp(this[HANDLE], 'nick');
559
+ },
560
+ get blurb() {
561
+ return native.paramSpecProp(this[HANDLE], 'blurb');
562
+ },
563
+ get flags() {
564
+ return native.paramSpecProp(this[HANDLE], 'flags');
565
+ },
566
+ get value_type() {
567
+ return native.paramSpecProp(this[HANDLE], 'valueType');
568
+ },
569
+ get owner_type() {
570
+ return native.paramSpecProp(this[HANDLE], 'ownerType');
571
+ },
572
+ get default_value() {
573
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
574
+ },
575
+ get_name() {
576
+ return native.paramSpecProp(this[HANDLE], 'name');
577
+ },
578
+ get_nick() {
579
+ return native.paramSpecProp(this[HANDLE], 'nick');
580
+ },
581
+ get_blurb() {
582
+ return native.paramSpecProp(this[HANDLE], 'blurb');
583
+ },
584
+ get_default_value() {
585
+ return wrapReturn(native.paramSpecProp(this[HANDLE], 'defaultValue'));
586
+ },
587
+ };
588
+
546
589
  function wrapParamSpec(handle) {
547
- const prop = (which) => native.paramSpecProp(handle, which);
548
- const pspec = {
549
- [HANDLE]: handle,
550
- get name() {
551
- return prop('name');
552
- },
553
- get nick() {
554
- return prop('nick');
555
- },
556
- get blurb() {
557
- return prop('blurb');
558
- },
559
- get flags() {
560
- return prop('flags');
561
- },
562
- get value_type() {
563
- return prop('valueType');
564
- },
565
- get owner_type() {
566
- return prop('ownerType');
567
- },
568
- get default_value() {
569
- return wrapReturn(prop('defaultValue'));
570
- },
571
- get_name: () => prop('name'),
572
- get_nick: () => prop('nick'),
573
- get_blurb: () => prop('blurb'),
574
- get_default_value: () => wrapReturn(prop('defaultValue')),
575
- };
590
+ const pspec = Object.create(paramSpecProto);
591
+ pspec[HANDLE] = handle;
576
592
  return pspec;
577
593
  }
578
594
 
@@ -1063,7 +1079,27 @@ function assignTemplateChildren(instance, handle, reg) {
1063
1079
  // resolves the same three spellings at the point of use instead — same reachability,
1064
1080
  // no accessor synthesis. Learn6502's SourceView is exactly why this matters: its
1065
1081
  // property is `line-numbers` and its accessor is `lineNumbers`.
1082
+ // Resolved per prototype, INCLUDING the "this class declares none" answer (null).
1083
+ // The native store calls runJsPropertySetter on every write of a custom property, so
1084
+ // without this a class with no accessor at all — the common case — paid three
1085
+ // prototype-chain walks and two regex rewrites per assignment to learn nothing. GJS
1086
+ // settles the same question once, at registerClass time (`_checkAccessors` mirrors
1087
+ // the declared spelling onto the other two); a prototype gaining an accessor later is
1088
+ // outside what either runtime honours.
1089
+ const propertySettersByProto = new WeakMap();
1090
+
1066
1091
  function findPropertySetter(userProto, propertyName) {
1092
+ let perProto = propertySettersByProto.get(userProto);
1093
+ if (perProto === undefined) propertySettersByProto.set(userProto, (perProto = new Map()));
1094
+ let setter = perProto.get(propertyName);
1095
+ if (setter === undefined) {
1096
+ setter = resolvePropertySetter(userProto, propertyName) ?? null;
1097
+ perProto.set(propertyName, setter);
1098
+ }
1099
+ return setter ?? undefined;
1100
+ }
1101
+
1102
+ function resolvePropertySetter(userProto, propertyName) {
1067
1103
  const underscored = propertyName.replace(/-/g, '_');
1068
1104
  const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
1069
1105
  for (const spelling of [propertyName, underscored, camel]) {
@@ -1548,6 +1584,30 @@ function wrapInstance(handle, userProto) {
1548
1584
  materialised.add(name);
1549
1585
  return had;
1550
1586
  };
1587
+ // The `connect`/`connect_after`/`emit`/`disconnect` thunks of this wrapper, each
1588
+ // built on its first use (see the `get` trap). A null prototype so an unrelated
1589
+ // name — `constructor`, `toString` — cannot be mistaken for a built shortcut.
1590
+ const signalShortcuts = Object.create(null);
1591
+ // The GI-method thunks of this wrapper, built on first use, `null` for a name
1592
+ // that resolves to no introspected method. A thunk is a pure function of
1593
+ // (handle, name) and presence is already memoized per GType (instanceHasMethod),
1594
+ // so the only thing that changes by keeping it is IDENTITY — and a STABLE one is
1595
+ // the GJS shape: there the method lives on the prototype, so `obj.m === obj.m`
1596
+ // (the reason a user prototype's own function is handed back unbound, #1175).
1597
+ // Built fresh, every `obj.m(…)` in a loop allocated a closure and re-asked for
1598
+ // the type name across the N-API boundary. Null prototype: `constructor` and
1599
+ // friends must not read as a cached thunk.
1600
+ const giMethodThunks = Object.create(null);
1601
+ const giMethodThunk = (prop) => {
1602
+ let thunk = giMethodThunks[prop];
1603
+ if (thunk === undefined) {
1604
+ thunk = instanceHasMethod(handle, prop)
1605
+ ? (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)))
1606
+ : null;
1607
+ giMethodThunks[prop] = thunk;
1608
+ }
1609
+ return thunk ?? undefined;
1610
+ };
1551
1611
  const proxy = new Proxy(target, {
1552
1612
  get(t, prop) {
1553
1613
  if (prop === HANDLE) return handle;
@@ -1561,10 +1621,16 @@ function wrapInstance(handle, userProto) {
1561
1621
  // introspectable ancestor (GObject.Object), yet on gjs 1.88.1
1562
1622
  // `socks5.connect` IS g_proxy_connect (Gio.Proxy, 3 IN args) — measured.
1563
1623
  // The same native walk the GI-method fallback below uses sees it.
1564
- if (instanceHasMethod(handle, prop)) {
1565
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1566
- }
1624
+ const method = giMethodThunk(prop);
1625
+ if (method !== undefined) return method;
1567
1626
  }
1627
+ // The four signal shortcuts below close over nothing but `handle`, so they
1628
+ // are per-wrapper constants — yet the switch rebuilt one on EVERY access,
1629
+ // i.e. one throwaway closure per `obj.emit(…)` in a loop. Built once here
1630
+ // and reused; on gjs these live on GObject.Object.prototype and have stable
1631
+ // identity too. Lazy, because most wrappers never reach for one.
1632
+ const shortcut = signalShortcuts[prop];
1633
+ if (shortcut !== undefined) return shortcut;
1568
1634
  switch (prop) {
1569
1635
  case '$typeName':
1570
1636
  // The instance's concrete RUNTIME GType name —
@@ -1581,24 +1647,25 @@ function wrapInstance(handle, userProto) {
1581
1647
  case 'connect':
1582
1648
  // Record the (user fn → id) mapping so signal_handlers_*_by_func can
1583
1649
  // resolve the private-closure handler back to its ids (see the registry).
1584
- return (signal, cb) => {
1650
+ return (signalShortcuts.connect = (signal, cb) => {
1585
1651
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
1586
1652
  recordSignalHandler(handle, cb, id);
1587
1653
  return id;
1588
- };
1654
+ });
1589
1655
  case 'connect_after':
1590
- return (signal, cb) => {
1656
+ return (signalShortcuts.connect_after = (signal, cb) => {
1591
1657
  const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
1592
1658
  recordSignalHandler(handle, cb, id);
1593
1659
  return id;
1594
- };
1660
+ });
1595
1661
  case 'emit':
1596
- return (signal, ...args) => wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
1662
+ return (signalShortcuts.emit = (signal, ...args) =>
1663
+ wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args))));
1597
1664
  case 'disconnect':
1598
- return (id) => {
1665
+ return (signalShortcuts.disconnect = (id) => {
1599
1666
  forgetSignalHandlerId(handle, id);
1600
1667
  return native.disconnectSignal(handle, id);
1601
- };
1668
+ });
1602
1669
  default:
1603
1670
  break;
1604
1671
  }
@@ -1674,10 +1741,7 @@ function wrapInstance(handle, userProto) {
1674
1741
  // camelCase like Gwebgl's `getString`) and only falls back to the
1675
1742
  // snake_case alias. Converting here would destroy a literal camelCase
1676
1743
  // GI name.
1677
- if (instanceHasMethod(handle, prop)) {
1678
- return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
1679
- }
1680
- return undefined;
1744
+ return giMethodThunk(prop);
1681
1745
  },
1682
1746
  set(t, prop, value) {
1683
1747
  if (typeof prop === 'string') {
@@ -10,16 +10,8 @@
10
10
  // throws; this is the same wrapper, so `try { … } catch` means the same on either runtime
11
11
  // (ADR 0099 conformance row `errors`).
12
12
  //
13
- // Two deliberate differences from the GJS override:
14
- // • the constructor check is a Proxy `construct` trap — a node-gi class has no `_init` to wrap;
15
- // • `set_int` / `set_uint` range-check their value, which in GJS is the argument marshaller's
16
- // job. node-gi's marshaller wraps modulo 2^32 (`set_uint('k', -1)` stores 4294967295), a
17
- // general divergence recorded in status/open-todos/node-gi.md; this keeps the settings
18
- // surface honest without touching the addon, so it also holds in an already-built payload.
19
-
20
- const INT32_MIN = -(2 ** 31);
21
- const INT32_MAX = 2 ** 31 - 1;
22
- const UINT32_MAX = 2 ** 32 - 1;
13
+ // One deliberate difference from the GJS override: the constructor check is a Proxy `construct`
14
+ // trap — a node-gi class has no `_init` to wrap.
23
15
 
24
16
  const CHECKED = [
25
17
  'get_boolean',
@@ -53,12 +45,6 @@ const CHECKED = [
53
45
  'reset',
54
46
  ];
55
47
 
56
- /** The range a setter's JS number must be in; GJS's marshaller throws this exact message. */
57
- const RANGES = {
58
- set_int: [INT32_MIN, INT32_MAX, 'int32'],
59
- set_uint: [0, UINT32_MAX, 'uint32'],
60
- };
61
-
62
48
  /**
63
49
  * Patch `Gio.Settings` / `Gio.SettingsSchema` like GJS does, once, and return the class to hand
64
50
  * out (a Proxy whose `new` refuses a schema that is not installed instead of aborting).
@@ -87,16 +73,8 @@ export function createGioSettings(Gio) {
87
73
  for (const name of CHECKED) {
88
74
  const real = proto[name];
89
75
  if (typeof real !== 'function') continue;
90
- const range = RANGES[name];
91
76
  proto[name] = function (key, ...args) {
92
77
  checkKey(this, key);
93
- if (range !== undefined) {
94
- const [min, max, type] = range;
95
- const value = args[0];
96
- if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
97
- throw new Error(`Argument value: value is out of range for ${type}`);
98
- }
99
- }
100
78
  return real.call(this, key, ...args);
101
79
  };
102
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.64.0",
3
+ "version": "0.64.1",
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
@@ -17,7 +17,8 @@
17
17
  // WrapBoxed; cairo_path_t is NOT ref-counted, so a borrowed path is COPIED via
18
18
  // the gjs CairoPath::copy_ptr dummy-context trick instead of ref'd). Slice:
19
19
  // Context (drawing ops, path copy/append, dash, matrix point transforms,
20
- // in-fill/in-stroke, getSource), Surface + ImageSurface (headless pixels),
20
+ // in-fill/in-stroke, getSource, the toy-text API: selectFontFace/setFontSize/
21
+ // showText/textPath/textExtents/fontExtents), Surface + ImageSurface (headless pixels),
21
22
  // SolidPattern, LinearGradient/RadialGradient (color stops), SurfacePattern
22
23
  // (extend/filter), Path handles. Deferred: region, PDF/SVG/PS surfaces,
23
24
  // text/font ops (showText/selectFontFace — canvas2d text rides PangoCairo).
@@ -733,6 +734,70 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
733
734
  CTX_EXTENTS("fillExtents", cairo_fill_extents)
734
735
  CTX_EXTENTS("strokeExtents", cairo_stroke_extents)
735
736
  CTX_EXTENTS("clipExtents", cairo_clip_extents)
737
+ // Toy text API (cairo_select_font_face & co), the GJS-compatible Context text ops.
738
+ // textExtents/fontExtents return the object shape gjs's cairo-context.cpp builds.
739
+ c.Set("selectFontFace", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
740
+ Napi::Env env = info.Env();
741
+ cairo_t* cr = CtxArg(env, info);
742
+ if (cr == nullptr) return env.Undefined();
743
+ std::string family = info[1].ToString().Utf8Value();
744
+ cairo_select_font_face(cr, family.c_str(),
745
+ static_cast<cairo_font_slant_t>(info[2].ToNumber().Int32Value()),
746
+ static_cast<cairo_font_weight_t>(info[3].ToNumber().Int32Value()));
747
+ CheckStatus(env, cairo_status(cr), "selectFontFace");
748
+ return env.Undefined();
749
+ }));
750
+ c.Set("setFontSize", Napi::Function::New(env, CTX_1D(cairo_set_font_size)));
751
+ c.Set("showText", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
752
+ Napi::Env env = info.Env();
753
+ cairo_t* cr = CtxArg(env, info);
754
+ if (cr == nullptr) return env.Undefined();
755
+ std::string text = info[1].ToString().Utf8Value();
756
+ cairo_show_text(cr, text.c_str());
757
+ CheckStatus(env, cairo_status(cr), "showText");
758
+ return env.Undefined();
759
+ }));
760
+ c.Set("textPath", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
761
+ Napi::Env env = info.Env();
762
+ cairo_t* cr = CtxArg(env, info);
763
+ if (cr == nullptr) return env.Undefined();
764
+ std::string text = info[1].ToString().Utf8Value();
765
+ cairo_text_path(cr, text.c_str());
766
+ CheckStatus(env, cairo_status(cr), "textPath");
767
+ return env.Undefined();
768
+ }));
769
+ c.Set("textExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
770
+ Napi::Env env = info.Env();
771
+ cairo_t* cr = CtxArg(env, info);
772
+ if (cr == nullptr) return env.Undefined();
773
+ std::string text = info[1].ToString().Utf8Value();
774
+ cairo_text_extents_t e;
775
+ cairo_text_extents(cr, text.c_str(), &e);
776
+ if (!CheckStatus(env, cairo_status(cr), "textExtents")) return env.Undefined();
777
+ Napi::Object o = Napi::Object::New(env);
778
+ o.Set("xBearing", Napi::Number::New(env, e.x_bearing));
779
+ o.Set("yBearing", Napi::Number::New(env, e.y_bearing));
780
+ o.Set("width", Napi::Number::New(env, e.width));
781
+ o.Set("height", Napi::Number::New(env, e.height));
782
+ o.Set("xAdvance", Napi::Number::New(env, e.x_advance));
783
+ o.Set("yAdvance", Napi::Number::New(env, e.y_advance));
784
+ return o;
785
+ }));
786
+ c.Set("fontExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
787
+ Napi::Env env = info.Env();
788
+ cairo_t* cr = CtxArg(env, info);
789
+ if (cr == nullptr) return env.Undefined();
790
+ cairo_font_extents_t e;
791
+ cairo_font_extents(cr, &e);
792
+ if (!CheckStatus(env, cairo_status(cr), "fontExtents")) return env.Undefined();
793
+ Napi::Object o = Napi::Object::New(env);
794
+ o.Set("ascent", Napi::Number::New(env, e.ascent));
795
+ o.Set("descent", Napi::Number::New(env, e.descent));
796
+ o.Set("height", Napi::Number::New(env, e.height));
797
+ o.Set("maxXAdvance", Napi::Number::New(env, e.max_x_advance));
798
+ o.Set("maxYAdvance", Napi::Number::New(env, e.max_y_advance));
799
+ return o;
800
+ }));
736
801
  c.Set("status", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
737
802
  Napi::Env env = info.Env();
738
803
  cairo_t* cr = CtxArg(env, info);
package/src/calls.cc CHANGED
@@ -1242,6 +1242,57 @@ static GIFunctionInfo* ResolveInstanceMethod(GIRepository* repo, GType gtype, GI
1242
1242
  return func;
1243
1243
  }
1244
1244
 
1245
+ // Resolved instance methods, keyed by the instance's concrete GType and the name
1246
+ // as JS spelled it. The resolution above is the expensive half of a GI call —
1247
+ // dup the default repository, walk the GType parents for the nearest
1248
+ // GIObjectInfo, then search own + interface + inherited method tables twice (the
1249
+ // literal name, then the snake_case alias) — and it repeats IDENTICALLY on every
1250
+ // call, because a typelib's method tables do not change at runtime. MEASURED on
1251
+ // this machine (gjs 1.88.1 / GLib 2.88.3): `set_property` on a registerClass
1252
+ // subclass cost 8224 ns through callMethod against 2815 ns for the same
1253
+ // g_object_set_property through setProperty — 5.4 µs of pure re-resolution per
1254
+ // call, which is most of what a property write on the real-GObject engine spends.
1255
+ //
1256
+ // Only a HIT is cached. A miss must stay uncached: namespaces load lazily, so a
1257
+ // name that resolves to nothing now can resolve after the typelib that declares
1258
+ // it is required — caching the miss would make load order change behaviour.
1259
+ //
1260
+ // The cache OWNS the ref it holds (GIFunctionInfos live as long as the process
1261
+ // anyway, and the entry count is bounded by the methods a program actually
1262
+ // calls); callers get a borrowed pointer and must not unref it. Written only
1263
+ // from the JS thread, like every other entry point here.
1264
+ static std::map<std::pair<GType, std::string>, GIFunctionInfo*> g_instanceMethodCache;
1265
+
1266
+ // The resolution of ResolveInstanceMethod, memoized. Returns a BORROWED pointer
1267
+ // (never unref) or nullptr.
1268
+ static GIFunctionInfo* ResolveInstanceMethodCached(GType gtype, const std::string& method) {
1269
+ const std::pair<GType, std::string> key(gtype, method);
1270
+ auto it = g_instanceMethodCache.find(key);
1271
+ if (it != g_instanceMethodCache.end()) return it->second;
1272
+
1273
+ GIRepository* repo = DupDefaultRepository();
1274
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1275
+ GIFunctionInfo* func = nullptr;
1276
+ if (objInfo != nullptr) {
1277
+ func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1278
+ gi_base_info_unref(objInfo);
1279
+ }
1280
+ g_object_unref(repo);
1281
+ if (func != nullptr) g_instanceMethodCache.emplace(key, func);
1282
+ return func;
1283
+ }
1284
+
1285
+ // Whether `gtype` has introspection info at all — the distinction callMethod
1286
+ // reports as "no introspection info for X" before it even looks for a method.
1287
+ static bool HasObjectInfo(GType gtype) {
1288
+ GIRepository* repo = DupDefaultRepository();
1289
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1290
+ const bool found = objInfo != nullptr;
1291
+ if (objInfo != nullptr) gi_base_info_unref(objInfo);
1292
+ g_object_unref(repo);
1293
+ return found;
1294
+ }
1295
+
1245
1296
  // callMethod(handle, methodName, args?: unknown[]) -> unknown
1246
1297
  // Resolve an instance method by walking the instance's GIObjectInfo (own +
1247
1298
  // implemented-interface methods at each level, then up the parent chain), then
@@ -1262,20 +1313,14 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1262
1313
  : Napi::Array::New(env, 0);
1263
1314
 
1264
1315
  GType gtype = G_OBJECT_TYPE(obj);
1265
- GIRepository* repo = DupDefaultRepository();
1316
+ GIFunctionInfo* func = ResolveInstanceMethodCached(gtype, method);
1266
1317
 
1267
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1268
- if (objInfo == nullptr) {
1269
- g_object_unref(repo);
1318
+ if (func == nullptr && !HasObjectInfo(gtype)) {
1270
1319
  Napi::Error::New(env, std::string("no introspection info for ") + G_OBJECT_TYPE_NAME(obj))
1271
1320
  .ThrowAsJavaScriptException();
1272
1321
  return env.Null();
1273
1322
  }
1274
1323
 
1275
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1276
- gi_base_info_unref(objInfo);
1277
- g_object_unref(repo);
1278
-
1279
1324
  if (func == nullptr) {
1280
1325
  // On a terminating env the method-name read (info[1].Utf8Value) degrades to
1281
1326
  // "" (a swallowed napi failure), so resolution misses and we land here — but
@@ -1290,15 +1335,13 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1290
1335
  return env.Null();
1291
1336
  }
1292
1337
  if (!gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1293
- gi_base_info_unref(func);
1294
1338
  Napi::TypeError::New(env, method + " is not an instance method").ThrowAsJavaScriptException();
1295
1339
  return env.Null();
1296
1340
  }
1297
1341
 
1298
- Napi::Value result =
1299
- InvokeFunctionInfo(env, func, obj, args, std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1300
- gi_base_info_unref(func);
1301
- return result;
1342
+ // The cache owns `func`; this is a borrowed pointer.
1343
+ return InvokeFunctionInfo(env, func, obj, args,
1344
+ std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1302
1345
  }
1303
1346
 
1304
1347
  // hasMethod(handle, methodName) -> boolean
@@ -1321,20 +1364,10 @@ Napi::Value HasMethod(const Napi::CallbackInfo& info) {
1321
1364
  if (obj == nullptr) return env.Null();
1322
1365
  std::string method = info[1].As<Napi::String>().Utf8Value();
1323
1366
 
1324
- GType gtype = G_OBJECT_TYPE(obj);
1325
- GIRepository* repo = DupDefaultRepository();
1326
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1327
- if (objInfo == nullptr) {
1328
- g_object_unref(repo);
1329
- return Napi::Boolean::New(env, false);
1330
- }
1331
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1332
- gi_base_info_unref(objInfo);
1333
- g_object_unref(repo);
1367
+ GIFunctionInfo* func = ResolveInstanceMethodCached(G_OBJECT_TYPE(obj), method);
1334
1368
  if (func == nullptr) return Napi::Boolean::New(env, false);
1335
- const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1336
- gi_base_info_unref(func);
1337
- return Napi::Boolean::New(env, invocable);
1369
+ return Napi::Boolean::New(env,
1370
+ gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func)));
1338
1371
  }
1339
1372
 
1340
1373
  // hasClassMethod(namespace, typeName, methodName) -> boolean
package/src/common.h CHANGED
@@ -35,6 +35,7 @@
35
35
  #endif
36
36
 
37
37
  #ifndef _WIN32
38
+ #include <cmath>
38
39
  #include <dlfcn.h> // dlopen/dlsym the GtkWidgetClass template API (no GTK link)
39
40
  #endif
40
41
  #include <napi.h>
@@ -280,6 +281,162 @@ inline uint64_t JsValueToUint64(Napi::Value v) {
280
281
  return n.IsEmpty() ? 0 : static_cast<uint64_t>(n.Int64Value());
281
282
  }
282
283
 
284
+ // IN integers, GJS-exact (refs/gjs/gi/js-value-inl.h, arg-inl.h): an integer tag
285
+ // REFUSES an out-of-range value instead of wrapping it. The rule is not uniform and is
286
+ // copied as is, never made stricter: 8/16-bit run ToInt32 first (so it wraps mod 2^32)
287
+ // and then compare against the tag range; int32 is not checked at all; uint32 and the
288
+ // 64-bit tags refuse what falls outside their range. undefined and a non-finite Number
289
+ // become 0. `shown` receives the text GJS prints for the offending value.
290
+ // kPending: a BigInt/Symbol hit ToNumber, the TypeError is already pending — the caller
291
+ // must stop without throwing a second error.
292
+ enum class NodeGiIntResult { kOk, kOutOfRange, kPending };
293
+ inline NodeGiIntResult NodeGiJsToInteger(Napi::Value v, GITypeTag tag, GIArgument* out,
294
+ std::string* shown) {
295
+ const bool is64 = tag == GI_TYPE_TAG_INT64 || tag == GI_TYPE_TAG_UINT64;
296
+ if (is64 && v.IsBigInt()) {
297
+ bool lossless = false;
298
+ if (tag == GI_TYPE_TAG_INT64) {
299
+ out->v_int64 = v.As<Napi::BigInt>().Int64Value(&lossless);
300
+ } else {
301
+ out->v_uint64 = v.As<Napi::BigInt>().Uint64Value(&lossless);
302
+ }
303
+ if (!lossless && shown != nullptr) *shown = "0";
304
+ return lossless ? NodeGiIntResult::kOk : NodeGiIntResult::kOutOfRange;
305
+ }
306
+ Napi::Number n = v.ToNumber();
307
+ if (n.IsEmpty()) return NodeGiIntResult::kPending;
308
+ double d = n.DoubleValue();
309
+ switch (tag) {
310
+ case GI_TYPE_TAG_INT8:
311
+ case GI_TYPE_TAG_UINT8:
312
+ case GI_TYPE_TAG_INT16:
313
+ case GI_TYPE_TAG_UINT16: {
314
+ int32_t i = n.Int32Value();
315
+ int32_t lo = tag == GI_TYPE_TAG_INT8 ? INT8_MIN : tag == GI_TYPE_TAG_INT16 ? INT16_MIN : 0;
316
+ int32_t hi = tag == GI_TYPE_TAG_INT8 ? INT8_MAX : tag == GI_TYPE_TAG_UINT8 ? UINT8_MAX
317
+ : tag == GI_TYPE_TAG_INT16 ? INT16_MAX : UINT16_MAX;
318
+ if (shown != nullptr) *shown = std::to_string(i);
319
+ if (i < lo || i > hi) return NodeGiIntResult::kOutOfRange;
320
+ if (tag == GI_TYPE_TAG_INT8) out->v_int8 = static_cast<int8_t>(i);
321
+ else if (tag == GI_TYPE_TAG_UINT8) out->v_uint8 = static_cast<uint8_t>(i);
322
+ else if (tag == GI_TYPE_TAG_INT16) out->v_int16 = static_cast<int16_t>(i);
323
+ else out->v_uint16 = static_cast<uint16_t>(i);
324
+ return NodeGiIntResult::kOk;
325
+ }
326
+ case GI_TYPE_TAG_INT32:
327
+ out->v_int32 = n.Int32Value();
328
+ return NodeGiIntResult::kOk;
329
+ case GI_TYPE_TAG_UINT32: {
330
+ bool zero = v.IsUndefined() || (v.IsNumber() && !std::isfinite(d));
331
+ if (zero) {
332
+ out->v_uint32 = 0;
333
+ return NodeGiIntResult::kOk;
334
+ }
335
+ if (shown != nullptr) *shown = std::to_string(d);
336
+ if (std::isnan(d) || d < 0 || d > static_cast<double>(UINT32_MAX))
337
+ return NodeGiIntResult::kOutOfRange;
338
+ out->v_uint32 = static_cast<uint32_t>(d);
339
+ return NodeGiIntResult::kOk;
340
+ }
341
+ case GI_TYPE_TAG_INT64: {
342
+ double t = std::isfinite(d) ? std::trunc(d) : 0;
343
+ if (t < -9223372036854775808.0 || t >= 9223372036854775808.0) {
344
+ if (shown != nullptr)
345
+ *shown = std::to_string(t < 0 ? INT64_MIN : INT64_MAX);
346
+ return NodeGiIntResult::kOutOfRange;
347
+ }
348
+ out->v_int64 = static_cast<int64_t>(t);
349
+ return NodeGiIntResult::kOk;
350
+ }
351
+ case GI_TYPE_TAG_UINT64: {
352
+ double t = std::isfinite(d) ? std::trunc(d) : 0;
353
+ if (t < 0 || t >= 18446744073709551616.0) {
354
+ if (shown != nullptr)
355
+ *shown = std::to_string(t < 0 ? uint64_t{0} : UINT64_MAX);
356
+ return NodeGiIntResult::kOutOfRange;
357
+ }
358
+ out->v_uint64 = static_cast<uint64_t>(t);
359
+ return NodeGiIntResult::kOk;
360
+ }
361
+ default:
362
+ return NodeGiIntResult::kOk;
363
+ }
364
+ }
365
+
366
+ enum class NodeGiIntContext { kArgument, kField, kReturnValue };
367
+ inline const char* NodeGiIntTagName(GITypeTag tag) {
368
+ switch (tag) {
369
+ case GI_TYPE_TAG_INT8: return "int8";
370
+ case GI_TYPE_TAG_UINT8: return "uint8";
371
+ case GI_TYPE_TAG_INT16: return "int16";
372
+ case GI_TYPE_TAG_UINT16: return "uint16";
373
+ case GI_TYPE_TAG_INT32: return "int32";
374
+ case GI_TYPE_TAG_UINT32: return "uint32";
375
+ case GI_TYPE_TAG_INT64: return "int64";
376
+ default: return "uint64";
377
+ }
378
+ }
379
+ // GJS's three texts (arg-cache.cpp / arg.cpp), plain Error.
380
+ inline void NodeGiThrowIntOutOfRange(Napi::Env env, GITypeTag tag, NodeGiIntContext ctx,
381
+ const char* name, const std::string& shown) {
382
+ std::string t = NodeGiIntTagName(tag);
383
+ std::string msg;
384
+ if (ctx == NodeGiIntContext::kArgument) {
385
+ msg = std::string("Argument ") + (name != nullptr ? name : "") + ": value is out of range for " + t;
386
+ } else {
387
+ msg = "value " + shown + " is out of range for " +
388
+ (ctx == NodeGiIntContext::kField ? std::string("Field '") + (name ? name : "") + "'"
389
+ : std::string("Return value")) +
390
+ " (type " + t + ")";
391
+ }
392
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
393
+ }
394
+
395
+ // IN array elements and GValue int/uint, GJS-exact: these WRAP instead of refusing.
396
+ // An array element goes through js_value_to_c_strict (refs/gjs/gi/arg.cpp:543), which
397
+ // is ToInt32/ToUint32 truncated to the tag for 8/16/32 bits and ToInt64/ToUint64 —
398
+ // modulo 2^64 — for 64 bits; a BigInt takes ToBigInt64/ToBigUint64 (also modulo 2^64)
399
+ // on a 64-bit tag and is a TypeError on a narrower one. kPending: that TypeError is
400
+ // already pending, the caller must stop without throwing a second error.
401
+ inline uint64_t NodeGiWrapToUint64(double d) {
402
+ if (!std::isfinite(d)) return 0;
403
+ double m = std::fmod(std::trunc(d), 18446744073709551616.0);
404
+ return m < 0 ? uint64_t{0} - static_cast<uint64_t>(-m) : static_cast<uint64_t>(m);
405
+ }
406
+ inline NodeGiIntResult NodeGiJsToIntegerWrap(Napi::Value v, GITypeTag tag, GIArgument* out) {
407
+ const bool is64 = tag == GI_TYPE_TAG_INT64 || tag == GI_TYPE_TAG_UINT64;
408
+ if (is64 && v.IsBigInt()) {
409
+ bool lossless = false;
410
+ out->v_uint64 = v.As<Napi::BigInt>().Uint64Value(&lossless);
411
+ return NodeGiIntResult::kOk;
412
+ }
413
+ Napi::Number n = v.ToNumber();
414
+ if (n.IsEmpty()) return NodeGiIntResult::kPending;
415
+ switch (tag) {
416
+ case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(n.Int32Value()); break;
417
+ case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(n.Uint32Value()); break;
418
+ case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(n.Int32Value()); break;
419
+ case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(n.Uint32Value()); break;
420
+ case GI_TYPE_TAG_INT32: out->v_int32 = n.Int32Value(); break;
421
+ case GI_TYPE_TAG_UINT32: out->v_uint32 = n.Uint32Value(); break;
422
+ case GI_TYPE_TAG_INT64:
423
+ case GI_TYPE_TAG_UINT64: out->v_uint64 = NodeGiWrapToUint64(n.DoubleValue()); break;
424
+ default: break;
425
+ }
426
+ return NodeGiIntResult::kOk;
427
+ }
428
+
429
+ // GValue char/uchar/int64/uint64: refused out of range with gjs_value_to_g_value's own
430
+ // text (refs/gjs/gi/value.cpp:602-690, throw_expect_type), a plain Error. `what` is the
431
+ // "expected" noun gjs prints; the offending value is its JS ToString.
432
+ inline void NodeGiThrowGValueOutOfRange(Napi::Env env, Napi::Value v, const char* what) {
433
+ Napi::String s = v.ToString();
434
+ std::string shown = s.IsEmpty() ? std::string() : s.Utf8Value();
435
+ Napi::Error::New(env, std::string("Wrong type ") + (v.IsBigInt() ? "bigint" : "number") + "; " +
436
+ what + " expected. But it's out of range: " + shown)
437
+ .ThrowAsJavaScriptException();
438
+ }
439
+
283
440
  // OUT (WarnIfUnsafe{Int,Uint}64): GJS ALWAYS returns a JS Number for a 64-bit int
284
441
  // (never a BigInt) and emits this g_warning when the value falls outside the range
285
442
  // a double represents exactly (|v| > 2^53 - 1 == Number.MAX_SAFE_INTEGER), because
@@ -391,7 +548,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
391
548
  CreatedClosures* closures = nullptr,
392
549
  CreatedBytes* bytes = nullptr,
393
550
  CreatedValues* values = nullptr,
394
- const char* argName = nullptr);
551
+ const char* argName = nullptr,
552
+ NodeGiIntContext intContext = NodeGiIntContext::kArgument);
395
553
 
396
554
  // ---- IN container building -----------------------------------------
397
555
  //
package/src/marshal.cc CHANGED
@@ -516,7 +516,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
516
516
  CreatedClosures* closures,
517
517
  CreatedBytes* bytes,
518
518
  CreatedValues* values,
519
- const char* argName) {
519
+ const char* argName,
520
+ NodeGiIntContext intContext) {
520
521
  if (v.IsEmpty()) {
521
522
  // Residue of a swallowed napi failure (a fallible Get()/coercion upstream
522
523
  // failed on a terminating env, or a throwing getter left the exception
@@ -536,16 +537,24 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
536
537
  GITypeTag tag = gi_type_info_get_tag(type);
537
538
  switch (tag) {
538
539
  case GI_TYPE_TAG_BOOLEAN: out->v_boolean = NodeGiToBool(v); return true;
539
- case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(NodeGiToInt32(v)); return true;
540
- case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(NodeGiToUint32(v)); return true;
541
- case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(NodeGiToInt32(v)); return true;
542
- case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(NodeGiToUint32(v)); return true;
543
- case GI_TYPE_TAG_INT32: out->v_int32 = NodeGiToInt32(v); return true;
544
- case GI_TYPE_TAG_UINT32: out->v_uint32 = NodeGiToUint32(v); return true;
545
- // 64-bit: accept a BigInt losslessly (else a Number, truncated) — never let a
546
- // BigInt reach ToNumber() (fatal abort). See JsValueTo{Int,Uint}64 in common.h.
547
- case GI_TYPE_TAG_INT64: out->v_int64 = JsValueToInt64(v); return true;
548
- case GI_TYPE_TAG_UINT64: out->v_uint64 = JsValueToUint64(v); return true;
540
+ case GI_TYPE_TAG_INT8:
541
+ case GI_TYPE_TAG_UINT8:
542
+ case GI_TYPE_TAG_INT16:
543
+ case GI_TYPE_TAG_UINT16:
544
+ case GI_TYPE_TAG_INT32:
545
+ case GI_TYPE_TAG_UINT32:
546
+ case GI_TYPE_TAG_INT64:
547
+ case GI_TYPE_TAG_UINT64: {
548
+ std::string shown;
549
+ switch (NodeGiJsToInteger(v, tag, out, &shown)) {
550
+ case NodeGiIntResult::kOk: return true;
551
+ case NodeGiIntResult::kPending: return false;
552
+ case NodeGiIntResult::kOutOfRange:
553
+ NodeGiThrowIntOutOfRange(env, tag, intContext, argName, shown);
554
+ return false;
555
+ }
556
+ return false;
557
+ }
549
558
  case GI_TYPE_TAG_FLOAT: out->v_float = static_cast<float>(NodeGiToDouble(v)); return true;
550
559
  case GI_TYPE_TAG_DOUBLE: out->v_double = NodeGiToDouble(v); return true;
551
560
  case GI_TYPE_TAG_UTF8:
@@ -1592,21 +1601,36 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
1592
1601
  // the element, so it gets a ref (objects) or an independent copy (boxed) instead.
1593
1602
  // Throws + returns false on an unsupported element.
1594
1603
  static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
1595
- GITransfer elemTransfer) {
1604
+ GITransfer elemTransfer, bool wrapIntegers = false) {
1596
1605
  memset(a, 0, sizeof(*a));
1597
1606
  switch (gi_type_info_get_tag(elem)) {
1598
1607
  // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
1599
1608
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
1600
1609
  // into Error::New(nullptr)'s fatal sites.
1601
1610
  case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
1602
- case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
1603
- case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
1604
- case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
1605
- case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
1606
- case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
1607
- case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
1608
- case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
1609
- case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(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.
1614
+ case GI_TYPE_TAG_INT8:
1615
+ case GI_TYPE_TAG_UINT8:
1616
+ case GI_TYPE_TAG_INT16:
1617
+ case GI_TYPE_TAG_UINT16:
1618
+ case GI_TYPE_TAG_INT32:
1619
+ case GI_TYPE_TAG_UINT32:
1620
+ case GI_TYPE_TAG_INT64:
1621
+ case GI_TYPE_TAG_UINT64:
1622
+ if (wrapIntegers)
1623
+ 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;
1633
+ }
1610
1634
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1611
1635
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1612
1636
  case GI_TYPE_TAG_GTYPE: {
@@ -1814,7 +1838,10 @@ bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer
1814
1838
  }
1815
1839
  std::string held;
1816
1840
  return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
1817
- argName);
1841
+ argName,
1842
+ argName != nullptr && strcmp(argName, "return value") == 0
1843
+ ? NodeGiIntContext::kReturnValue
1844
+ : NodeGiIntContext::kArgument);
1818
1845
  }
1819
1846
 
1820
1847
  } // namespace
@@ -2273,7 +2300,8 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2273
2300
  continue;
2274
2301
  }
2275
2302
  GIArgument a;
2276
- ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer);
2303
+ ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer,
2304
+ /*wrapIntegers=*/true);
2277
2305
  if (!ok) break;
2278
2306
  // Copy the element's storage bytes (LE: the low elemSize bytes of the union
2279
2307
  // alias the active field — v_string/v_pointer for pointers, the scalar
@@ -2973,7 +3001,8 @@ Napi::Value SetBoxedField(const Napi::CallbackInfo& info) {
2973
3001
  GIArgument arg;
2974
3002
  memset(&arg, 0, sizeof(arg));
2975
3003
  std::string heldString;
2976
- if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr)) {
3004
+ if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr,
3005
+ nullptr, nullptr, nullptr, name.c_str(), NodeGiIntContext::kField)) {
2977
3006
  if (!gi_field_info_set_field(field, bh->ptr, &arg)) {
2978
3007
  Napi::Error::New(env, std::string("Writing field ") + typeName + "." + name + " is not supported")
2979
3008
  .ThrowAsJavaScriptException();
package/src/object.cc CHANGED
@@ -444,17 +444,53 @@ bool JsToGValue(Napi::Env env, Napi::Value js, GValue* v) {
444
444
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
445
445
  // into Error::New(nullptr)'s fatal sites (the terminate-mid-`newObject` path).
446
446
  case G_TYPE_BOOLEAN: g_value_set_boolean(v, NodeGiToBool(js)); return true;
447
- case G_TYPE_CHAR: g_value_set_schar(v, static_cast<gint8>(NodeGiToInt32(js))); return true;
448
- case G_TYPE_UCHAR: g_value_set_uchar(v, static_cast<guchar>(NodeGiToUint32(js))); return true;
449
- case G_TYPE_INT: g_value_set_int(v, NodeGiToInt32(js)); return true;
450
- case G_TYPE_UINT: g_value_set_uint(v, NodeGiToUint32(js)); return true;
451
- // 64-bit (glong/gulong are 64-bit on LP64): accept a BigInt losslessly, else a
452
- // truncated Number — never let a BigInt reach ToNumber(). Shared with the GI
453
- // scalar marshaller via JsValueTo{Int,Uint}64 (common.h).
447
+ // char/uchar/int64/uint64 refuse an out-of-range value and int/uint wrap, as gjs's
448
+ // gjs_value_to_g_value does (value.cpp:602-690) — the same rule the constructor
449
+ // properties and signal arguments reach. A BigInt on a 32-bit tag is ToNumber's
450
+ // TypeError, left pending.
451
+ case G_TYPE_CHAR:
452
+ case G_TYPE_UCHAR:
453
+ case G_TYPE_INT64:
454
+ case G_TYPE_UINT64: {
455
+ GITypeTag tag = ft == G_TYPE_CHAR ? GI_TYPE_TAG_INT8
456
+ : ft == G_TYPE_UCHAR ? GI_TYPE_TAG_UINT8
457
+ : ft == G_TYPE_INT64 ? GI_TYPE_TAG_INT64
458
+ : GI_TYPE_TAG_UINT64;
459
+ GIArgument a;
460
+ memset(&a, 0, sizeof(a));
461
+ switch (NodeGiJsToInteger(js, tag, &a, nullptr)) {
462
+ case NodeGiIntResult::kPending: return false;
463
+ case NodeGiIntResult::kOutOfRange:
464
+ NodeGiThrowGValueOutOfRange(env, js,
465
+ ft == G_TYPE_CHAR ? "char"
466
+ : ft == G_TYPE_UCHAR ? "unsigned char"
467
+ : ft == G_TYPE_INT64 ? "64-bit integer"
468
+ : "unsigned 64-bit integer");
469
+ return false;
470
+ case NodeGiIntResult::kOk: break;
471
+ }
472
+ if (ft == G_TYPE_CHAR) g_value_set_schar(v, a.v_int8);
473
+ else if (ft == G_TYPE_UCHAR) g_value_set_uchar(v, a.v_uint8);
474
+ else if (ft == G_TYPE_INT64) g_value_set_int64(v, a.v_int64);
475
+ else g_value_set_uint64(v, a.v_uint64);
476
+ return true;
477
+ }
478
+ case G_TYPE_INT:
479
+ case G_TYPE_UINT: {
480
+ GIArgument a;
481
+ memset(&a, 0, sizeof(a));
482
+ if (NodeGiJsToIntegerWrap(js, ft == G_TYPE_INT ? GI_TYPE_TAG_INT32 : GI_TYPE_TAG_UINT32,
483
+ &a) == NodeGiIntResult::kPending)
484
+ return false;
485
+ if (ft == G_TYPE_INT) g_value_set_int(v, a.v_int32);
486
+ else g_value_set_uint(v, a.v_uint32);
487
+ return true;
488
+ }
489
+ // glong/gulong are 64-bit on LP64: accept a BigInt losslessly, else a truncated
490
+ // Number — never let a BigInt reach ToNumber(). Shared with the GI scalar marshaller
491
+ // via JsValueTo{Int,Uint}64 (common.h). gjs has no G_TYPE_LONG case here at all.
454
492
  case G_TYPE_LONG: g_value_set_long(v, static_cast<glong>(JsValueToInt64(js))); return true;
455
493
  case G_TYPE_ULONG: g_value_set_ulong(v, static_cast<gulong>(JsValueToUint64(js))); return true;
456
- case G_TYPE_INT64: g_value_set_int64(v, JsValueToInt64(js)); return true;
457
- case G_TYPE_UINT64: g_value_set_uint64(v, JsValueToUint64(js)); return true;
458
494
  case G_TYPE_FLOAT: g_value_set_float(v, static_cast<float>(NodeGiToDouble(js))); return true;
459
495
  case G_TYPE_DOUBLE: g_value_set_double(v, NodeGiToDouble(js)); return true;
460
496
  case G_TYPE_ENUM: g_value_set_enum(v, NodeGiToInt32(js)); return true;
@@ -611,7 +647,16 @@ Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
611
647
  }
612
648
  g_value_init(&values[i], pspec->value_type);
613
649
  initialised = i + 1;
614
- if (!JsToGValue(env, props.Get(nameStorage[i]), &values[i])) {
650
+ Napi::Value propValue = props.Get(nameStorage[i]);
651
+ if (!propValue.IsEmpty() && propValue.IsUndefined()) {
652
+ // gjs refuses `undefined` for every property type (object.cpp:2090).
653
+ Napi::Error::New(env, "Invalid value 'undefined' for property " + std::string(pspec->name) +
654
+ " in object initializer.")
655
+ .ThrowAsJavaScriptException();
656
+ ok = false;
657
+ break;
658
+ }
659
+ if (!JsToGValue(env, propValue, &values[i])) {
615
660
  ok = false;
616
661
  break;
617
662
  }
@@ -734,6 +779,30 @@ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
734
779
  Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
735
780
  return env.Undefined();
736
781
  }
782
+ // gjs refuses an out-of-range value for these property types on assignment
783
+ // (object.cpp, with its own text); JsToGValue below now refuses them too, with the
784
+ // gjs_value_to_g_value text.
785
+ GITypeTag intTag = GI_TYPE_TAG_VOID;
786
+ switch (G_TYPE_FUNDAMENTAL(pspec->value_type)) {
787
+ case G_TYPE_CHAR: intTag = GI_TYPE_TAG_INT8; break;
788
+ case G_TYPE_UCHAR: intTag = GI_TYPE_TAG_UINT8; break;
789
+ case G_TYPE_UINT: intTag = GI_TYPE_TAG_UINT32; break;
790
+ case G_TYPE_INT64: intTag = GI_TYPE_TAG_INT64; break;
791
+ case G_TYPE_UINT64: intTag = GI_TYPE_TAG_UINT64; break;
792
+ default: break;
793
+ }
794
+ if (intTag != GI_TYPE_TAG_VOID) {
795
+ GIArgument probe;
796
+ std::string shown;
797
+ NodeGiIntResult r = NodeGiJsToInteger(info[2], intTag, &probe, &shown);
798
+ if (r == NodeGiIntResult::kPending) return env.Undefined();
799
+ if (r == NodeGiIntResult::kOutOfRange) {
800
+ Napi::Error::New(env, "value " + shown + " is out of range for " + pspec->name +
801
+ " (type " + NodeGiIntTagName(intTag) + ")")
802
+ .ThrowAsJavaScriptException();
803
+ return env.Undefined();
804
+ }
805
+ }
737
806
  GValue v = G_VALUE_INIT;
738
807
  g_value_init(&v, pspec->value_type);
739
808
  if (JsToGValue(env, info[2], &v)) {
package/src/private.cc CHANGED
@@ -25,6 +25,10 @@
25
25
 
26
26
  #include "common.h"
27
27
 
28
+ #ifdef _WIN32
29
+ #include <windows.h>
30
+ #endif
31
+
28
32
  namespace nodegi {
29
33
 
30
34
  // ---- GLib.log_set_writer_func ----------------------------------------------
@@ -557,10 +561,39 @@ Napi::Value Bindtextdomain(const Napi::CallbackInfo& info) {
557
561
  }
558
562
  location = info[1].As<Napi::String>().Utf8Value();
559
563
  }
564
+ #ifdef _WIN32
565
+ // bindtextdomain() reads its path in the ANSI code page, so a UTF-8 JS string with
566
+ // a non-ASCII directory (".../Steuererklärung/share/locale") binds a path that
567
+ // does not exist and every lookup silently answers the msgid (measured in a Windows
568
+ // 11 VM: the same catalog under an ASCII path translated). GNU gettext >= 0.21
569
+ // ships wbindtextdomain() for exactly this; both directions go through UTF-16.
570
+ std::wstring wlocation;
571
+ if (!query) {
572
+ int n = MultiByteToWideChar(CP_UTF8, 0, location.c_str(), -1, nullptr, 0);
573
+ if (n <= 0) {
574
+ Napi::TypeError::New(env, "bindtextdomain: location is not valid UTF-8")
575
+ .ThrowAsJavaScriptException();
576
+ return env.Null();
577
+ }
578
+ wlocation.resize(n);
579
+ MultiByteToWideChar(CP_UTF8, 0, location.c_str(), -1, wlocation.data(), n);
580
+ wlocation.resize(n - 1);
581
+ }
582
+ const wchar_t* wresult = wbindtextdomain(domain.c_str(), query ? nullptr : wlocation.c_str());
583
+ if (!query) bind_textdomain_codeset(domain.c_str(), "UTF-8");
584
+ if (wresult == nullptr) return env.Null();
585
+ int len = WideCharToMultiByte(CP_UTF8, 0, wresult, -1, nullptr, 0, nullptr, nullptr);
586
+ if (len <= 0) return env.Null();
587
+ std::string utf8(len, '\0');
588
+ WideCharToMultiByte(CP_UTF8, 0, wresult, -1, utf8.data(), len, nullptr, nullptr);
589
+ utf8.resize(len - 1);
590
+ return Napi::String::New(env, utf8);
591
+ #else
560
592
  const char* result = bindtextdomain(domain.c_str(), query ? nullptr : location.c_str());
561
593
  if (!query) bind_textdomain_codeset(domain.c_str(), "UTF-8");
562
594
  if (result == nullptr) return env.Null();
563
595
  return Napi::String::New(env, result);
596
+ #endif
564
597
  }
565
598
 
566
599
  // localeCategories() -> { ALL, COLLATE, CTYPE, MESSAGES, MONETARY, NUMERIC, TIME }