Zig is a new-ish Programming Language that works well with C. And it comes with built-in Safety Checks at runtime.
PinePhone is an Arm64 Linux Phone. PinePhone Apps are typically coded in C with GUI Toolkits like GTK.
Can we use Zig to code PinePhone Apps? Maybe make them a little simpler and safer?
Let's find out! Our Zig App shall call the zgt GUI Library, which works with Linux (GTK), Windows and WebAssembly...
zgt is in Active Development, some features may change.
(Please support the zgt project! 🙏)
Join me as we dive into our Zig App for PinePhone...
Inside The App
Let's create a PinePhone App (pic above) that has 3 Widgets (UI Controls)...
Save Button
Run Button
Editable Text Box
In a while we'll learn to do this in Zig: src/main.zig
What if we're not familiar with Zig?
The following sections assume that we're familiar with C.
The parts that look Zig-ish shall be explained with examples in C.
(If we're keen to learn Zig, see this)
Import Libraries
We begin by importing the zgt GUI Library and Zig Standard Library into our app: main.zig
// Import the zgt library and Zig Standard Library
const zgt = @import("zgt");
const std = @import("std");
The Zig Standard Library has all kinds of Algos, Data Structures and Definitions.
(More about the Zig Standard Library)
Main Function
Next we define the Main Function for our app: main.zig
/// Main Function for our app
pub fn main() !void {
"!void
" is the Return Type for our Main Function...
-
Our Main Function doesn't return any value
(Hence "
void
") -
But our function might return an Error
(Hence the "
!
")
Then we initialise the zgt Library and fetch the Window for our app...
// Init the zgt library
try zgt.backend.init();
// Fetch the Window
var window = try zgt.Window.init();
Why the "try
"?
Remember that our Main Function can return an Error.
When "try
" detects an Error in the Called Function (like "zgt.backend.init"), it stops the Main Function and returns the Error to the caller.
Let's fill in the Window for our app...
Set the Widgets
Now we populate the Widgets (UI Controls) into our Window: main.zig
// Set the Window Contents
try window.set(
// One Column of Widgets
zgt.Column(.{}, .{
// Top Row of Widgets
zgt.Row(.{}, .{
// Save Button
zgt.Button(.{
.label = "Save",
.onclick = buttonClicked
}),
// Run Button
zgt.Button(.{
.label = "Run",
.onclick = buttonClicked
}),
}), // End of Row
This code creates a Row of Widgets: Save Button and Run Button.
(We'll talk about buttonClicked in a while)
Next we add an Editable Text Area that will fill up the rest of the Column...
// Expanded means the widget will take all
// the space it can in the parent container (Column)
zgt.Expanded(
// Editable Text Area
zgt.TextArea(.{
.text = "Hello World!\n\nThis is a Zig GUI App...\n\nBuilt for PinePhone...\n\nWith zgt Library!"
})
) // End of Expanded
}) // End of Column
); // End of Window
What's .{ ... }
?
.{ ... }
creates a Struct that matches the Struct Type expected by the Called Function (like "zgt.Button").
Thus this code...
// Button with Anonymous Struct
zgt.Button(.{
.label = "Save",
.onclick = buttonClicked
}),
Is actually the short form of...
// Button with "zgt.Button_Impl.Config"
zgt.Button(zgt.Button_Impl.Config {
.label = "Save",
.onclick = buttonClicked
}),
Because the function zgt.Button expects a Struct of type zgt.Button_Impl.Config.
(Anonymous Struct is the proper name for .{ ... }
)
Show the Window
We set the Window Size and show the Window: main.zig
// Resize the Window (might not be correct for PinePhone)
window.resize(800, 600);
// Show the Window
window.show();
Finally we start the Event Loop that will handle Touch Events...
// Run the Event Loop to handle Touch Events
zgt.runEventLoop();
} // End of Main Function
We're done with our Main Function!
Let's talk about the Event Handling for our Buttons...
Handle the Buttons
Let's print a message when the Buttons are clicked: main.zig
/// This function is called when the Buttons are clicked
fn buttonClicked(button: *zgt.Button_Impl) !void {
// Print the Button Label to console
std.log.info(
"You clicked button with text {s}",
.{ button.getLabel() }
);
}
(*zgt.Button_Impl
means "pointer to a Button_Impl Struct")
What's std.log.info?
That's the Zig equivalent of printf for Formatted Printing.
In the Format String, "{s}
" is similar to "%s
" in C.
(Though we write "{}
" for printing numbers)
(More about Format Specifiers)
How is buttonClicked called?
Earlier we did this: main.zig
// Save Button
zgt.Button(.{
.label = "Save",
.onclick = buttonClicked
}),
// Run Button
zgt.Button(.{
.label = "Run",
.onclick = buttonClicked
}),
This tells the zgt Library to call buttonClicked when the Save and Run Buttons are clicked.
And that's the complete code for our PinePhone App!
(For comparison, here's a GTK app coded in C)
(Our PinePhone App is based on this zgt demo)
Install Zig Compiler
Let's get ready to build our PinePhone App.
On PinePhone, download the latest Zig Compiler zig-linux-aarch64 from the Zig Compiler Downloads, like so...
## Download the Zig Compiler
curl -O -L https://ziglang.org/builds/zig-linux-aarch64-0.10.0-dev.2674+d980c6a38.tar.xz
## Extract the Zig Compiler
tar xf zig-linux-aarch64-0.10.0-dev.2674+d980c6a38.tar.xz
## Add to PATH. TODO: Also add this line to ~/.bashrc
export PATH="$HOME/zig-linux-aarch64-0.10.0-dev.2674+d980c6a38:$PATH"
## Test the Zig Compiler, should show "0.10.0-dev.2674+d980c6a38"
zig version
It's OK to use SSH to run the above commands remotely on PinePhone.
Or we may use VSCode Remote to run commands and edit source files on PinePhone. (See this)
Will Zig Compiler run on any PinePhone?
I tested the Zig Compiler with Manjaro Phosh on PinePhone (pic above).
But it will probably work on any PinePhone distro since the Zig Compiler is a self-contained Arm64 Linux Binary.
(Zig Compiler works with Mobian on PinePhone too)
Install Zigmod
Download the latest Zigmod Package Manager zigmod-aarch64-linux from the Zigmod Releases, like so...
## Download Zigmod Package Manager
curl -O -L https://github.com/nektro/zigmod/releases/download/r80/zigmod-aarch64-linux
## Make it executable
chmod +x zigmod-aarch64-linux
## Move it to the Zig Compiler directory, rename as zigmod
mv zigmod-aarch64-linux zig-linux-aarch64-0.10.0-dev.2674+d980c6a38/zigmod
## Test Zigmod, should show "zigmod r80 linux aarch64 musl"
zigmod
We'll run Zigmod in the next step to install the dependencies for zgt Library.
Build The App
To build our Zig App on PinePhone...
## Download the Source Code
git clone --recursive https://github.com/lupyuen/zig-pinephone-gui
cd zig-pinephone-gui
## Install the dependencies for zgt library
pushd libs/zgt
zigmod fetch
popd
## Build the app
zig build
If the build fails, check that the "gtk+-3.0" library is installed on PinePhone. (Here's why)
(Our app builds OK on Mobian after installing "gtk+-3.0")
Run The App
To run our Zig App on PinePhone, enter this...
zig-out/bin/zig-pinephone-gui
We should see the screen below.
When we tap the Run and Save Buttons, we should see...
info: You clicked button with text Run
info: You clicked button with text Save
Yep we have successfully built a Zig App for PinePhone with zgt! 🎉
Is the app fast and responsive on PinePhone?
Yes our Zig App feels as fast and responsive as a GTK app coded in C.
That's because Zig is a compiled language, and our compiled app calls the GTK Library directly.
Zig Outcomes
Have we gained anything by coding our app in Zig?
If we compare our Zig App (pic above) with a typical GTK App in C...
Our Zig App looks cleaner and less cluttered, with minimal repetition.
(Hopefully our Zig App is also easier to extend and maintain)
What about Runtime Safety?
Unlike C, Zig automatically does Safety Checks on our app at runtime: Underflow, Overflow, Array Out-of-Bounds, ...
Here's another: Remember that we used "try
" to handle Runtime Errors?
// Init the zgt library
try zgt.backend.init();
// Fetch the Window
var window = try zgt.Window.init();
Zig Compiler stops us if we forget to handle the errors with "try
".
What happens when our Zig App hits a Runtime Error?
Zig shows a helpful Stack Trace...
$ zig-out/bin/zig-pinephone-gui
Unable to init server: Could not connect: Connection refused
error: InitializationError
zig-pinephone-gui/libs/zgt/src/backends/gtk/backend.zig:25:13: 0x21726e in .zgt.backends.gtk.backend.init (zig-pinephone-gui)
return BackendError.InitializationError;
^
zig-pinephone-gui/src/main.zig:9:5: 0x216b37 in main (zig-pinephone-gui)
try zgt.backend.init();
^
Compare that with a GTK App coded in C...
$ ./a.out
Unable to init server: Could not connect: Connection refused
(a.out:19579): Gtk-WARNING **: 19:17:31.039: cannot open display:
What about bad pointers?
Zig doesn't validate pointers (like with a Borrow Checker), but it tries to be helpful when it encounters bad pointers...
Anything else we should know about Zig?
Zig Compiler will happily import C Header Files and make them callable from Zig. (Without creating any wrappers)
That's why the zgt GUI Library works so well across multiple GUI platforms: GTK, Win32 AND WebAssembly...
Instead of Makefiles, Zig has a Build System (essentially a tiny custom Zig program) that automates the build steps...
Pinebook Pro
Will the Zig GUI App run on Arm64 laptops like Pinebook Pro?
Yep! The same steps above will work on Pinebook Pro.
Here's our Zig GUI App running with Manjaro Xfce on Pinebook Pro...
What's Next
I hope this article has inspired you to create PinePhone apps in Zig!
Check out the Sample Apps for zgt...
zgt Widgets are explained in the zgt Wiki...
Tips for learning Zig...
Zig works great on Microcontrollers too! Here's what I did...
Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn't have been possible without your support.
Got a question, comment or suggestion? Create an Issue or submit a Pull Request here...
lupyuen.github.io/src/pinephone.md
Notes
This article is the expanded version of this Twitter Thread
-
Zig works for complex PinePhone Apps too...
Appendix: Learning Zig
How do we learn Zig?
As of June 2022, Zig hasn't reached version 1.0 so the docs are a little spotty. This is probably the best tutorial for Zig...
After that the Zig Language Reference will be easier to understand...
We need to refer to the Zig Standard Library as well...
Check out the insightful articles at Zig News...
And join the Zig Community on Reddit...
The Gamedev Guide has some helpful articles on Zig...
Appendix: VSCode Remote
For convenience, we may use VSCode Remote to edit source files and run commands remotely on PinePhone (pic above)...
Just connect VSCode to PinePhone via SSH, as described here...
In the Remote Session, remember to install the Zig Extension for VSCode...
Appendix: Zig Handles Bad Pointers
How does Zig handle bad pointers?
Zig doesn't validate pointers (like with a Borrow Checker) so it isn't Memory Safe (yet)...
But it tries to be helpful when it encounters bad pointers. Let's do an experiment...
Remember this function from earlier?
/// This function is called when the Buttons are clicked
fn buttonClicked(button: *zgt.Button_Impl) !void {
// Print the Button Label to console
std.log.info(
"You clicked button with text {s}",
.{ button.getLabel() }
);
}
The above code is potentially unsafe because it dereferences a pointer to a Button...
// `button` is a pointer to a Button Struct
button.getLabel()
Let's hack it by passing a Null Pointer...
// Create a Null Pointer
const bad_ptr = @intToPtr(
*zgt.Button_Impl, // Pointer Type
0 // Address
);
// Pass the Null Pointer to the function
try buttonClicked(bad_ptr);
Note that @intToPtr is an Unsafe Builtin Function, we shouldn't call it in normal programs.
When we compile the code above, Zig Compiler helpfully stops us from creating a Null Pointer...
$ zig build
./src/main.zig:8:21: error: pointer type '*.zgt.button.Button_Impl' does not allow address zero
const bad_ptr = @intToPtr(*zgt.Button_Impl, 0);
Nice! Let's circumvent the best intentions of Zig Compiler and create another Bad Pointer...
// Create a Bad Pointer
const bad_ptr = @intToPtr(
*zgt.Button_Impl, // Pointer Type
0xdeadbee0 // Address
);
// Pass the Bad Pointer to the function
try buttonClicked(bad_ptr);
Zig Compiler no longer stops us. (Remember: @intToPtr is supposed to be unsafe anyway)
When we run it, we get a helpful Stack Trace...
$ zig-out/bin/zig-pinephone-gui
Segmentation fault at address 0xdeadbee8
zig-pinephone-gui/libs/zgt/src/button.zig:62:9: 0x2184dc in .zgt.button.Button_Impl.getLabel (zig-pinephone-gui)
if (self.peer) |*peer| {
^
zig-pinephone-gui/src/main.zig:56:27: 0x217269 in buttonClicked (zig-pinephone-gui)
.{ button.getLabel() }
^
zig-pinephone-gui/src/main.zig:9:22: 0x216b0e in main (zig-pinephone-gui)
try buttonClicked(bad_ptr);
^
zig/lib/std/start.zig:581:37: 0x21e657 in std.start.callMain (zig-pinephone-gui)
const result = root.main() catch |err| {
^
zig/lib/std/start.zig:515:12: 0x217a87 in std.start.callMainWithArgs (zig-pinephone-gui)
return @call(.{ .modifier = .always_inline }, callMain, .{});
^
zig/lib/std/start.zig:480:12: 0x217832 in std.start.main (zig-pinephone-gui)
return @call(.{ .modifier = .always_inline }, callMainWithArgs, .{ @intCast(usize, c_argc), c_argv, envp });
^
???:?:?: 0x7f6c902640b2 in ??? (???)
Aborted
Which will be super handy for troubleshooting our Zig App.
Appendix: Zig Build System
How does "zig build
" build our Zig App?
Instead of Makefiles, Zig has a Build System (essentially a tiny custom Zig program) that automates the build steps...
When we created our Zig App with...
zig init-exe
It generates this Zig Build Program that will build our Zig App: build.zig
// Zig Build Script. Originally generated by `zig init-exe`
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("zig-pinephone-gui", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
// Add zgt library to build
@import("libs/zgt/build.zig")
.install(exe, "./libs/zgt")
catch {};
}
We inserted the last part into the auto-generated code...
// Add zgt library to build
@import("libs/zgt/build.zig")
.install(exe, "./libs/zgt")
catch {};
To add the zgt GUI Library to our build.
Appendix: GTK Backend for zgt
zgt GUI Library works with GTK, Windows AND WebAssembly. How on earth does it achieve this incredible feat?
Very cleverly! zgt includes multiple GUI Backends, one for each GUI Platform...
Here's the zgt Backend for GTK (as used in our PinePhone App)...
But how does zgt talk to GTK, which is coded in C?
Zig Compiler will happily import C Header Files and make them callable from Zig. (Without creating any wrappers)
This auto-importing of C Header Files works really well, as I have experienced here...
zgt imports the C Header Files for GTK like so: libs/zgt/src/backends/gtk/backend.zig
pub const c = @cImport({
@cInclude("gtk/gtk.h");
});
Then zgt calls the imported GTK Functions like this: backend.zig
pub const Button = struct {
peer: *c.GtkWidget,
pub usingnamespace Events(Button);
fn gtkClicked(peer: *c.GtkWidget, userdata: usize) callconv(.C) void {
_ = userdata;
const data = getEventUserData(peer);
if (data.user.clickHandler) |handler| {
handler(data.userdata);
}
}
pub fn create() BackendError!Button {
// Call gtk_button_new_with_label() from GTK Library
const button = c.gtk_button_new_with_label("") orelse return error.UnknownError;
// Call gtk_widget_show() from GTK Library
c.gtk_widget_show(button);
try Button.setupEvents(button);
// Call g_signal_connect_data() from GTK Library
_ = c.g_signal_connect_data(button, "clicked", @ptrCast(c.GCallback, gtkClicked), null, @as(c.GClosureNotify, null), 0);
return Button{ .peer = button };
}
pub fn setLabel(self: *const Button, label: [:0]const u8) void {
// Call gtk_button_set_label() from GTK Library
c.gtk_button_set_label(@ptrCast(*c.GtkButton, self.peer), label);
}
pub fn getLabel(self: *const Button) [:0]const u8 {
// Call gtk_button_get_label() from GTK Library
const label = c.gtk_button_get_label(@ptrCast(*c.GtkButton, self.peer));
return std.mem.span(label);
}
};
Super Brilliant! 👏
How does zgt link our Zig App with the GTK Library?
zgt uses a Zig Build Program to link the required GUI Libraries with the executable: GTK, Win32, WebAssembly...
Oldest comments (1)
I see the link is redirected to github.com/capy-ui/capy, so is zgt renamed?