Programming

Using GTK4/libadwaita with Mojo: GUI Options

Can you use GTK (GTK4/libadwaita) for Mojo GUI development? No native support—use C FFI, embed Python PyGObject, or IPC. Guides, examples, pitfalls, and alternatives like mojo-qt.

1 answer 1 view

Can I use GTK (GTK 4 / libadwaita) for GUI development in the Mojo programming language? Specifically:

  • Is GTK supported natively in Mojo, or do I need to call it via C FFI / bindings?
  • Can I call Python’s PyGObject (the gi repository) from Mojo by embedding Python (for example using from python import Python and importing Gtk, Adw, Gio)? If so, how do I set up and link the runtime and dependencies, and are there working examples or best practices?
  • If GTK is not practical, what GUI frameworks or toolkits are currently available or recommended for Mojo?

Context: I tried converting a GTK Python example (using Gtk.Application, Adw.Application, Gtk.ApplicationWindow, Gio.SimpleAction) to Mojo and attempted to run it by executing Python code from Mojo. Any guidance, references, or example code would be very helpful.

Mojo does not include native GTK (GTK4 / libadwaita) bindings — you’ll need to call GTK through C FFI/bindings or run a GTK frontend in a separate process (for example, Python + PyGObject). Mojo can interoperate with CPython (the runtime is used as a dynamic library), so embedding Python is theoretically possible, but calling PyGObject/gi from Mojo requires writing shims, linking the matching libpython and GI libraries, and careful handling of threads and GObject lifetimes; there are no official, ready-made examples. Practically: prefer a small C wrapper that exposes a stable C API or keep the GUI in Python/Rust and talk to your Mojo backend over IPC for fastest, safest results.


Contents


Mojo GTK: native support and short answer

Short answer: No — there is no official, native Mojo binding for GTK4 or libadwaita today. The community and curated lists make the same point: you must either call GTK through a C FFI/binding you write or keep the GUI in another runtime (Python, Rust, C, etc.) and communicate with Mojo via IPC. The community-curated list explicitly notes there are no stable GTK/libadwaita bindings for Mojo and that embedding PyGObject would require embedding a Python interpreter and additional FFI work (not supported out of the box) — see the community notes on GitHub for details: https://github.com/mojicians/awesome-mojo.

