A Gtk4 binding for GNU Modula.

Eric Streit 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
examples 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
lib 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
src 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
tests 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
tools 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
.gitignore 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
Makefile 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前
README.md 0418ccb46e m2-GTK4 v1.0.0: GNU Modula-2 bindings for GTK4 (+ libadwaita) 3 天之前

README.md

m2-GTK4 — GTK4 bindings for GNU Modula-2

Thin DEFINITION MODULE FOR "C" bindings of GTK 4 for gm2 (GCC 16 / GNU Modula-2), plus a small pure-Modula-2 helper layer.

The first slice covers the canonical GTK4 application shape (GtkApplication + GtkWindow + GtkLabel / GtkButton / GtkBox), a working "activate" / "clicked" signal path, and the GLib/GObject runtime underneath it. The second slice adds the common input widgets: GtkEntry/GtkEditable, GtkCheckButton, GtkSwitch, GtkScale (GtkRange), plus a shared GtkEnums module. The third slice adds the layout containers: GtkGrid, GtkStack + GtkStackSwitcher, GtkNotebook, GtkScrolledWindow. The fourth slice adds actions, menus and accelerators: GIO's GAction / GActionMap / GSimpleAction, GMenu / GMenuModel / GVariant, plus GtkApplicationWindow and GtkPopoverMenuBar. The fifth slice switches the project to the ISO dialect (-fiso) and adds per-connection signal closures (GtkClosures) and window chrome (GtkHeaderBar, GtkMenuButton, gtk_window_set_titlebar). The sixth slice adds more inputs: GtkSpinButton, GtkSearchEntry, GtkDropDown (with GtkStringList / GListModel) and GtkTextView + GtkTextBuffer. The seventh slice adds more containers: GtkFrame, GtkExpander, GtkPaned, GtkOverlay, GtkListBox (+ GtkListBoxRow). The eighth slice adds GIO extras: GListStore (+ GtkStringObject), GFile, GtkFileDialog and GSettings (+ GSettingsSchema). The ninth slice adds model-backed views: GtkListView, GtkGridView, GtkColumnView (+ GtkColumnViewColumn / GtkColumnViewCell), the selection models GtkSingleSelection / GtkNoSelection, and the GtkSignalListItemFactory / GtkListItem pair. A tools/gir2def.py generator additionally emits broad .def coverage from GObject-Introspection .gir XML. A tenth slice adds libadwaita (AdwApplication, header/toolbar, preferences rows, toast, about/message dialogs) and GError handling plus an ownership helper layer.

Verified with gm2 16.0.1 (experimental) and GTK 4.18.6 on Linux.

Status

