7 min readApr 4, 2025
–
Press enter or click to view image in full size
Getting started with GTK using the Zig programming language
The Zig programming language is my most recent fascination and like with any shiny new hammer, I experience that overwhelming urge to go and find some nails to drive.
Learning GTK definitely seemed like an appropriate nail: GTK is implemented in the C programming language, so one can invoke the API directly from Zig, without the need for any custom language bindings. And additionally, being able to use GTK with Zig, opens the door for some future hobby projects that concern desktop applications.
So like any noob, I started working through the [Getting Started with GTK]…
7 min readApr 4, 2025
–
Press enter or click to view image in full size
Getting started with GTK using the Zig programming language
The Zig programming language is my most recent fascination and like with any shiny new hammer, I experience that overwhelming urge to go and find some nails to drive.
Learning GTK definitely seemed like an appropriate nail: GTK is implemented in the C programming language, so one can invoke the API directly from Zig, without the need for any custom language bindings. And additionally, being able to use GTK with Zig, opens the door for some future hobby projects that concern desktop applications.
So like any noob, I started working through the Getting Started with GTK tutorial; but I used Zig instead of the C programming language. And with a few minor hiccups, I manage to get through the first few exercises in no time at all. However, I got stuck on the A trivial application exercise… it turned out that the trivial application was not so trivial after all.
While I’m certainly not the first person to use Zig with GTK, it does seem that most of the online articles on this topic stop short of the Trivial application exercise. So I thought that teh interwebs could do with yet another article.
Disclaimer
The code snippets in this article were developed for Zig 0.13.0, and may need some customisation to work with newer releases of the Zig programming language. The code is also not meant to be representative of idiomatic Zig, but merely a demonstration of how one could follow the steps of the GTK tutorial using the Zig programming language.
Smooth sailing
As mentioned earlier, the first few steps of the GTK tutorial could be done with little effort. And while I’d expect most people wouldn’t get stuck with these steps, it is perhaps useful to cover some of the basics.
There are a few things one need to know to progress through these early exercises:
- How to link GTK with a Zig application.
- How interoperability between C and Zig works.
- That GTK often uses convenience macros for function calling.
- How to use “gresources” with a Zig application.
Since GTK is licensed under the terms of the GNU Lesser General Public License one would need to do dynamic linking when using this library with non GPL/LGPL works; and for simplicity I just installed GTK with the system package manager. So to link GTK, we just need to add the following two lines in our build file. This will link in both the GTK- and the standard C libraries, and automatically set up the corresponding include paths.
exe.linkSystemLibrary("gtk4");exe.linkLibC();
We can use a cImport block to import the C header files into our Zig code. Generally one would want a single cImport block for the entire project, since it cuts down on the number of times Zig has to invoke clang and avoid duplicates. By convention the cImport block would be assigned to a c constant, and one can call the C functions and structs using the c. prefix.
const c = @cImport({ @cInclude("gtk/gtk.h");});pub fn main() !u8 { const app = c.gtk_application_new("inc.swindlers.gtkz", c.G_APPLICATION_FLAGS_NONE); defer c.g_object_unref(app);}
We can use callconv(.c) to specify when a Zig function should use the C procedure call standard, which is necessary for situations where a C library needs to call functions in our Zig application.
fn printHello(widget: *c.GtkWidget, data: ?*anyopaque) callconv(.C) void { c.g_print("Hello world\n"); _ = widget; _ = data;}
My first hiccup was g_signal_connect that caused a compiler error due to some mismatching parenthesis, which was when I learned that GTK often use convenience macros for function calling. Since macros contain preprocessor directives for a C/C++ compiler, they may present some problems when using it with other programming languages.
So to get ourselves going again, we need to pull up the GTK docs to understand what the macro function does, and which C function we can call instead. While this certainly slowed me down a bit, it also forced me to become more familiar with GTK… remember that thing that the tutorial was trying to teach us.
Of course we can create our own convenience functions in Zig that do the same thing the macro functions did. E.g. Below is a function for connecting to a signal where the arguments would be swapped when the handler function is called. I made this my default, because the first function parameter of a Zig struct function is typically the self argument. And I wanted my handler functions to look like regular struct functions.
pub fn signalConnect( receiver: anytype, sender: anytype, detailed_signal: []const u8, callback_handler: *const fn (@TypeOf(receiver), @TypeOf(sender)) callconv(.C) void,) void { _ = c.g_signal_connect_data( sender, detailed_signal.ptr, @ptrCast(callback_handler), receiver, null, c.G_CONNECT_SWAPPED, );}
The last exercise before the trivial application exercise, introduced GtkBuilder and GResource. To make this work with Zig, we need to do the following:
- Add a system command in our build file to generate C sources from the resource files.
- Add the generated header to our include path.
- Register the resources at application startup.
The not so trivial application
Feeling pumped up with confidence, I pulled up the A trivial application exercise and… Oh WTF? What are G_DEFINE_TYPE and G_DECLARE_FINAL_TYPE? And where are all the declarations and definitions for these structs and functions?
No sweat, let’s just google this… Oh, the top search results don’t have any good articles that cover this topic… I know, let’s ask teh AIs… More WTFs, well at least I’m reassured that my job as a software engineer is still secure.
Ok, so let’s once again pull up the GTK docs. We can see that G_DEFINE_TYPE is a convenience macro for G_DEFINE_TYPE_EXTENDED. And we can see an example of the code that G_DEFINE_TYPE_EXTENDED would generate, but that example is a bit confusing since it declares a type that has a private section.
If we discard the parts that are only relevant to the private sections, then we can see that we only need two variables: the type ID for the new type registration, and the parent class pointer. And we can also see that we will need a couple of functions to register the new type, and to capture the parent class pointer.
As a first attempt, I added those variables and functions to each of my modules… It worked, but all the duplication just doesn’t seem right. Fortunately, we can do this with Zig’s comptime and behold! Below is a convenient function that would generate a new struct for each of our modules.
pub fn defineType( get_parent_type: *const fn () callconv(.C) c.GType, type_name: [*c]const c.gchar, comptime Class: type, // https://docs.gtk.org/gobject/callback.ClassInitFunc.html class_init: *const fn (*Class, c.gpointer) callconv(.C) void, comptime Instance: type, // https://docs.gtk.org/gobject/callback.InstanceInitFunc.html instance_init: *const fn (*Instance, *Class) callconv(.C) void, flags: c.GTypeFlags,) type { return struct { var parent_class: c.gpointer = null; var static_g_define_type_id: c.GType = 0; fn classInternInit(class: *Class, class_data: c.gpointer) callconv(.C) void { parent_class = c.g_type_class_peek_parent(class); class_init(class, class_data); } pub fn getType() callconv(.C) c.GType { if (c.g_once_init_enter_pointer(&static_g_define_type_id) != 0) { const g_define_type_id = c.g_type_register_static_simple( get_parent_type(), c.g_intern_static_string(type_name), @sizeOf(Class), @ptrCast(&classInternInit), @sizeOf(Instance), @ptrCast(instance_init), flags, ); c.g_once_init_leave_pointer( &static_g_define_type_id, @ptrFromInt(g_define_type_id), ); } return static_g_define_type_id; } pub fn getParentClass(comptime ParentClass: type) ?*ParentClass { return @ptrCast(@alignCast(parent_class)); } };}
Next we look at the docs for G_DECLARE_FINAL_TYPE and we can see that it is mostly function and struct declarations. Since our convenience function already contains the signatures for everything we need, we can discard G_DECLARE_FINAL_TYPE and simply create Zig structs for our instance and class types. An for C interoperability we need to declare our structs as extern.
So each one of our modules would now contain three structs: An instance struct, a class struct, and a type struct. Bellow is an extract of my application module.
pub const App = extern struct { parent: c.GtkApplication = undefined, const Self = @This(); pub fn new() *Self { var names = [_][*c]const u8{ "application-id", "flags" }; var values = [names.len]c.GValue{ undefined, undefined }; c.g_value_set_static_string( c.g_value_init(&values[0], c.G_TYPE_STRING), "inc.swindlers.gtkz", ); c.g_value_set_int( c.g_value_init(&values[1], c.G_TYPE_INT), c.G_APPLICATION_HANDLES_OPEN, ); const app = c.g_object_new_with_properties( AppType.getType(), names.len, &names, &values, ); return @ptrCast(@alignCast(app)); } fn init(self: *Self, class: *AppClass) callconv(.C) void { ... }};const AppClass = extern struct { parent_class: c.GtkApplicationClass = undefined, const Self = @This(); fn init(self: *Self, class_data: c.gpointer) callconv(.C) void { ... }};const AppType = gtk.defineType( &c.gtk_application_get_type, "GtkzApp", AppClass, &AppClass.init, App, &App.init, 0,);
So now that we’re cooking with gas again, I continued through the remaining exercises. It is mostly just repeating techniques that I used with earlier exercises: finding the C functions for macro functions, using system commands in the build to generate the schema, and dealing with the odd variadic arguments (which I mostly avoided by finding equivalent API functions without variadic arguments).
For the curious amongst you, below is a mapping of the tutorial exercises to the git diffs in my project code:
- Hello, world (git diff)
- Packing buttons (git diff)
- Drawing in response to input (git diff)
- Packing buttons with GtkBuilder (git diff)
- A trivial application (git diff)
- Opening files (git diff)
- A menu (git diff)
- A preferences dialog (git diff)
- Adding a search bar (git diff)
- Adding a sidebar (git diff)
- Properties (git diff)
While battling through the A trivial application exercise, I found this article on Zero-cost bindings with Zig for GTK. It focuses mostly on the Hello, world exercise, but it is worth checking out for some ideas on how to neatly wrap all the c. expressions.
What next?
For me this is now time to move on to another hobby project. There are still a few of my unfinished hobby projects that could use some of my attention. But to reinforce the learnings from the GTK tutorial, I’ll try and pick up a new desktop application for my next hobby project.
Generally speaking, my fascination with Zig will most likely continue. What I enjoy about this language is how it combines low level systems programming features with modern language features (e.g. memory safety), without needless complexity. And the ease of interoperability with C makes this a great choice for working with existing C projects.