Quick answers to your bullets:

  • Is GTK supported natively in Mojo? — No. Use C FFI or an external GUI process.
  • Can you call PyGObject (gi) from Mojo by embedding Python? — In principle yes (Mojo can use CPython as a dynamic library), but not trivially; you’ll need shims, matching libpython, typelibs and careful lifecycle/thread handling. The official docs note Mojo uses CPython as a dynamic library: “You can import existing Python modules and use them in a Mojo program. This is 100% compatible because we use the CPython runtime without modification for full compatibility with existing Python libraries.” See https://docs.modular.com/mojo/manual/python/.
  • Are there working examples / best practices? — No published, complete examples of embedding PyGObject from Mojo were found; community threads recommend C wrappers or running the GUI externally (forum thread: https://forum.modular.com/t/while-making-a-mojo-gui-ui-lessons-learned/1539).

How Mojo interoperates with Python (what the docs say)

Modular’s documentation states that Mojo can interoperate with CPython by using the CPython runtime as a dynamic library. Quote from the docs: “You can import existing Python modules and use them in a Mojo program. This is 100% compatible because we use the CPython runtime without modification for full compatibility with existing Python libraries.” That means Mojo can call into Python code if the runtime and symbols are present on the system, and you can construct Python objects and invoke functions from Mojo (see https://docs.modular.com/mojo/manual/python/).

Reality checks and practical limits:

  • The docs show the capability in principle, but the community warns the higher-level convenience syntax you might hope to use (for example, a magic from python import Python that lets you import gi directly) is not a turnkey feature everywhere. The curated community notes call out that “The from python import Python syntax is not part of the language, so you cannot directly import gi or Gtk from Mojo” without embedding a Python interpreter or FFI glue (https://github.com/mojicians/awesome-mojo).
  • Embedding PyGObject requires system-level pieces: the correct libpython (matching Python version), PyGObject installed for that Python, and the GTK/GI typelibs (gir/*.typelib). Those are system packages on most distros and must be discoverable at runtime.

If you plan to attempt embedding, you’ll need:

  • A matching Python runtime (install python3-dev / python3-devel so you have libpython and headers).
  • PyGObject and the GTK typelibs installed (distribution packages are usually easier than pip for pygobject).
  • To link/load libpython at runtime (LD_LIBRARY_PATH / DYLD_LIBRARY_PATH or proper link flags) so Mojo can find it.
  • Careful handling of the GIL, GMainLoop and the requirement that GTK UI work happen on the main thread.

Why GTK / libadwaita isn’t native in Mojo

Why hasn’t an official GTK binding landed? A few reasons:

  • GObject Introspection is complex: GTK libraries are built around GObject; bindings normally come from the GI introspection layer (PyGObject uses girepository to produce Python bindings). Exposing that from a new language requires a stable FFI surface and a binding generator or a robust manual binding effort.
  • Mojo is still early: community and project discussion point out missing language features and standard-library infrastructure (lifetimes, richer ADTs, expanded interop features) that make large non-trivial bindings harder to maintain right now — see the project discussion about library maturity at https://github.com/modular/modular/discussions/1576.
  • Maintenance cost: GTK and libadwaita evolve; a binding needs effort to keep in sync and to solve ownership/refcounting issues. That’s why community projects exist but remain experimental.

Option A — Call GTK from Mojo via C FFI / bindings

The most pragmatic native route is to write a thin C wrapper around the GTK API you need, compile it to a shared library, and call that library from Mojo via dynamic loading / FFI (the community forum suggests the DLHandle pattern and exposing only simple C functions that use primitive types).

Why a C shim?

  • Hides complex GObject types and memory management behind a stable C API.
  • Lets you keep event loop and widget state in the C side where GTK expects it (main-thread, GObject lifetime rules).
  • Reduces the amount of Mojo-specific FFI you must write.

Minimal C wrapper template (conceptual):

c
// gtk_wrapper.c — compile as shared lib
#include <gtk/gtk.h>

static GtkApplication *app = NULL;

static void activate_cb(GtkApplication *app, gpointer user_data) {
 GtkWidget *win = gtk_application_window_new(GTK_APPLICATION(app));
 gtk_window_set_title(GTK_WINDOW(win), "Mojo GTK4 Window");
 gtk_window_set_default_size(GTK_WINDOW(win), 400, 300);
 gtk_widget_show(win);
}

int gtk_wrapper_init(const char *appid) {
 if (app) return 0;
 app = GTK_APPLICATION(gtk_application_new(appid, G_APPLICATION_FLAGS_NONE));
 g_signal_connect(app, "activate", G_CALLBACK(activate_cb), NULL);
 return 0;
}

int gtk_wrapper_run(int argc, char **argv) {
 if (!app) return -1;
 int status = g_application_run(G_APPLICATION(app), argc, argv);
 g_object_unref(app);
 app = NULL;
 return status;
}

Build command (Linux example):

gcc -fPIC -shared gtk_wrapper.c -o libgtk_wrapper.so $(pkg-config --cflags --libs gtk4)
# If you need libadwaita symbols:
# gcc -fPIC -shared gtk_wrapper.c -o libgtk_wrapper.so $(pkg-config --cflags --libs gtk4 libadwaita-1)

Load and call from Mojo (pseudocode — adapt to your FFI API):

// Pseudocode — adjust to the actual Mojo DLHandle / FFI syntax:
let lib = DLHandle.open("./libgtk_wrapper.so");
let init = lib.symbol("gtk_wrapper_init");
init("com.example.MojoApp"); // pass C string
let run = lib.symbol("gtk_wrapper_run");
run(0, null); // runs the GTK event loop (blocks until exit)

Practical tips:

  • Keep the C API simple (ints, C strings, simple callbacks).
  • Avoid passing GTK pointers into Mojo; keep widget objects behind the C boundary.
  • If you need events routed back to Mojo, consider an IPC channel (Unix socket or pipe) rather than complex function-pointer callbacks across FFI.

Forum threads recording real-world experience and pitfalls for C-FFI with Mojo are useful reading: https://forum.modular.com/t/while-making-a-mojo-gui-ui-lessons-learned/1539.


Option B — Embed Python and call PyGObject (gi) from Mojo

Yes — Mojo can interoperate with CPython. That means embedding PyGObject is theoretically possible, but it’s the hardest option and has several moving parts.

What embedding PyGObject entails

  1. Install the system pieces:
  • Python development headers and libpython (e.g., python3-dev / python3-devel).
  • PyGObject (often python3-gi / python3-gi-cairo on Debian/Ubuntu).
  • GTK4 and libadwaita and their typelibs (gir1.2-gtk-4.0, gir1.2-adw-1 or distro equivalents).
    Example (Ubuntu):
sudo apt install python3-dev python3-gi python3-gi-cairo gir1.2-gtk-4.0 gir1.2-adw-1 libgtk-4-dev libadwaita-1-dev pkg-config build-essential

On Fedora, similar packages are: python3-gobject, gtk4-devel, libadwaita-devel, etc.

  1. Ensure libpython is discoverable to the Mojo process (LD_LIBRARY_PATH / DYLD_LIBRARY_PATH on macOS if needed) or link the shim with the correct python embed flags:
gcc -fPIC -shared python_gtk_shim.c -o libpython_gtk_shim.so $(python3-config --cflags --ldflags) $(pkg-config --cflags --libs gtk4)

(Use --embed on recent python-config if available.)

  1. Create a C shim that uses the Python C API to import gi and run your Python UI script. Example (conceptual):
c
// python_gtk_shim.c
#include <Python.h>

int start_python_gtk() {
 Py_Initialize();
 PyRun_SimpleString(
 "import gi\n"
 "gi.require_version('Gtk', '4.0')\n"
 "from gi.repository import Gtk\n"
 "app = Gtk.Application(application_id='com.example.PyGui')\n"
 "def on_activate(app):\n"
 " win = Gtk.ApplicationWindow(application=app)\n"
 " win.set_default_size(400,300)\n"
 " win.present()\n"
 "app.connect('activate', on_activate)\n"
 "app.run(None)\n"
 );
 Py_Finalize();
 return 0;
}
  1. Export start_python_gtk() from the shared library and call it from Mojo via DLHandle.

Key caveats:

  • PyGObject uses GObject Introspection and loads typelibs; those must match the installed GTK version.
  • The embedded Python must be the same ABI/version as the libpython you link against.
  • GTK expects to run on the main thread; your shim must start the GTK main loop from the process’s main thread. If Mojo’s runtime doesn’t let you do that easily, you’ll run into trouble.
  • Threading and the Python GIL: manage the GIL if you mix calls to Python from different threads.
  • There are no published, tested examples of doing this from Mojo specifically — you’re on the experimental path. Community notes make that clear: embedding PyGObject will require a non-trivial FFI effort and manual lifecycle management (https://github.com/mojicians/awesome-mojo and https://forum.modular.com/t/while-making-a-mojo-gui-ui-lessons-learned/1539).

Recommendation: If you must get a GTK GUI working quickly, prefer running the Python GUI as a separate process and communicating via IPC (see next sections).


A practical recipe: C shim + DLHandle pattern (minimal example)

A pragmatic, relatively low-risk plan that usually works:

  1. Implement the UI and all GTK-specific logic in a small shared library (C or C++), exposing a tiny C API with functions such as init, run, shutdown, and maybe send_event.
  2. Compile the shared library with pkg-config --cflags --libs gtk4 (and libadwaita-1 if you use it).
  3. From Mojo, dynamically open the shared library (DLHandle) and call init then run. Let the shared lib own the GTK main loop.
  4. For communication, prefer a small local IPC channel (Unix domain socket, TCP loopback, gRPC or JSON over stdin/stdout). This avoids complex FFI callback wiring and keeps ownership/threads clear.
  5. If you need two-way calls, the GUI side can send JSON events to Mojo, and Mojo can send commands back.

Why this pattern? It keeps GTK in the environment it understands (C and main thread) and avoids passing opaque GTK pointers into Mojo. It’s easier to debug, and crashes are isolated to the GUI process/library.


Alternatives and recommended GUI toolkits for Mojo

If GTK via FFI/embedding looks like too much work, consider these options (community projects and patterns):

  • Community bindings and demos (check the curated list): https://github.com/mojicians/awesome-mojo — lists mojo-qt (Qt demo), CombustUI (FLTK wrapper), mojo-sdl (SDL2 bindings), and other experimental UI efforts.
  • mojo-qt: demo-level Qt integration — attractive if you want a cross-platform native-feel UI and are willing to depend on Qt.
  • FLTK / SDL wrappers: lighter-weight, easier to bind via simple C APIs.
  • Web-based UI (local web server + browser/embedded webview / Tauri): often fastest to iterate; Mojo backend can serve JSON over HTTP or use websockets.
  • Keep GUI in a language that already has mature GTK bindings (Python / Rust / C) and use IPC to talk to Mojo backend — this often gives the best tradeoff between developer time and robustness.

Practical architecture patterns & workflow recommendation

If you’ve already converted a Python Gtk example and tried to run it from Mojo, pick one of these paths depending on your priorities:

  • Fastest to ship / least risky: Keep the Python GTK code as-is; start it as a separate process from Mojo and communicate via stdin/stdout, a local socket, or gRPC. Use JSON for messages. Pros: minimal FFI, clean separation. Cons: two processes to coordinate.
  • Medium: Create a C shim around your Python GUI (embedding Python inside a shared lib) and load that from Mojo. Pros: single process. Cons: tricky: make sure Python and GTK run on the main thread and link the correct libpython.
  • Native but heavier: Write a C wrapper for GTK and call it from Mojo with DLHandle. Pros: avoids Python and GI; works cross-platform. Cons: more C glue code; you still own the GTK codebase.
  • Experimental: Try a community project like mojo-qt or CombustUI if it meets your needs; be prepared for gaps or to contribute fixes.

If you’re building something production-grade, the IPC approach tends to be the most robust in the short term.


Troubleshooting & common pitfalls

Checklist and commands to debug common failures:

  • Can Python import gi and Gtk on its own?
  • python3 -c "import gi; gi.require_version('Gtk','4.0'); from gi.repository import Gtk; print('ok')"
  • Are typelibs present?
  • Check /usr/lib/girepository-1.0/ for Gtk-4.0.typelib and Adw-1.typelib.
  • Is libpython visible to the process?
  • ldd ./libpython_gtk_shim.so | grep libpython
  • Set LD_LIBRARY_PATH (Linux) or DYLD_LIBRARY_PATH (macOS) if needed.
  • Are you running GTK on the main thread?
  • GTK must be started/created on the process’s main thread; starting it on a background thread will cause undefined behavior.
  • Version mismatches: ensure the Python you embed matches the installed PyGObject and libpython ABI.
  • Crashes on import: often missing typelibs or libpython mismatch. Run the shim directly under gdb/strace to see missing libraries.
  • macOS and Windows: packaging PyGObject / GTK is harder; consider MSYS2 on Windows or Homebrew on macOS; documentation varies.

If you get stuck, produce a minimal reproducible example (tiny C shim + simple call) and share it on the Modular forum — the community often points out platform-specific quirks: https://forum.modular.com/t/while-making-a-mojo-gui-ui-lessons-learned/1539.


Sources


Conclusion

Mojo GTK is not available as an official, native binding right now — you’ll need to use C FFI/bindings or an external GUI process. Embedding PyGObject is technically possible because Mojo can use CPython as a dynamic library, but it’s complex and unsupported out of the box: you’ll need to provide a C or embedding shim, match libpython and GI typelibs, and handle thread/GObject lifecycles carefully. For most projects the fastest, most maintainable path is either a thin C wrapper that owns the GTK main loop or keeping the GUI in Python/Rust and communicating with your Mojo backend over a simple IPC channel. If you want, I can draft a minimal C shim + Mojo DLHandle example tailored to your platform (Linux/macOS/Windows) or an IPC example that adapts your existing Python Gtk sample to a Mojo backend.

Authors
Verified by moderation
Moderation
Using GTK4/libadwaita with Mojo: GUI Options