Module C header Contents
GLib <glib.h> base C types, GMainLoop, timeouts/idle sources, memory, strings, clock
GObject <glib-object.h> ref/unref, GType, g_object_new, g_signal_connect_data, handler disconnect, properties
Gio <gio/gapplication.h> GApplication flags, run/quit/hold/release
Gtk <gtk/gtk.h> gtk_init, library version helpers
GtkApplication <gtk/gtkapplication.h> create/add/remove windows
GtkWidget <gtk/gtkwidget.h> visibility, sensitivity, margins, expand, align, size
GtkWindow <gtk/gtkwindow.h> create, title, size, child, present/close/destroy
GtkBox <gtk/gtkbox.h> orientation, append/prepend/remove, spacing
GtkLabel <gtk/gtklabel.h> text, markup
GtkButton <gtk/gtkbutton.h> label buttons
GtkEnums <gtk/gtkenums.h> orientation, alignment, position constants
GtkEditable <gtk/gtkeditable.h> text get/set, editable, alignment, selection
GtkEntry <gtk/gtkentry.h> placeholder, max length, visibility, progress
GtkCheckButton <gtk/gtkcheckbutton.h> active, label, inconsistent
GtkSwitch <gtk/gtkswitch.h> active / state
GtkRange <gtk/gtkrange.h> numeric value (base of GtkScale)
GtkScale <gtk/gtkscale.h> slider, digits, value position
GtkGrid <gtk/gtkgrid.h> cell attach, spacing, homogeneous
GtkStack <gtk/gtkstack.h> named/titled pages, visible child, transitions
GtkStackSwitcher <gtk/gtkstackswitcher.h> button row that drives a stack
GtkNotebook <gtk/gtknotebook.h> tabbed pages
GtkScrolledWindow <gtk/gtkscrolledwindow.h> scrollable viewport, policy
GtkApplicationWindow <gtk/gtkapplicationwindow.h> app window, window-scoped actions, show-menubar
GtkPopoverMenuBar <gtk/gtkpopovermenubar.h> in-window menu bar from a model
GtkHeaderBar <gtk/gtkheaderbar.h> title widget, pack start/end, title buttons, decoration layout
GtkMenuButton <gtk/gtkmenubutton.h> button that pops up a menu model
GAction <gio/gaction.h> activate, name, enabled, state
GActionMap <gio/gactionmap.h> add / lookup / remove named actions
GSimpleAction <gio/gsimpleaction.h> parameterless and stateful actions
GMenu <gio/gmenu.h> GMenu + GMenuItem builder
GMenuModel <gio/gmenumodel.h> model item count
GVariant <glib/gvariant.h> boolean / string / int32 values
GtkSpinButton <gtk/gtkspinbutton.h> numeric entry, range/increments/digits/wrap
GtkSearchEntry <gtk/gtksearchentry.h> search-styled entry, placeholder
GListModel <gio/glistmodel.h> indexed model interface
GtkStringList <gtk/gtkstringlist.h> simple string model
GtkDropDown <gtk/gtkdropdown.h> choose one item from a model
GtkTextIter <gtk/gtktextiter.h> opaque 80-byte iterator (by address)
GtkTextBuffer <gtk/gtktextbuffer.h> text model for a text view
GtkTextView <gtk/gtktextview.h> multi-line text widget
GtkFrame <gtk/gtkframe.h> labelled border around one child
GtkExpander <gtk/gtkexpander.h> collapsible section
GtkPaned <gtk/gtkpaned.h> two panes with a draggable divider
GtkOverlay <gtk/gtkoverlay.h> stack overlays on a main child
GtkListBox <gtk/gtklistbox.h> vertical row list + GtkListBoxRow
GListStore <gio/gliststore.h> in-memory list model
GtkStringObject <gtk/gtkstringobject.h> GObject string wrapper (item type)
GtkSignalListItemFactory <gtk/gtksignallistitemfactory.h> setup/bind/teardown item factory
GtkListItem <gtk/gtklistitem.h> per-row object passed to a factory
GtkSelectionModel <gtk/gtkselectionmodel.h> selection interface for a view
GtkSingleSelection <gtk/gtksingleselection.h> zero/one selected item
GtkNoSelection <gtk/gtknoselection.h> display-only (no selection)
GtkListView <gtk/gtklistview.h> model-driven list
GtkGridView <gtk/gtkgridview.h> model-driven wrapping grid
GtkColumnView <gtk/gtkcolumnview.h> model-driven multi-column table
GtkColumnViewColumn <gtk/gtkcolumnviewcolumn.h> one column
GtkColumnViewCell <gtk/gtkcolumnviewcell.h> column cell (GtkListItem subclass)
GFile <gio/gfile.h> path/URI file handle
GtkFileDialog <gtk/gtkfiledialog.h> async native open/save/folder dialog
GSettings <gio/gsettings.h> typed schema keys
GSettingsSchema <gio/gsettingsschema.h> schema lookup / key check
GError <glib/gerror.h> error value (message/domain/code)
GioError <gio/gioerror.h> GIO error domain and codes
AdwApplication <libadwaita-1/adw-application.h> Adwaita application
AdwApplicationWindow <libadwaita-1/adw-application-window.h> Adwaita window
AdwHeaderBar <libadwaita-1/adw-header-bar.h> header bar
AdwToolbarView <libadwaita-1/adw-toolbar-view.h> top/bottom bars + content
AdwPreferencesPage <libadwaita-1/adw-preferences-page.h> page of groups
AdwPreferencesGroup <libadwaita-1/adw-preferences-group.h> titled group of rows
AdwPreferencesRow <libadwaita-1/adw-preferences-row.h> base row (title)
AdwActionRow <libadwaita-1/adw-action-row.h> row with subtitle/prefix/suffix
AdwSwitchRow <libadwaita-1/adw-switch-row.h> row with a switch
AdwEntryRow <libadwaita-1/adw-entry-row.h> row with an entry
AdwComboRow <libadwaita-1/adw-combo-row.h> row with a drop-down
AdwExpanderRow <libadwaita-1/adw-expander-row.h> row that expands
AdwStatusPage <libadwaita-1/adw-status-page.h> placeholder page
AdwToast <libadwaita-1/adw-toast.h> transient notification
AdwToastOverlay <libadwaita-1/adw-toast-overlay.h> toast host
AdwBanner <libadwaita-1/adw-banner.h> dismissible info bar
AdwClamp <libadwaita-1/adw-clamp.h> max-size centred container
AdwAboutWindow <libadwaita-1/adw-about-window.h> about dialog
AdwMessageDialog <libadwaita-1/adw-message-dialog.h> message / confirm dialog
GtkUtils (lib/) — CStrToM2, StrAppend, IntToStr, Connect, SetAccel, Unref, Free, ErrorMessage, ClearError
GtkClosures (lib/) — per-connection signal contexts (Connect2/Connect3, owned variants)

