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.
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
girepository) from Mojo by embedding Python (for example usingfrom python import Pythonand importingGtk,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
- Short answer: Mojo GTK support
- How Mojo interoperates with Python (what the docs say)
- Why GTK / libadwaita isn’t native in Mojo
- Option A — Call GTK from Mojo via C FFI / bindings
- Option B — Embed Python and call PyGObject (gi) from Mojo
- A practical recipe: C shim + DLHandle pattern (minimal example)
- Alternatives and recommended GUI toolkits for Mojo
- Practical architecture patterns & workflow recomendation
- Troubleshooting & common pitfalls
- Sources
- Conclusion
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 Pythonthat lets you importgidirectly) is not a turnkey feature everywhere. The curated community notes call out that “Thefrom python import Pythonsyntax is not part of the language, so you cannot directly importgiorGtkfrom 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):
// 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
- Install the system pieces:
- Python development headers and libpython (e.g.,
python3-dev/python3-devel). - PyGObject (often
python3-gi/python3-gi-cairoon Debian/Ubuntu). - GTK4 and libadwaita and their typelibs (
gir1.2-gtk-4.0,gir1.2-adw-1or 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.
- 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.)
- Create a C shim that uses the Python C API to import gi and run your Python UI script. Example (conceptual):
// 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;
}
- 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:
- 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 maybesend_event. - Compile the shared library with
pkg-config --cflags --libs gtk4(andlibadwaita-1if you use it). - From Mojo, dynamically open the shared library (DLHandle) and call
initthenrun. Let the shared lib own the GTK main loop. - 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.
- 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/forGtk-4.0.typelibandAdw-1.typelib. - Is libpython visible to the process?
ldd ./libpython_gtk_shim.so | grep libpython- Set
LD_LIBRARY_PATH(Linux) orDYLD_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
- Modular docs — Python interoperability: https://docs.modular.com/mojo/manual/python/
- Awesome-Mojo (community-curated list): https://github.com/mojicians/awesome-mojo
- Forum thread — “While making a mojo GUI UI, lessons learned”: https://forum.modular.com/t/while-making-a-mojo-gui-ui-lessons-learned/1539
- Project discussion — library maturity and cross-platform GUI notes: https://github.com/modular/modular/discussions/1576
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.