devlog

gtk.ts

I am writing a set of Node.js bindings for GTK and the GObject ecosystem.

Why?

For fun and learning.

There is ample previous art in this space and I am aware of it. [1] However, I've deliberately avoided to study their internals or base my work off them, in order to (again) focus on the process of learning and discovery. So, yes, this is wheel-maxxing.

There is of course a deeper reason to attempt this. I've dabbled my feet with GTK programming, and while I like the ecosystem, I've always felt a bit of friction while using its many bindings. Writing in pure C is... well, it's C. The Rust bindings are remarkably designed, but the dynamic nature of GObject fits into it like a square peg. I am a JS person at the end of the day, and I believe that a set of strongly typed JS bindings might constitute the most ergonomic and expresive possibility for GTK. [2]

Now, when I took a light glance at the aforementioned previous art, it didn't immediately spark joy.[3] The platonic bindings I would like to see would have several characteristics:

  • They would be be written for Node.js. I don't know the rationale for GJS being based off SpiderMonkey (probably predates the popularity of Node?),[4] but whether you like it or now, the Node/JS ecosystem comes with a vast pool of developers, packages, etc. that calls for an obvious entrypoint into GTK with less friction that a niche JS runtime.

  • They would be TypeScript-first. Not only it feels like a must these days, it greatly enhances the developer experience and helps navigating the complexity of GObject.

  • On a more technical note, they would be written using Node-API. This is the "right" way of writing native addons; "right" as in: with better prospects of future support, most recommended, most deliberately designed, etc. As a bonus point, it automatically gives you compatibility with Deno and Bun, as they support Node-API as well.

  • And another technical detail: they would blend the two event loops (the one coming from the JS runtime and GLib's own MainContext) seamlessly. This is something that is tricky to get right, and is mostly unique to JavaScript, where you most often get an event loop as a side to your language runtime. Fulfilling this constraint while keeping a Node-API-only approach (that does not rely on libuv implementation details) was one the big puzzles that kept me thinking about this whole endeavor.

I'm writing it in Rust[5], although I gave it a fair attempt in C first. Among other things, I was trying to make it easy to package it for a distro, if only as an extra constraint to make it more interesting. Sanity prevailed, I guess. I am however aiming for zero dependencies (again, in the interest of packageability), even if that means rolling my own FFI layer for Node-API and gobject-introspection. This is for fun, remember?

https://codeberg.org/_rvidal/gtk-ts

And now, for the "Hello, world":

 


  1. To wit, GJS, node-gtk, ts-for-gir, plus others. ↩

  2. This is all highly subjective, of course, blub paradox, yadda, yadda, yadda. I imagine that Kotlin might also be a good fit as well. Look, I'm sorry OCaml is not more mainstream, it's not my fault. ↩

  3. These impressions were formd around a year ago, or even before that. I am not up to date on the current state of other projects. ↩

  4. And I certainly don't question it. ↩

  5. Mostly. There is a sizable chunk written in TypeScript itself (the types codegen). ↩