@gjsify/node-gi 0.22.0 → 0.24.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/README.md +10 -0
- package/gi.d.ts +1 -1
- package/gi.js +5 -2
- package/globals.d.ts +13 -0
- package/globals.js +6 -0
- package/index.js +50 -12
- package/overrides/byte-array.js +197 -0
- package/package.json +8 -1
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/src/marshal.cc +38 -1
- package/src/object.cc +26 -1
- package/system.js +41 -5
package/README.md
CHANGED
|
@@ -678,6 +678,16 @@ imports.signals.addSignalMethods(emitter); // the pure-JS Signals mixin
|
|
|
678
678
|
emitter.connect('ready', () => imports.mainloop.quit());
|
|
679
679
|
imports.mainloop.timeout_add(50, () => { emitter.emit('ready'); return false; });
|
|
680
680
|
imports.mainloop.run(); // thin GLib.MainLoop wrapper
|
|
681
|
+
|
|
682
|
+
// imports.byteArray — the legacy byte-array module, with GJS semantics
|
|
683
|
+
// (fromString/toString are zero-terminated + fatal-decode; fromGBytes/toGBytes
|
|
684
|
+
// round-trip GLib.Bytes; fromArray wraps in the legacy ByteArray class). This
|
|
685
|
+
// is the seam @gjsify/utils' cli()/gbytesToUint8Array — and through them
|
|
686
|
+
// @gjsify/os + @gjsify/child_process — read GLib subprocess output with:
|
|
687
|
+
const [ok, out] = imports.gi.GLib.spawn_command_line_sync('echo hi');
|
|
688
|
+
console.log(imports.byteArray.toString(out)); // 'hi\n'
|
|
689
|
+
const bytes = imports.byteArray.toGBytes(Uint8Array.of(1, 2, 3));
|
|
690
|
+
imports.byteArray.fromGBytes(bytes); // Uint8Array [1, 2, 3] (a copy)
|
|
681
691
|
```
|
|
682
692
|
|
|
683
693
|
A follow-up `--app node` build step will inject this automatically for any
|
package/gi.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export interface GObjectNamespace extends GiNamespace {
|
|
|
93
93
|
ParamSpec: ParamSpecFactories;
|
|
94
94
|
/**
|
|
95
95
|
* The FULL introspected `GParamFlags` bitfield (READABLE/WRITABLE/READWRITE/
|
|
96
|
-
* CONSTRUCT/CONSTRUCT_ONLY/LAX_VALIDATION/STATIC_
|
|
96
|
+
* CONSTRUCT/CONSTRUCT_ONLY/LAX_VALIDATION/STATIC_.../EXPLICIT_NOTIFY/DEPRECATED/
|
|
97
97
|
* …) — the named members below are the common convenience bits; the index
|
|
98
98
|
* signature covers the remaining introspected members.
|
|
99
99
|
*/
|
package/gi.js
CHANGED
|
@@ -985,7 +985,10 @@ let pumpRefCount = 0;
|
|
|
985
985
|
* @returns {() => void}
|
|
986
986
|
*/
|
|
987
987
|
export function startMainContextPump() {
|
|
988
|
-
|
|
988
|
+
// Node: the libuv↔GLib bridge already co-pumps. GJS: the host loop IS
|
|
989
|
+
// GLib's default main context — a JS-timer pump would only iterate the
|
|
990
|
+
// context recursively from within its own dispatch.
|
|
991
|
+
if (native.isNodeRuntime || native.RUNTIME === 'gjs') return () => {};
|
|
989
992
|
pumpRefCount++;
|
|
990
993
|
if (pumpTimer === null) {
|
|
991
994
|
// ~4 ms cadence: low latency without busy-spinning. Each tick drains every
|
|
@@ -1011,7 +1014,7 @@ export function startMainContextPump() {
|
|
|
1011
1014
|
|
|
1012
1015
|
/** Decrement the main-context pump reference count; clears the timer at zero. */
|
|
1013
1016
|
export function stopMainContextPump() {
|
|
1014
|
-
if (native.isNodeRuntime || pumpRefCount === 0) return;
|
|
1017
|
+
if (native.isNodeRuntime || native.RUNTIME === 'gjs' || pumpRefCount === 0) return;
|
|
1015
1018
|
pumpRefCount--;
|
|
1016
1019
|
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
1017
1020
|
clearInterval(pumpTimer);
|
package/globals.d.ts
CHANGED
|
@@ -15,6 +15,19 @@ export interface GjsImports {
|
|
|
15
15
|
system: SystemModule;
|
|
16
16
|
/** `imports.gettext` — the `@gjsify/node-gi/gettext` module (no-translation passthrough). */
|
|
17
17
|
gettext: GettextModule;
|
|
18
|
+
/** `imports.byteArray` — the legacy GJS byte-array module (GJS semantics: zero-terminated, fatal decode). */
|
|
19
|
+
byteArray: {
|
|
20
|
+
ByteArray: new (arg?: number | Uint8Array) => {
|
|
21
|
+
length: number;
|
|
22
|
+
toString(encoding?: string): string;
|
|
23
|
+
toGBytes(): unknown;
|
|
24
|
+
};
|
|
25
|
+
fromArray(array: Iterable<number>): unknown;
|
|
26
|
+
fromGBytes(bytes: unknown): Uint8Array;
|
|
27
|
+
fromString(string: string, encoding?: string): Uint8Array;
|
|
28
|
+
toGBytes(array: Uint8Array): unknown;
|
|
29
|
+
toString(byteArray: Uint8Array, encoding?: string): string;
|
|
30
|
+
};
|
|
18
31
|
versions: Record<string, unknown>;
|
|
19
32
|
}
|
|
20
33
|
|
package/globals.js
CHANGED
|
@@ -22,6 +22,7 @@ import cairo from './cairo.js';
|
|
|
22
22
|
// Legacy `imports.signals` (the pure-JS Signals mixin) + `imports.mainloop` (the
|
|
23
23
|
// GLib.MainLoop convenience layer) — the GJS script modules many older sources use.
|
|
24
24
|
import * as signalsModule from './overrides/_signals.js';
|
|
25
|
+
import { createByteArray } from './overrides/byte-array.js';
|
|
25
26
|
import { createMainloop } from './overrides/mainloop.js';
|
|
26
27
|
import { createPackage } from './overrides/package.js';
|
|
27
28
|
|
|
@@ -109,6 +110,11 @@ function makeImports() {
|
|
|
109
110
|
system,
|
|
110
111
|
gettext,
|
|
111
112
|
cairo,
|
|
113
|
+
// `imports.byteArray` — the legacy GJS byte-array module
|
|
114
|
+
// (fromGBytes/toGBytes/fromString/toString/fromArray/ByteArray), the seam
|
|
115
|
+
// `@gjsify/utils`' gbytesToUint8Array/cli() read subprocess output through.
|
|
116
|
+
// GLib is only pulled in lazily (toGBytes), so seeding it here is free.
|
|
117
|
+
byteArray: createByteArray(requireGi),
|
|
112
118
|
signals: { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll },
|
|
113
119
|
// GJS's `imports.searchPath` — `imports.package.init` unshifts the app's
|
|
114
120
|
// moduledir onto it. Inert on node-gi (ESM + gi://), but present + safe so
|
package/index.js
CHANGED
|
@@ -49,19 +49,25 @@ const require = createRequire(import.meta.url);
|
|
|
49
49
|
const here = dirname(fileURLToPath(import.meta.url)); // package root
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Which JS runtime we are on. The Node-API addon loads on all
|
|
52
|
+
* Which JS runtime we are on. The Node-API addon loads on all four: Node, Bun
|
|
53
|
+
* and Deno implement Node-API natively; under GJS the `@gjsify/napi` shim is
|
|
54
|
+
* the N-API host and the addon is loaded through it (`loadNative` below). The
|
|
53
55
|
* libuv-backed main-loop bridge (startMainLoop) is Node-only — Deno exports no
|
|
54
56
|
* libuv symbols and Bun panics on uv_backend_fd — so Bun/Deno use the portable
|
|
55
|
-
* GLib-iteration pump instead (see gi.js)
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* GLib-iteration pump instead (see gi.js), and GJS needs NEITHER: the host
|
|
58
|
+
* loop already IS GLib's default main context. Detection order matters: GJS is
|
|
59
|
+
* probed first via its own globals (`imports` + `print` — no other runtime
|
|
60
|
+
* defines both), then Bun/Deno (both expose a `process` shim), then Node.
|
|
61
|
+
* @type {'bun' | 'deno' | 'gjs' | 'node'}
|
|
58
62
|
*/
|
|
59
63
|
export const RUNTIME =
|
|
60
|
-
typeof globalThis.
|
|
61
|
-
? '
|
|
62
|
-
: typeof globalThis.
|
|
63
|
-
? '
|
|
64
|
-
: '
|
|
64
|
+
typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
|
|
65
|
+
? 'gjs'
|
|
66
|
+
: typeof globalThis.Bun !== 'undefined'
|
|
67
|
+
? 'bun'
|
|
68
|
+
: typeof globalThis.Deno !== 'undefined'
|
|
69
|
+
? 'deno'
|
|
70
|
+
: 'node';
|
|
65
71
|
|
|
66
72
|
/** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
|
|
67
73
|
export const isNodeRuntime = RUNTIME === 'node';
|
|
@@ -84,9 +90,37 @@ function nativeCandidates() {
|
|
|
84
90
|
return [prebuild, release, debug];
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
// GJS host mode: the addon is loaded through the @gjsify/napi shim, not
|
|
94
|
+
// `require()`. The loader native is `globalThis.__gjsifyNapiLoadAddon`,
|
|
95
|
+
// installed by `GjsifyNapi.init()`; if another consumer (the @gjsify/napi L1)
|
|
96
|
+
// already captured + deleted it, init() re-defines it (idempotent). The
|
|
97
|
+
// GjsifyNapi typelib must be resolvable (GI_TYPELIB_PATH/LD_LIBRARY_PATH →
|
|
98
|
+
// the shim prebuild).
|
|
99
|
+
function gjsLoadAddon(path) {
|
|
100
|
+
let load = globalThis.__gjsifyNapiLoadAddon;
|
|
101
|
+
if (typeof load !== 'function') {
|
|
102
|
+
const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
|
|
103
|
+
if (!GjsifyNapi || !GjsifyNapi.init()) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
|
|
106
|
+
'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
load = globalThis.__gjsifyNapiLoadAddon;
|
|
110
|
+
if (typeof load !== 'function') {
|
|
111
|
+
throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
|
|
112
|
+
}
|
|
113
|
+
// Capture-then-delete, the same no-side-channel discipline as the L1.
|
|
114
|
+
delete globalThis.__gjsifyNapiLoadAddon;
|
|
115
|
+
}
|
|
116
|
+
return load(path);
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
function loadNative() {
|
|
88
120
|
for (const candidate of nativeCandidates()) {
|
|
89
|
-
if (existsSync(candidate))
|
|
121
|
+
if (existsSync(candidate)) {
|
|
122
|
+
return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
|
|
123
|
+
}
|
|
90
124
|
}
|
|
91
125
|
throw new Error(
|
|
92
126
|
`@gjsify/node-gi: native addon not found for ${RUNTIME} on ${process.platform}-${process.arch}. ` +
|
|
@@ -94,7 +128,8 @@ function loadNative() {
|
|
|
94
128
|
'Run `node-gyp rebuild` in ' +
|
|
95
129
|
here +
|
|
96
130
|
' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
|
|
97
|
-
'or install a package build that ships a prebuild for your platform.'
|
|
131
|
+
'or install a package build that ships a prebuild for your platform. ' +
|
|
132
|
+
'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
|
|
98
133
|
);
|
|
99
134
|
}
|
|
100
135
|
|
|
@@ -133,7 +168,10 @@ try {
|
|
|
133
168
|
// matching Node's checkpoint and GJS (SpiderMonkey drains the promise-job queue
|
|
134
169
|
// when the last JS frame exits). Node never registers — its checkpoint already
|
|
135
170
|
// runs natively, so the Node path is byte-identical to before.
|
|
136
|
-
|
|
171
|
+
// GJS never registers: SpiderMonkey/GJS drains the promise-job queue from its
|
|
172
|
+
// own main-loop source whenever the last JS frame exits — the exact semantic
|
|
173
|
+
// this checkpoint recreates for Bun/Deno.
|
|
174
|
+
if (!isNodeRuntime && RUNTIME !== 'gjs') {
|
|
137
175
|
try {
|
|
138
176
|
let drain = null;
|
|
139
177
|
if (RUNTIME === 'bun') {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
|
|
2
|
+
// SPDX-FileCopyrightText: 2017 Philip Chimento <philip.chimento@gmail.com>
|
|
3
|
+
//
|
|
4
|
+
// Adapted from GJS (refs/gjs/modules/script/byteArray.js + the native
|
|
5
|
+
// refs/gjs/gjs/byteArray.cpp `imports._byteArrayNative` functions). Copyright
|
|
6
|
+
// (c) 2017 Philip Chimento. MIT OR LGPL-2.0-or-later.
|
|
7
|
+
// Modifications: the legacy `imports.byteArray` module ported to
|
|
8
|
+
// @gjsify/node-gi. The native `fromGBytes`/`fromString`/`toString` are
|
|
9
|
+
// reimplemented in JS over the engine's boxed-GBytes surface and
|
|
10
|
+
// TextEncoder/TextDecoder, preserving GJS semantics:
|
|
11
|
+
// - toString/fromString are ZERO-TERMINATED (data truncates at the first NUL
|
|
12
|
+
// byte — gjs/text-encoding.cpp `zero_terminated_length`), and decoding is
|
|
13
|
+
// FATAL (invalid sequences throw a TypeError, like gjs's fatal=true).
|
|
14
|
+
// - fromGBytes/fromString return a COPY as a plain Uint8Array carrying the
|
|
15
|
+
// legacy own `toString(encoding)` method gjs tacks on
|
|
16
|
+
// (`define_legacy_tostring`), minus gjs's once-per-callsite deprecation
|
|
17
|
+
// warning (stderr-only noise a byte-golden never sees).
|
|
18
|
+
// - toGBytes routes through `new GLib.Bytes(array)` exactly as the gjs
|
|
19
|
+
// script module does; GLib is loaded lazily so building `imports` never
|
|
20
|
+
// pulls the namespace in.
|
|
21
|
+
|
|
22
|
+
// Truncate a byte array at the first NUL, per gjs's ZERO_TERMINATED handling
|
|
23
|
+
// (gjs/text-encoding.cpp zero_terminated_length).
|
|
24
|
+
function zeroTerminated(u8) {
|
|
25
|
+
const nul = u8.indexOf(0);
|
|
26
|
+
return nul === -1 ? u8 : u8.subarray(0, nul);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// gjs routes non-UTF-8 encodings through iconv; Node's TextDecoder (decode
|
|
30
|
+
// side) accepts WHATWG labels which cover the practical iconv set, so decoding
|
|
31
|
+
// just passes the label through. The ENCODE side has no general converter on
|
|
32
|
+
// Node — map the labels gjs consumers actually use onto Buffer encodings and
|
|
33
|
+
// throw a clear error for the rest.
|
|
34
|
+
const ENCODE_LABELS = new Map([
|
|
35
|
+
['latin1', 'latin1'],
|
|
36
|
+
['iso-8859-1', 'latin1'],
|
|
37
|
+
['iso8859-1', 'latin1'],
|
|
38
|
+
['ascii', 'ascii'],
|
|
39
|
+
['us-ascii', 'ascii'],
|
|
40
|
+
['utf-16', 'utf16le'],
|
|
41
|
+
['utf-16le', 'utf16le'],
|
|
42
|
+
['utf16le', 'utf16le'],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function isUtf8Label(encoding) {
|
|
46
|
+
const label = String(encoding).toLowerCase();
|
|
47
|
+
return label === 'utf-8' || label === 'utf8';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The legacy own `toString(encoding)` gjs defines on arrays returned by
|
|
51
|
+
// fromString/fromGBytes/fromArray (byteArray.cpp define_legacy_tostring).
|
|
52
|
+
function defineLegacyToString(u8, toString) {
|
|
53
|
+
Object.defineProperty(u8, 'toString', {
|
|
54
|
+
value(encoding = 'utf-8') {
|
|
55
|
+
return toString(u8, encoding);
|
|
56
|
+
},
|
|
57
|
+
writable: true,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
configurable: true,
|
|
60
|
+
});
|
|
61
|
+
return u8;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the legacy `imports.byteArray` module bound to the L1 backend.
|
|
66
|
+
* @param {(ns: string, version?: string) => Record<string, any>} requireGi
|
|
67
|
+
* @returns {{ ByteArray: any, fromArray: Function, fromGBytes: Function, fromString: Function, toGBytes: Function, toString: Function }}
|
|
68
|
+
*/
|
|
69
|
+
export function createByteArray(requireGi) {
|
|
70
|
+
/**
|
|
71
|
+
* @param {Uint8Array} byteArray bytes to decode
|
|
72
|
+
* @param {string} [encoding] a WHATWG/iconv label, default UTF-8
|
|
73
|
+
* @returns {string} the decoded string, stopping at the first NUL byte
|
|
74
|
+
*/
|
|
75
|
+
function toString(byteArray, encoding = 'utf-8') {
|
|
76
|
+
// Plain Error, not TypeError — gjs raises these argument failures via
|
|
77
|
+
// gjs_throw (Error); only the GBytes typecheck below is a TypeError.
|
|
78
|
+
if (!(byteArray instanceof Uint8Array)) throw new Error('Argument to decode() must be a Uint8Array');
|
|
79
|
+
const data = zeroTerminated(byteArray);
|
|
80
|
+
if (data.length === 0) return '';
|
|
81
|
+
// fatal:true mirrors gjs's fatal decode (invalid sequences throw).
|
|
82
|
+
return new TextDecoder(encoding, { fatal: true }).decode(data);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} string the string to encode
|
|
87
|
+
* @param {string} [encoding] a WHATWG/iconv label, default UTF-8
|
|
88
|
+
* @returns {Uint8Array} the encoded bytes, truncated at the first NUL byte
|
|
89
|
+
*/
|
|
90
|
+
function fromString(string, encoding = 'utf-8') {
|
|
91
|
+
if (typeof string !== 'string') throw new Error('Argument to fromString() must be a string');
|
|
92
|
+
let encoded;
|
|
93
|
+
if (isUtf8Label(encoding)) {
|
|
94
|
+
encoded = new TextEncoder().encode(string);
|
|
95
|
+
} else {
|
|
96
|
+
const bufferEncoding = ENCODE_LABELS.get(String(encoding).toLowerCase());
|
|
97
|
+
if (!bufferEncoding) throw new Error(`Unsupported encoding for fromString(): ${encoding}`);
|
|
98
|
+
const buf = Buffer.from(string, bufferEncoding);
|
|
99
|
+
encoded = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
100
|
+
}
|
|
101
|
+
const data = zeroTerminated(encoded);
|
|
102
|
+
// Copy into a fresh plain Uint8Array (gjs returns a standalone buffer, and
|
|
103
|
+
// Buffer.from may hand out a view into Node's shared slab).
|
|
104
|
+
const out = new Uint8Array(data.length);
|
|
105
|
+
out.set(data);
|
|
106
|
+
return defineLegacyToString(out, toString);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {any} bytes a GLib.Bytes boxed handle
|
|
111
|
+
* @returns {Uint8Array} a copy of the bytes as a plain Uint8Array
|
|
112
|
+
*/
|
|
113
|
+
function fromGBytes(bytes) {
|
|
114
|
+
// The engine's boxed GBytes proxy carries `toArray()` (gi.js wrapBoxed) —
|
|
115
|
+
// the same convenience gjs adds to GLib.Bytes.prototype. Its absence means
|
|
116
|
+
// the value is not a GBytes, matching gjs's G_TYPE_BYTES typecheck throw.
|
|
117
|
+
if (!bytes || typeof bytes.toArray !== 'function') {
|
|
118
|
+
throw new TypeError('Argument to fromGBytes() must be a GLib.Bytes');
|
|
119
|
+
}
|
|
120
|
+
const data = bytes.toArray();
|
|
121
|
+
// toArray() already returns a copy of the GBytes data; re-view it as a
|
|
122
|
+
// PLAIN Uint8Array (gjs returns Uint8Array, not Node's Buffer subclass).
|
|
123
|
+
const out = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
124
|
+
return defineLegacyToString(out, toString);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {Uint8Array} array the Uint8Array to convert to GLib.Bytes
|
|
129
|
+
* @returns {any} a GLib.Bytes boxed handle
|
|
130
|
+
*/
|
|
131
|
+
function toGBytes(array) {
|
|
132
|
+
if (!(array instanceof Uint8Array)) throw new Error('Argument to ByteArray.toGBytes() must be a Uint8Array');
|
|
133
|
+
const GLib = requireGi('GLib', '2.0');
|
|
134
|
+
return new GLib.Bytes(array);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The legacy ByteArray wrapper class, ported verbatim from
|
|
138
|
+
// refs/gjs/modules/script/byteArray.js (backwards compatibility only).
|
|
139
|
+
const ByteArray = class ByteArray {
|
|
140
|
+
constructor(arg = 0) {
|
|
141
|
+
if (arg instanceof Uint8Array) this._array = arg;
|
|
142
|
+
else this._array = new Uint8Array(arg);
|
|
143
|
+
return new Proxy(this, ByteArray);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static get(target, prop, receiver) {
|
|
147
|
+
if (!Number.isNaN(Number.parseInt(prop))) return Reflect.get(target._array, prop);
|
|
148
|
+
return Reflect.get(target, prop, receiver);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static set(target, prop, val, receiver) {
|
|
152
|
+
const ix = Number.parseInt(prop);
|
|
153
|
+
if (!Number.isNaN(ix)) {
|
|
154
|
+
if (ix >= target._array.length) {
|
|
155
|
+
const newArray = new Uint8Array(ix + 1);
|
|
156
|
+
newArray.set(target._array);
|
|
157
|
+
target._array = newArray;
|
|
158
|
+
}
|
|
159
|
+
return Reflect.set(target._array, prop, val);
|
|
160
|
+
}
|
|
161
|
+
return Reflect.set(target, prop, val, receiver);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get length() {
|
|
165
|
+
return this._array.length;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
set length(newLength) {
|
|
169
|
+
if (newLength === this._array.length) return;
|
|
170
|
+
if (newLength < this._array.length) {
|
|
171
|
+
this._array = new Uint8Array(this._array.buffer, 0, newLength);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const newArray = new Uint8Array(newLength);
|
|
175
|
+
newArray.set(this._array);
|
|
176
|
+
this._array = newArray;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
toString(encoding = 'UTF-8') {
|
|
180
|
+
return toString(this._array, encoding);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
toGBytes() {
|
|
184
|
+
return toGBytes(this._array);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {Iterable<number>} array an iterable to convert into a ByteArray wrapper
|
|
190
|
+
* @returns {InstanceType<ByteArray>}
|
|
191
|
+
*/
|
|
192
|
+
function fromArray(array) {
|
|
193
|
+
return new ByteArray(Uint8Array.from(array));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { ByteArray, fromArray, fromGBytes, fromString, toGBytes, toString };
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
"prebuilds"
|
|
59
59
|
],
|
|
60
60
|
"gjsify": {
|
|
61
|
+
"platforms": [
|
|
62
|
+
"linux-x64",
|
|
63
|
+
"linux-arm64",
|
|
64
|
+
"darwin-arm64",
|
|
65
|
+
"win32-x64"
|
|
66
|
+
],
|
|
61
67
|
"runtimes": {
|
|
62
68
|
"gjs": "none",
|
|
63
69
|
"node": "polyfill",
|
|
@@ -79,6 +85,7 @@
|
|
|
79
85
|
"test:dbus": "dbus-run-session -- env NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
|
|
80
86
|
"test:bun": "node scripts/cross-runtime.mjs bun",
|
|
81
87
|
"test:conformance": "node scripts/conformance.mjs",
|
|
88
|
+
"test:conformance:gjs-napi": "node scripts/conformance.mjs --runtimes=gjs-napi",
|
|
82
89
|
"test:gimarshalling": "node scripts/gimarshalling.mjs",
|
|
83
90
|
"test:deno": "node scripts/cross-runtime.mjs deno",
|
|
84
91
|
"test:cairo": "NODE_GI_NATIVE=build node --test test/cairo.test.mjs",
|
|
Binary file
|
|
Binary file
|
package/src/marshal.cc
CHANGED
|
@@ -153,6 +153,43 @@ BoxedHandle* TryGetBoxedHandle(Napi::Value v) {
|
|
|
153
153
|
return ext.Data();
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// The pointer to hand a callee for a boxed/struct IN argument, honouring the
|
|
157
|
+
// arg's TRANSFER annotation.
|
|
158
|
+
//
|
|
159
|
+
// A transfer-full (or -container) IN arg is ADOPTED by the callee: it will free
|
|
160
|
+
// the pointer. Handing over the very block this handle still owns makes both
|
|
161
|
+
// sides free it — the finalizer's g_boxed_free then hits already-released
|
|
162
|
+
// memory (`free(): invalid pointer`, SIGABRT). The canonical case is
|
|
163
|
+
// `GstWebRTC.WebRTCSessionDescription.new(type, sdp)`, whose `sdp` param is
|
|
164
|
+
// `(transfer full)`: the fresh `GstSDPMessage` from
|
|
165
|
+
// `GstSdp.SDPMessage.new_from_text()` (an OUT `(transfer full)`, so our handle
|
|
166
|
+
// owns it) got freed once by `gst_webrtc_session_description_free` and again by
|
|
167
|
+
// its own finalizer.
|
|
168
|
+
//
|
|
169
|
+
// gjs solves this by COPYING on transfer — refs/gjs/gi/wrapperutils.h
|
|
170
|
+
// `GIWrapperBase::transfer_to_gi_argument` (`GI_DIRECTION_IN &&
|
|
171
|
+
// transfer != GI_TRANSFER_NOTHING` → `Instance::copy_ptr`), with
|
|
172
|
+
// refs/gjs/gi/struct.h `StructInstance::copy_ptr` = `g_variant_ref` for
|
|
173
|
+
// GVariant else refs/gjs/gi/boxed.h `BoxedInstance::copy_ptr` = `g_boxed_copy`.
|
|
174
|
+
// Mirror that: the callee owns an independent instance, the handle keeps (and
|
|
175
|
+
// still owns) its own. Transfer-NOTHING is a plain borrow — unchanged.
|
|
176
|
+
//
|
|
177
|
+
// A handle with no copy function (a plain, non-boxed C struct — G_TYPE_INVALID,
|
|
178
|
+
// or a `rawOwned` caller-allocates OUT block) cannot be duplicated. gjs throws
|
|
179
|
+
// there; we instead RELINQUISH ownership so the finalizer stays out of the
|
|
180
|
+
// callee's way — a leak-free hand-over is impossible either way, and not
|
|
181
|
+
// double-freeing is strictly safer than the alternative.
|
|
182
|
+
gpointer TransferBoxedIn(BoxedHandle* h, GITransfer transfer) {
|
|
183
|
+
if (h == nullptr) return nullptr;
|
|
184
|
+
if (transfer == GI_TRANSFER_NOTHING || h->ptr == nullptr) return h->ptr;
|
|
185
|
+
if (h->gtype == G_TYPE_VARIANT) return g_variant_ref(static_cast<GVariant*>(h->ptr));
|
|
186
|
+
if (h->gtype != G_TYPE_INVALID && G_TYPE_IS_BOXED(h->gtype))
|
|
187
|
+
return g_boxed_copy(h->gtype, h->ptr);
|
|
188
|
+
h->owns = false;
|
|
189
|
+
h->rawOwned = false;
|
|
190
|
+
return h->ptr;
|
|
191
|
+
}
|
|
192
|
+
|
|
156
193
|
// Read a boxed handle's pointer if `v` is one (tag-checked; no deref of ptr).
|
|
157
194
|
bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
|
|
158
195
|
if (!v.IsExternal()) return false;
|
|
@@ -440,7 +477,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
440
477
|
.ThrowAsJavaScriptException();
|
|
441
478
|
return false;
|
|
442
479
|
}
|
|
443
|
-
out->v_pointer = h
|
|
480
|
+
out->v_pointer = TransferBoxedIn(h, transfer);
|
|
444
481
|
handled = true;
|
|
445
482
|
}
|
|
446
483
|
}
|
package/src/object.cc
CHANGED
|
@@ -258,7 +258,32 @@ Napi::Value GValueToJs(Napi::Env env, const GValue* v) {
|
|
|
258
258
|
// NOT special-cased here: gjs leaves it as a GLib.Bytes boxed handle
|
|
259
259
|
// (value.cpp has no GBytes case → the generic boxed branch), which is what the
|
|
260
260
|
// MakeBoxedHandle below already produces (verified: a GBytes signal param →
|
|
261
|
-
// `instanceof GLib.Bytes`).
|
|
261
|
+
// `instanceof GLib.Bytes`).
|
|
262
|
+
//
|
|
263
|
+
// G_TYPE_STRV (a NULL-terminated gchar**, e.g. Gtk.Widget:css-classes,
|
|
264
|
+
// Gio.ThemedIcon:names, or any Vala `public string[] x { get; }` property —
|
|
265
|
+
// valac emits g_param_spec_boxed(…, G_TYPE_STRV) for those). This is the READ
|
|
266
|
+
// half of the pair JsToGValue's GStrv branch opened: without it a GStrv
|
|
267
|
+
// property fell through to MakeBoxedHandle and JS got an opaque boxed handle
|
|
268
|
+
// instead of an array — SILENTLY, because a handle is a truthy object whose
|
|
269
|
+
// `.length` is undefined, so `arr ?? []` keeps it and every index/`for` over it
|
|
270
|
+
// yields nothing (this is how @gjsify/http lost every request header on the
|
|
271
|
+
// node-gi bridge: `bridgeReq.header_pairs` became a handle and the pair loop
|
|
272
|
+
// ran zero times). Mirrors GJS gjs_value_from_g_value_internal
|
|
273
|
+
// (refs/gjs/gi/value.cpp:1082 `gtype == G_TYPE_STRV` → gjs_array_from_strv).
|
|
274
|
+
// NULL → an EMPTY ARRAY, not null: GJS explicitly excludes G_TYPE_STRV from its
|
|
275
|
+
// "pointer-valued NULL → JS null" pre-check (value.cpp:1023) and documents the
|
|
276
|
+
// choice in gjs_array_from_strv (arg.cpp:2267) — "clients would need to always
|
|
277
|
+
// check for both an empty array and null" otherwise. g_value_get_boxed BORROWS;
|
|
278
|
+
// every string is copied into JS, so nothing is owned here.
|
|
279
|
+
if (G_VALUE_HOLDS(v, G_TYPE_STRV)) {
|
|
280
|
+
const gchar* const* strv = static_cast<const gchar* const*>(g_value_get_boxed(v));
|
|
281
|
+
guint len = 0;
|
|
282
|
+
while (strv != nullptr && strv[len] != nullptr) len++;
|
|
283
|
+
Napi::Array arr = Napi::Array::New(env, len);
|
|
284
|
+
for (guint i = 0; i < len; i++) arr.Set(i, Napi::String::New(env, strv[i]));
|
|
285
|
+
return arr;
|
|
286
|
+
}
|
|
262
287
|
if (G_VALUE_HOLDS(v, G_TYPE_BYTE_ARRAY)) {
|
|
263
288
|
GByteArray* ba = static_cast<GByteArray*>(g_value_get_boxed(v));
|
|
264
289
|
if (ba == nullptr) return env.Null();
|
package/system.js
CHANGED
|
@@ -20,25 +20,61 @@
|
|
|
20
20
|
// returning the same shape GJS does — they exist so GJS source that calls them
|
|
21
21
|
// keeps running unmodified, not to reproduce SpiderMonkey internals.
|
|
22
22
|
|
|
23
|
+
// The RUNTIME's own `process`, bound ONCE at module evaluation.
|
|
24
|
+
//
|
|
25
|
+
// `globalThis.process` is NOT a safe source for this module: a GJS program run
|
|
26
|
+
// through the reverse bridge routinely installs the gjsify `@gjsify/process`
|
|
27
|
+
// polyfill over it (`@gjsify/node-globals/register` defines it unconditionally),
|
|
28
|
+
// and that polyfill's `exit()` takes its GJS path whenever `imports.system.exit`
|
|
29
|
+
// exists — i.e. it delegates back into THIS module. Reading `globalThis.process`
|
|
30
|
+
// in `exit()` closes that loop into an infinite, GLib-idle-driven recursion
|
|
31
|
+
// (exit → polyfill → system.exit → polyfill → …) that spins at 100 % CPU and
|
|
32
|
+
// never terminates. `getBuiltinModule('node:process')` (Node ≥ 22.3 / Bun /
|
|
33
|
+
// Deno 2) returns the real builtin regardless of what the global currently is;
|
|
34
|
+
// the module-evaluation capture is the fallback for hosts without it — this
|
|
35
|
+
// module is loaded from `@gjsify/node-gi/globals`, before any bundle body (and
|
|
36
|
+
// therefore before any register side effect) runs.
|
|
37
|
+
const runtimeProcess = (() => {
|
|
38
|
+
const hostProcess = typeof process !== 'undefined' ? process : undefined;
|
|
39
|
+
try {
|
|
40
|
+
const builtin = hostProcess?.getBuiltinModule?.('node:process');
|
|
41
|
+
if (builtin && typeof builtin.exit === 'function') return builtin;
|
|
42
|
+
} catch {
|
|
43
|
+
// getBuiltinModule missing or refusing the specifier — use the captured one.
|
|
44
|
+
}
|
|
45
|
+
return hostProcess;
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
const runtimeExit = typeof runtimeProcess?.exit === 'function' ? runtimeProcess.exit.bind(runtimeProcess) : undefined;
|
|
49
|
+
|
|
23
50
|
// `programArgs` mirrors GJS's ARGV: the script arguments, excluding the
|
|
24
51
|
// interpreter (argv[0]) and the script path (argv[1]).
|
|
25
52
|
function readProgramArgs() {
|
|
26
|
-
return
|
|
53
|
+
return Array.isArray(runtimeProcess?.argv) ? runtimeProcess.argv.slice(2) : [];
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
// `programInvocationName` / `programPath` track the running script — Node's
|
|
30
57
|
// `process.argv[1]`.
|
|
31
58
|
function readProgramInvocationName() {
|
|
32
|
-
return
|
|
59
|
+
return runtimeProcess?.argv?.[1] || '';
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
function readProgramPath() {
|
|
36
|
-
return
|
|
63
|
+
return runtimeProcess?.argv?.[1] || null;
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Exit the process (GJS `System.exit`).
|
|
68
|
+
*
|
|
69
|
+
* Terminal by contract: under gjs this is the `exit()` syscall — it never
|
|
70
|
+
* returns and nothing a program installs can intercept it. The Node/Bun/Deno
|
|
71
|
+
* equivalent is the RUNTIME's own `process.exit`, resolved above; routing
|
|
72
|
+
* through `globalThis.process` instead is what produced the never-exiting
|
|
73
|
+
* reverse-bridge process (see the `runtimeProcess` note and
|
|
74
|
+
* `conformance/programs/system-exit-lifetime.conf.mjs`).
|
|
75
|
+
*/
|
|
40
76
|
export function exit(code) {
|
|
41
|
-
if (
|
|
77
|
+
if (runtimeExit !== undefined) runtimeExit(code ?? 0);
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
/** Trigger a garbage collection if the host exposed `globalThis.gc` (`--expose-gc`). */
|