Layout

m2-GTK4/
  src/        FOR "C" .def bindings (one per GTK/GIO object)
  lib/        pure-M2 helpers built on the bindings (GtkUtils, GtkClosures)
  tests/      gm2 smoke tests + run_tests.sh (+ a private GSettings schema)
  examples/   runnable GTK4 programs (hello, counter, ...)
  tools/      gir2def.py (GIR -> .def generator) + run_gir_check.sh
  gen/        generator output (git-ignored)
  build/      generated binaries (git-ignored)

Prerequisites

  • gm2 (tested: 16.0.1 experimental, ~/bin/Modula2/Gm2/bin/gm2)
  • GTK4 dev files (pkg-config --cflags --libs gtk4, tested: 4.18.6)
  • libadwaita dev files (pkg-config --cflags --libs libadwaita-1, tested: 1.7.6) — optional; enables the Adw* modules, test_adw and the adw example. The build detects them automatically.

The build uses the ISO dialect (-fiso). That is required for SYSTEM.CAST and for NEW/DISPOSE (which need ALLOCATE/DEALLOCATE from the ISO Storage module), both used by GtkClosures. make passes -fiso by default.

export PATH=$HOME/bin/Modula2/Gm2/bin:$PATH

Build & test

make            # tests + examples -> build/
./tests/run_tests.sh
./build/examples/hello
./build/examples/counter
./build/examples/inputs
./build/examples/containers
./build/examples/actions
./build/examples/headerbar
./build/examples/editor
./build/examples/layouts
./build/examples/files
./build/examples/views
./build/examples/adw

