@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 +25 -0
- package/cairo.js +23 -0
- package/gi.js +130 -66
- package/overrides/gio-settings.js +2 -24
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-arm64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/cairo.cc +66 -1
- package/src/calls.cc +59 -26
- package/src/common.h +159 -1
- package/src/marshal.cc +52 -23
- package/src/object.cc +79 -10
- package/src/private.cc +33 -0
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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', [
|
|
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', [
|
|
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
|
|
548
|
-
|
|
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
|
-
|
|
1565
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
//
|
|
14
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
1316
|
+
GIFunctionInfo* func = ResolveInstanceMethodCached(gtype, method);
|
|
1266
1317
|
|
|
1267
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
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:
|
|
540
|
-
case GI_TYPE_TAG_UINT8:
|
|
541
|
-
case GI_TYPE_TAG_INT16:
|
|
542
|
-
case GI_TYPE_TAG_UINT16:
|
|
543
|
-
case GI_TYPE_TAG_INT32:
|
|
544
|
-
case GI_TYPE_TAG_UINT32:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
case
|
|
1606
|
-
case
|
|
1607
|
-
case
|
|
1608
|
-
case
|
|
1609
|
-
case
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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 }
|