@gjsify/node-gi
v0.13.0
Published
GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.
Readme
@gjsify/node-gi
GObject-Introspection runtime for Node.js — the native engine that lets
unchanged GJS / GObject-Introspection code run under Node.js, the inverse of
gjsify's Node/Web/DOM → GJS direction (see the gjsify AGENTS.md
### Axis 5 active track).
It loads gi:// namespaces (GLib, GObject, Gio, …) via libgirepository and
exposes them with GJS-compatible semantics, so the same source builds and runs
on both GJS and Node via gjsify build --app {gjs,node}.
Status: milestone 1 (headless core) — in progress. The native engine over the modern
girepository-2.0API now does: resolve the default repository,requirea namespace, enumerate its infos; call namespace-level functions and instance methods (own + implemented-interface methods, up the parent chain) with value marshalling (numbers, booleans, strings, GObjects, enums/flags); construct GObjects and read/write properties (GValue round-trip); connect / emit / disconnect signals (incl. detailed names likenotify::prop); and register GObject subclasses (subtype + construct-by-type, inheriting the parent's properties/methods, plus custom properties + signals declared on the subclass). Ownership rides N-API finalizers (no V8-GC reentrancy). On top of the engine, an L1 GJS-compatibility layer (@gjsify/node-gi/gi,requireGi) surfaces a GJS-shaped namespace:new Gio.SimpleAction({ name }),action.nameproperty access,action.get_name()methods,.connect()/.emit()/.disconnect(), and enums / flags / constants (Gio.BusType.SESSION,GLib.PRIORITY_DEFAULT); constructor/static methods (Gio.File.new_for_path(...)); and both snake_case and camelCase accessors. A libuv↔GLib mainloop bridge (startMainLoop, auto-attached byrequireGi) nests Node's libuv loop inside the GLib loop, so a blockingGLib.MainLoop.run()keeps Node's timers/I/O alive — including the boxed/struct slice that needs (GLib.MainLoop.new(...)→ a boxed handle →.run()/.quit()). JS functions marshal as GI callbacks via an ffi closure (GLib.timeout_add/idle_addfire from the loop, the boolean return drives source continuation; the hidden user_data/destroy slots are auto-filled). register GObject subclasses (subtype + construct-by-type, inheriting the parent's properties/methods, plus custom properties + signals, plus vfunc overrides — a JS function overriding a parent GObject vfunc, hooked into the new type's class vtable). The gjsify--app nodebundler integration already rewritesgi://onto the L1 layer. vfunc chain-up to the parent implementation (with the toggle-ref GC bridge) and general struct field access land in subsequent drops — for now a vfunc override fully replaces the inherited implementation.
Provenance
Derived from node-gtk (romgrk and
contributors, MIT) — vendored and rewritten under MIT (see LICENSE). The
native binding is retargeted to girepository-2.0 (the GIRepository merged
into GLib ≥ 2.80); the standalone libgirepository-1.0 node-gtk linked is no
longer shipped on modern systems. GJS's own gi/repo.cpp is the reference for
the girepository-2.0 API surface. node-gtk's own examples and tests are not
vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
Requirements
- Node.js ≥ 20
- A C++ toolchain (
g++/clang,make),node-gyp - GLib ≥ 2.80 development headers exposing
girepository-2.0(Fedora:glib2-devel gobject-introspection-devel gcc-c++; Debian/Ubuntu:libglib2.0-dev libgirepository-2.0-dev g++) - At runtime, the target libraries' typelibs must be installed (same as
gi://under GJS).
Build & test
npm install # builds the native addon via node-gyp (install script)
npm test # node --test (smoke tests)
# or rebuild explicitly:
npm run rebuildUsage (milestone 1)
import {
requireNamespace, listInfoNames, callFunction,
newObject, getProperty, setProperty, callMethod,
connectSignal, emitSignal,
} from '@gjsify/node-gi';
requireNamespace('GLib', '2.0');
console.log(listInfoNames('GLib').includes('MainLoop')); // true
console.log(callFunction('GLib', 'get_host_name')); // namespace function
requireNamespace('Gio', '2.0');
const action = newObject('Gio', 'SimpleAction', { name: 'greet', enabled: true });
console.log(getProperty(action, 'name')); // 'greet' (GValue round-trip)
console.log(callMethod(action, 'get_name')); // 'greet' (interface method)
callMethod(action, 'set_enabled', [false]); // method with an IN argument
const c = newObject('Gio', 'Cancellable', {});
connectSignal(c, 'cancelled', () => console.log('cancelled'));
emitSignal(c, 'cancelled');Register a GObject subclass and construct it (inherited properties + methods):
import { registerClass, constructType, callMethod } from '@gjsify/node-gi';
const MyAction = registerClass('MyAction', 'Gio', 'SimpleAction');
const a = constructType(MyAction, { name: 'greet', enabled: true });
console.log(callMethod(a, 'get_name')); // 'greet' (inherited GAction method)Declare custom properties and signals on the subclass:
import { registerClass, constructType, getProperty, setProperty,
connectSignal, emitSignal } from '@gjsify/node-gi';
const Counter = registerClass('Counter', 'GObject', 'Object', {
properties: [{ name: 'count', type: 'int', default: 0, minimum: 0, maximum: 100 }],
signals: [{ name: 'changed', paramTypes: ['int'] }],
});
const c = constructType(Counter, { count: 1 });
connectSignal(c, 'notify::count', (pspec) => console.log('changed:', pspec.name));
connectSignal(c, 'changed', (n) => console.log('count is now', n));
setProperty(c, 'count', 5); // fires notify::count
emitSignal(c, 'changed', [5]);
console.log(getProperty(c, 'count')); // 5Override a parent GObject vfunc with a JS function (the override runs as a method
on the instance — this is the GObject handle). Chain-up to the parent vfunc
lands in a later drop, so an override fully replaces the inherited implementation:
import { registerClass, constructType, getProperty } from '@gjsify/node-gi';
const Greeter = registerClass('Greeter', 'Gio', 'SimpleAction', {
vfuncs: {
// GObject's `constructed` vfunc — runs once, after construct properties are
// set. `name` is a CONSTRUCT_ONLY property already available on `this`.
constructed() {
console.log('constructed:', getProperty(this, 'name'));
},
},
});
constructType(Greeter, { name: 'greet' }); // logs "constructed: greet"L1 — GJS-shaped surface (@gjsify/node-gi/gi)
The ergonomic layer the gjsify --app node build rewrites gi:// imports onto.
This is the same code you would write under GJS:
import { requireGi } from '@gjsify/node-gi/gi';
const GLib = requireGi('GLib', '2.0');
console.log(GLib.get_host_name());
const Gio = requireGi('Gio', '2.0');
const action = new Gio.SimpleAction({ name: 'greet', enabled: true });
console.log(action.name); // 'greet' (property accessor)
console.log(action.get_name()); // 'greet' (method)
action.enabled = false; // property set → set_property
const c = new Gio.Cancellable();
c.connect('cancelled', () => console.log('cancelled'));
c.cancel(); // fires the signal
// enums, flags and constants (GJS-style UPPER_CASE members)
console.log(GLib.PRIORITY_DEFAULT); // 0
console.log(Gio.BusType.SESSION); // 2
console.log(Gio.ApplicationFlags.HANDLES_OPEN); // 4
// constructor/static methods + camelCase aliases
const file = Gio.File.new_for_path('/usr/bin/gjs');
console.log(file.get_path()); // '/usr/bin/gjs'
console.log(file.getBasename()); // 'gjs' (camelCase alias)
// mainloop: a blocking GLib loop, with Node's libuv kept alive underneath
const loop = GLib.MainLoop.new(null, false);
setTimeout(() => loop.quit(), 100); // a libuv timer that fires during run()
loop.run(); // blocks like under GJS; returns on quit()
// GI callbacks: a JS function passed where a GI callback is expected. The
// GLib source fires from the loop; returning false (G_SOURCE_REMOVE) stops it.
const ticker = GLib.MainLoop.new(null, false);
let n = 0;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
if (++n >= 3) { ticker.quit(); return false; }
return true;
});
ticker.run();The mainloop bridge (startMainLoop) is auto-attached the first time requireGi
loads a namespace, so GLib.MainLoop.run() / Gio.Application.run() block as
they do under GJS while Node's timers, I/O and signal handlers keep running.
GJS ambient globals (@gjsify/node-gi/globals)
GJS source relies on globals that exist implicitly under gjs — print,
printerr, log, logError, ARGV, and the legacy imports object. Importing
@gjsify/node-gi/globals (a side effect) installs Node-backed equivalents that
route through the same backend:
import '@gjsify/node-gi/globals';
print('hello', 1, true); // → stdout, GJS String()-join
const GLib = imports.gi.GLib; // legacy imports.gi (honours .versions)
imports.gi.versions.Gtk = '4.0';
console.log(imports.gettext.gettext('x')); // no-translation passthroughA follow-up --app node build step will inject this automatically for any
bundle that references those globals (so const Gtk = imports.gi.Gtk /
print(...) GJS source runs unmodified on Node); today it is an explicit import.
The remaining GJS-compatible surface (import GLib from 'gi://GLib?version=2.0',
const GLib = imports.gi.GLib, the core overrides, _promisify, the legacy
imports.* modules) is layered on top of this engine in the gjsify bundler
integration and subsequent drops.