make check is make tests + the runner script. The GUI tests are skipped automatically when neither DISPLAY nor WAYLAND_DISPLAY is set. The runner also compiles tests/schemas/*.gschema.xml into build/schemas and points GSETTINGS_SCHEMA_DIR at it, so test_gio can exercise GSettings without touching real user settings.

Generator (broad coverage)

The hand-written modules in src/ are the idiomatic API. For breadth, tools/gir2def.py generates FOR "C" .def files directly from GObject-Introspection .gir XML:

make gir                # Gtk-4.0 -> gen/   (301 modules, ~3.7k procedures)
make gir GIR=Gio-2.0    # any namespace
make gir-check          # generate + compile/link a sample

It emits one module per class/interface/record (named <Namespace><Name>, e.g. GtkButton), namespace functions in <Namespace>Functions.def, and enums/bitfields in <Namespace>Enums.def. Procedure names are the exact GIR c:identifier, so the output links straight against the C library. All 633 generated modules across Gtk/GLib/Gio/Adw parse under gm2 -fiso.

make gir-verify closes the loop by checking that every generated identifier actually exists as an exported symbol in the installed library (via nm); it reports 0 unexpected across Gtk, GLib, Gio and Adw (a short allowlist covers macros/deprecated no-ops).

See tools/README.md for the type mapping and the known limits (value structs are opaque, signals/properties are not generated, variadics and GError** are dropped). Output goes to gen/ and is meant to be cherry-picked, not to replace src/.

Usage

MODULE hello ;

FROM Gio IMPORT g_application_run;
FROM GtkApplication IMPORT gtk_application_new;
FROM GtkWindow IMPORT gtk_application_window_new, gtk_window_set_title,
   gtk_window_set_default_size, gtk_window_set_child, gtk_window_present;
FROM GtkLabel IMPORT gtk_label_new;
FROM GtkUtils IMPORT Connect;
FROM SYSTEM IMPORT ADDRESS, ADR;

PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
VAR window, label: ADDRESS;
BEGIN
   window := gtk_application_window_new(application);
   gtk_window_set_title(window, "Hello");
   gtk_window_set_default_size(window, 360, 120);
   label := gtk_label_new("Hello, GNU Modula-2 + GTK4!");
   gtk_window_set_child(window, label);
   gtk_window_present(window)
END OnActivate;

VAR app: ADDRESS;
BEGIN
   app := gtk_application_new("org.example.Hello", 0);
   Connect(app, "activate", ADR(OnActivate), NIL);
   g_application_run(app, 0, NIL)
END hello.

Compile a program against the bindings:

gm2 -fiso -Isrc -Ilib myprog.mod build/objs/GtkUtils.o \
    build/objs/GtkClosures.o -o myprog \
    $(pkg-config --cflags --libs gtk4)

How it was built, step by step

Step 0 — feasibility spike. Before writing anything, the riskiest interop points were proved against the real library: creating a GTK4 window from Modula-2, running the GLib main loop, and invoking a Modula-2 procedure from C as a GSourceFunc. This fixed the design.

Step 1 — discover the GNU Modula-2 ↔ C rules. See the next section. These rules decide every line of a binding .def.

Step 2 — GLib and GObject. Everything else sits on the base types, the main loop and the signal machinery, so these came first.

Step 3 — application and window. GtkApplication + GApplication drive the loop; GtkWindow gives something to show. Using gtk_application_window_new(app) (not gtk_window_new()) is what keeps the application alive while the window is open.

Step 4 — widgets and layout. GtkBox, GtkLabel, GtkButton, and the per-widget GtkWidget layout helpers.

Step 5 — helpers and signal ergonomics. GtkUtils.Connect wraps the g_signal_connect macro (which has no C symbol) by calling the real g_signal_connect_data; CStrToM2 and IntToStr handle the string/number conversions GTK code always needs.

Step 6 — tests and examples. A headless GLib loop test, a version/link test, a widget + signal round-trip test, and an application test; plus hello and counter examples.

Step 7 — input widgets. GtkEnums was extracted as the shared home for enum constants, then GtkEditable/GtkEntry, GtkCheckButton, GtkSwitch, GtkRange/GtkScale were added, with a test_inputs round-trip test and the inputs example. This step also pinned down the real-number mapping (REAL = C double).

Step 8 — layout containers. GtkGrid, GtkStack + GtkStackSwitcher, GtkNotebook and GtkScrolledWindow, with a test_containers test and the containers example (a notebook whose pages are a grid, a switcher-driven stack, and a scrolled list).

Step 9 — actions, menus, accelerators. GIO's action/menu stack (GAction, GActionMap, GSimpleAction, GMenu, GMenuModel, GVariant), GtkApplicationWindow, and GtkPopoverMenuBar, wired together in the actions example (menu bar + app/win actions + Ctrl+N/Q/B accelerators via GtkUtils.SetAccel) and checked by test_actions.

Step 10 — ISO dialect and per-connection closures. The project was switched to -fiso, which provides SYSTEM.CAST and intrinsic NEW/DISPOSE (with Storage's ALLOCATE/DEALLOCATE). That made the GtkClosures module possible: each signal connection gets its own heap context holding the handler and payload, a trampoline forwards the call, and a GClosure destroy notify frees the context - and optionally the payload - on disconnect or finalize. Covered by test_closure.

Step 11 — header bar and window controls. GtkHeaderBar, GtkMenuButton, and gtk_window_set_titlebar() were added, then combined in the headerbar example (title widget, a connected "New" button packed start, a menu button packed end) and checked by test_headerbar.

Step 12 — more inputs. GtkSpinButton, GtkSearchEntry, GtkStringList + GListModel, GtkDropDown, and GtkTextView + GtkTextBuffer with an opaque 80-byte GtkTextIter marshalled by address. Checked by test_inputs2 and shown in the editor example (search entry, text view, wrap-mode drop-down, padding spin button).

Step 13 — more containers. GtkFrame, GtkExpander, GtkPaned, GtkOverlay, and GtkListBox + GtkListBoxRow (with GtkSelectionMode in GtkEnums). Checked by test_containers2 and shown in the layouts example (a paned split, frame/expander on the left, overlay + list box on the right, row-activated via GtkClosures.Connect3).

Step 14 — GIO extras. GListStore + GtkStringObject (a concrete list model), GFile, the asynchronous GtkFileDialog, and GSettings

  • GSettingsSchema. Checked by test_gio (headless) and test_filedialog; shown in the files example (button -> native open dialog -> open_finish -> GFile basename). The GSettings test uses a private schema compiled by the runner.

Step 15 — model-backed views. GtkSignalListItemFactory + GtkListItem, the selection models GtkSingleSelection / GtkNoSelection, and the views GtkListView, GtkGridView, GtkColumnView (+ GtkColumnViewColumn, GtkColumnViewCell). Checked by test_views, which presents a window and asserts (from the main loop) that the factory "setup"/"bind" callbacks ran, and shown in the views example (one store feeding list, grid and column views).

Step 16 — generator verification. tools/gir_verify.py collects every c:identifier that gir2def.py would emit and checks it against the exported symbols of the libraries pkg-config links for the namespace (symbol-version suffixes stripped). make gir-verify reports 0 unexpected across Gtk/GLib/Gio/Adw, with a small allowlist for GIR-listed macros and per-module ABI functions. tools/run_gir_check.sh also bulk-parses every generated module.

Step 17 — libadwaita. Curated Adw* modules (application, window, header bar, toolbar view, preferences page/group and the action/switch/ entry/combo/expander rows, status page, toast/overlay, banner, clamp, about and message dialogs), checked by test_adw and shown in the adw example. libadwaita is optional in the build.

Step 18 — GError and ownership. GError (with its public struct layout) and GioError (domain + codes), plus GtkUtils.Unref/Free/ ErrorMessage/ClearError and a documented ownership discipline. Checked by test_gerror (a real GIO_ERROR_NOT_FOUND from g_file_delete).

GNU Modula-2 interop rules learned

  1. Declare the module DEFINITION MODULE FOR "C" Name ; and list every export in EXPORT UNQUALIFIED. Without EXPORT UNQUALIFIED the compiler prefixes symbols with Module_, and the link fails.
  2. Formal parameters in a procedure type must be unnamed: GSourceFunc = PROCEDURE (ADDRESS) : INTEGER; (named parameters are a syntax error in gm2's default dialect).
  3. A function result that callers may ignore is declared : [ T ] (e.g. PROCEDURE g_timeout_add (...) : [ guint ];). Without the brackets, ignoring the result is a hard error. This also works in ordinary modules — GtkUtils.Connect uses it.
  4. ARRAY OF CHAR maps onto char *, so string literals and CHAR arrays can be passed straight to const char * parameters.
  5. VAR record/string parameters map onto T *; opaque handles (GObject *, GtkWidget *, ...) travel as ADDRESS (use NIL to test for NULL).
  6. BOOLEAN is not gboolean: pass INTEGER with GLib.gTrue / GLib.gFalse, or use INTEGER directly.
  7. A top-level Modula-2 procedure is a plain C function pointer; pass it directly to a callback-typed parameter, or use ADR(proc) where the binding takes an ADDRESS/GCallback.
  8. Real numbers: gm2 REAL is 8 bytes and matches C double; gm2 SHORTREAL is 4 bytes and matches C float; gm2 LONGREAL is 16 bytes and matches nothing in GTK. Use REAL for gdouble / double (e.g. GtkRange values, GtkScale ranges).
  9. Beware *) and (* inside comments — they close/open a comment early. Write void ptr, not void *, in prose.
  10. gm2 has no pointer arithmetic and its T(x) cast requires equal sizes; CStrToM2 therefore copies with libc strncpy instead of walking a char *.
  11. GVariant values from g_variant_new_* are floating: ownership transfers to the callee (e.g. g_simple_action_new_stateful). Values returned by g_action_get_state() / g_variant_get_* own a reference and must be released with g_variant_unref().
  12. ISO dialect. Compiled with -fiso: CAST(T, expr) (ISO SYSTEM) and NEW/DISPOSE (with Storage's ALLOCATE/DEALLOCATE) are available there but not in PIM.
  13. When a parameter is typed as a procedure type, pass the procedure directly (Connect2(b, "clicked", OnClick, ...)), not ADR(OnClick) — gm2 type-checks it. Use ADR(proc) only where the parameter is a plain ADDRESS / GCallback.
  14. A large opaque C struct that is always passed by address (e.g. GtkTextIter) can be modelled as a record of ADDRESS words sized to sizeof it: that gives the right size and 64-bit alignment. GtkTextIter is 80 bytes, hence ARRAY [0..9] OF ADDRESS.

Roadmap

The original roadmap is complete: GLib/GObject core, widgets, inputs, containers, actions/menus/accelerators, header bars, GIO extras, model-backed views, and the GIR generator are all in. On top of that: generator verification (make gir-verify), curated libadwaita, and GError + ownership helpers.

Possible next steps:

  1. Hand-curate more of the generated surface (signals, properties, value-struct records, GtkCssProvider styling).
  2. Turn the generator's ADDRESS placeholders into cross-module type aliases and [ ]/VAR refinements using the GIR annotations.
  3. Grow the test suite around the newly generated modules.
  4. Prune/promote useful gen/ output into src/ as it proves out.