LVGL is a popular GUI Library in C that powers the User Interfaces of many Embedded Devices. (Like smartwatches)
Zig is a new-ish Programming Language that works well with C. And it comes with built-in Safety Checks at runtime.
Can we use Zig to code an LVGL Touchscreen Application?
Maybe make LVGL a little safer and friendlier... By wrapping the LVGL API in Zig?
Or will we get blocked by something beyond our control? (Like Bit Fields in LVGL Structs)
Let's find out! We'll do this on Pine64's PineDio Stack BL604 RISC-V Board (pic above) with Apache NuttX RTOS.
(The steps will be similar for other platforms)
Join me as we dive into our LVGL Touchscreen App in Zig...
(Spoiler: Answers are Yes, Maybe, Somewhat)
LVGL App in C
We begin with a barebones LVGL App in C that renders a line of text...
Fetch the Active Screen from LVGL
Create a Label Widget
Set the Properties, Text and Position of the Label
(Like the pic at the top of this article)
static void create_widgets(void) {
// Get the Active Screen
lv_obj_t *screen = lv_scr_act();
// Create a Label Widget
lv_obj_t *label = lv_label_create(screen, NULL);
// Wrap long lines in the label text
lv_label_set_long_mode(label, LV_LABEL_LONG_BREAK);
// Interpret color codes in the label text
lv_label_set_recolor(label, true);
// Center align the label text
lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
// Set the label text and colors
lv_label_set_text(
label,
"#ff0000 HELLO# " // Red Text
"#00aa00 PINEDIO# " // Green Text
"#0000ff STACK!# " // Blue Text
);
// Set the label width
lv_obj_set_width(label, 200);
// Align the label to the center of the screen, shift 30 pixels up
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, -30);
// Omitted: LVGL Canvas (we'll find out why)
}
In a while we shall convert this LVGL App to 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)
Where's the rest of the code that initialises LVGL?
We hit some complications converting the code to Zig, more about this in a while.
Zig LVGL App
Now the same LVGL App, but in Zig...
fn createWidgetsUnwrapped() !void {
// Get the Active Screen
const screen = c.lv_scr_act().?;
// Create a Label Widget
const label = c.lv_label_create(screen, null).?;
// Wrap long lines in the label text
c.lv_label_set_long_mode(label, c.LV_LABEL_LONG_BREAK);
// Interpret color codes in the label text
c.lv_label_set_recolor(label, true);
// Center align the label text
c.lv_label_set_align(label, c.LV_LABEL_ALIGN_CENTER);
// Set the label text and colors.
// `++` concatenates two strings or arrays.
c.lv_label_set_text(
label,
"#ff0000 HELLO# " ++ // Red Text
"#00aa00 PINEDIO# " ++ // Green Text
"#0000ff STACK!# " // Blue Text
);
// Set the label width
c.lv_obj_set_width(label, 200);
// Align the label to the center of the screen, shift 30 pixels up
c.lv_obj_align(label, null, c.LV_ALIGN_CENTER, 0, -30);
}
Our Zig App calls the LVGL Functions imported from C, as denoted by "c.
something".
But this looks mighty similar to C!
Yep and we see that...
-
We no longer specify Type Names
(Like lv_obj_t)
-
We write "
.?
" to catch Null Pointers(Coming up in the next section)
What's "!void
"?
"!void
" is the Return Type for our Zig Function...
-
Our Zig Function doesn't return any value
(Hence "
void
") -
But our function might return an Error
(Hence the "
!
")
Let's talk about Null Pointers and Runtime Safety...
Zig Checks Null Pointers
Earlier we saw our Zig App calling the LVGL Functions imported from C...
// Zig calls a C function
const disp_drv = c.get_disp_drv().?;
Note that we write ".?
" to catch Null Pointers returned by C Functions.
What happens if the C Function returns a Null Pointer to Zig?
// Suppose this C Function...
lv_disp_drv_t *get_disp_drv(void) {
// Returns a Null Pointer to Zig
return NULL;
}
When we run this code, we'll see a Zig Panic and a Stack Trace...
!ZIG PANIC!
attempt to use null value
Stack Trace:
0x23023606
Looking up address 23023606
in the RISC-V Disassembly for our firmware...
zig-lvgl-nuttx/lvgltest.zig:50
const disp_drv = c.get_disp_drv().?;
230235f4: 23089537 lui a0,0x23089
230235f8: 5ac50513 addi a0,a0,1452 # 230895ac <__unnamed_10>
230235fc: 4581 li a1,0
230235fe: 00000097 auipc ra,0x0
23023602: c92080e7 jalr -878(ra) # 23023290 <panic>
23023606: ff042503 lw a0,-16(s0)
2302360a: fea42623 sw a0,-20(s0)
We discover that 23023606
points to the line of code that caught the Null Pointer.
Hence Zig is super helpful for writing safer programs.
What if we omit ".?
" and do this?
const disp_drv = c.get_disp_drv();
This crashes with a RISC-V Exception when our program tries to dereference the Null Pointer in a later part of the code.
Which isn't as helpful as an immediate Zig Panic (upon receiving the Null Pointer).
Thus we always write ".?
" to catch Null Pointers returned by C Functions.
(Hopefully someday we'll have a Zig Lint Tool that will warn us if we forget to use ".?
")
Import C Functions
How do we import the C Functions and Macros for LVGL?
This is how we import the Functions and Macros from C into Zig: lvgltest.zig
/// Import the LVGL Library from C
const c = @cImport({
// NuttX Defines
@cDefine("__NuttX__", "");
@cDefine("NDEBUG", "");
@cDefine("ARCH_RISCV", "");
@cDefine("LV_LVGL_H_INCLUDE_SIMPLE", "");
// This is equivalent to...
// #define __NuttX__
// #define NDEBUG
// #define ARCH_RISCV
// #define LV_LVGL_H_INCLUDE_SIMPLE
At the top of our Zig App we set the #define Macros that will be referenced by the C Header Files coming up.
The settings above are specific to Apache NuttX RTOS and the BL602 RISC-V SoC. (Here's why)
Next comes a workaround for a C Macro Error that appears on Zig with Apache NuttX RTOS...
// Workaround for "Unable to translate macro: undefined identifier `LL`"
@cDefine("LL", "");
@cDefine("__int_c_join(a, b)", "a"); // Bypass zig/lib/include/stdint.h
We import the C Header Files for Apache NuttX RTOS...
// NuttX Header Files. This is equivalent to...
// #include "...";
@cInclude("arch/types.h");
@cInclude("../../nuttx/include/limits.h");
@cInclude("stdio.h");
@cInclude("nuttx/config.h");
@cInclude("sys/boardctl.h");
@cInclude("unistd.h");
@cInclude("stddef.h");
@cInclude("stdlib.h");
Followed by the C Header Files for the LVGL Library...
// LVGL Header Files
@cInclude("lvgl/lvgl.h");
// App Header Files
@cInclude("fbdev.h");
@cInclude("lcddev.h");
@cInclude("tp.h");
@cInclude("tp_cal.h");
});
And our Application-Specific Header Files for LCD Display and Touch Panel.
That's how we import the LVGL Library into our Zig App!
Why do we write "c.
something" when we call C functions? Like "c.lv_scr_act()"?
Remember that we import all C Functions and Macros into the "c
" Namespace...
/// Import Functions and Macros into "c" Namespace
const c = @cImport({ ... });
That's why we write "c.
something" when we refer to C Functions and Macros.
What about the Main Function of our Zig App?
It gets complicated. We'll talk later about the Main Function.
Compile Zig App
Below are the steps to compile our Zig LVGL App for Apache NuttX RTOS and BL602 RISC-V SoC.
First we download the latest version of Zig Compiler (0.10.0 or later), extract it and add to PATH...
Then we download and compile Apache NuttX RTOS for PineDio Stack BL604...
After building NuttX, we download and compile our Zig LVGL App...
## Download our Zig LVGL App for NuttX
git clone --recursive https://github.com/lupyuen/zig-lvgl-nuttx
cd zig-lvgl-nuttx
## Compile the Zig App for BL602
## (RV32IMACF with Hardware Floating-Point)
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
zig build-obj \
--verbose-cimport \
-target riscv32-freestanding-none \
-mcpu=baseline_rv32-d \
-isystem "$HOME/nuttx/nuttx/include" \
-I "$HOME/nuttx/apps/graphics/lvgl" \
-I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \
-I "$HOME/nuttx/apps/include" \
-I "$HOME/nuttx/apps/examples/lvgltest" \
lvgltest.zig
Note that target and mcpu are specific to BL602...
How did we get the Compiler Options -isystem
and -I
?
Remember that we'll link our Compiled Zig App with Apache NuttX RTOS.
Hence the Zig Compiler Options must be the same as the GCC Options used to compile NuttX.
(See the GCC Options for NuttX)
Next comes a quirk specific to BL602: We must patch the ELF Header from Software Floating-Point ABI to Hardware Floating-Point ABI...
## Patch the ELF Header of `lvgltest.o` from Soft-Float ABI to Hard-Float ABI
xxd -c 1 lvgltest.o \
| sed 's/00000024: 01/00000024: 03/' \
| xxd -r -c 1 - lvgltest2.o
cp lvgltest2.o lvgltest.o
Finally we inject our Compiled Zig App into the NuttX Project Directory and link it into the NuttX Firmware...
## Copy the compiled app to NuttX and overwrite `lvgltest.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp lvgltest.o $HOME/nuttx/apps/examples/lvgltest/lvgltest*.o
## Build NuttX to link the Zig Object from `lvgltest.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cd $HOME/nuttx/nuttx
make
## For WSL: Copy the NuttX Firmware to c:\blflash for flashing
mkdir /mnt/c/blflash
cp nuttx.bin /mnt/c/blflash
We're ready to run our Zig App!
Run Zig App
Follow these steps to flash and boot NuttX (with our Zig App inside) on PineDio Stack...
In the NuttX Shell, enter this command to start our Zig App...
lvgltest
We should see...
Zig LVGL Test
tp_init: Opening /dev/input0
cst816s_get_touch_data: DOWN: id=0, touch=0, x=176, y=23
...
tp_cal result
offset x:23, y:14
range x:189, y:162
invert x/y:1, x:0, y:1
Our Zig App responds to touch and correctly renders the LVGL Screen (pic above).
Yep we have successfully built an LVGL Touchscreen App with Zig!
(We'll talk about Touch Input in a while)
Simplify LVGL API
Can we make LVGL a little friendlier with Zig? Such that this code...
// Get the Active Screen
const screen = c.lv_scr_act().?;
// Create a Label Widget
const label = c.lv_label_create(screen, null).?;
// Wrap long lines in the label text
c.lv_label_set_long_mode(label, c.LV_LABEL_LONG_BREAK);
// Interpret color codes in the label text
c.lv_label_set_recolor(label, true);
Becomes this?
// Get the Active Screen
var screen = try lvgl.getActiveScreen();
// Create a Label Widget
var label = try screen.createLabel();
// Wrap long lines in the label text
label.setLongMode(c.LV_LABEL_LONG_BREAK);
// Interpret color codes in the label text
label.setRecolor(true);
Yes we can! By wrapping the LVGL API in Zig, which we'll do in the next section.
Note that we now use "try
" instead of ".?
" to check the values returned by LVGL...
// Check that Active Screen is valid with `try`
var screen = try lvgl.getActiveScreen();
What happens if we forget to "try
"?
If we don't "try
", like this...
// Get the Active Screen without `try`
var screen = lvgl.getActiveScreen();
// Attempt to use the Active Screen
_ = screen;
Zig Compiler stops us with an error...
./lvgltest.zig:109:9:
error: error is discarded.
consider using `try`, `catch`, or `if`
_ = screen;
^
Thus "try
" is actually safer than ".?
", Zig Compiler mandates that we check for errors.
What if LVGL returns a Null Pointer to Zig?
Our app will fail gracefully with an Application Error...
lv_scr_act failed
createWidgets failed: error.UnknownError
(Because of this Error Handler)
Wrap LVGL API
Earlier we saw the hypothetical LVGL API wrapped with Zig, let's make it real in 3 steps...
Write a function to fetch the Active Screen from LVGL
Create a Zig Struct that wraps an LVGL Screen
And another Zig Struct that wraps an LVGL Label
Get Active Screen
Below is the implementation of getActiveScreen, which fetches the Active Screen from LVGL...
/// Return the Active Screen
pub fn getActiveScreen() !Object {
// Get the Active Screen
const screen = c.lv_scr_act();
// If successfully fetched...
if (screen) |s| {
// Wrap Active Screen as Object and return it
return Object.init(s);
} else {
// Unable to get Active Screen
std.log.err("lv_scr_act failed", .{});
return LvglError.UnknownError;
}
}
What's this unusual if
expression?
if (screen) |s|
{ ... } else { ... }
That's how we check if screen is null.
If screen is not null, then s becomes the non-null value of screen. And we create an Object Struct with s inside...
if (screen) |s|
{ return Object.init(s); }
...
But if screen is null, we do the else clause and return an Error...
if (screen) |s|
{ ... }
else
{ return LvglError.UnknownError; }
That's why the Return Type for our function is !Object
pub fn getActiveScreen() !Object
{ ... }
It returns either an Object Struct or an Error. ("!
" means Error)
Let's talk about the Object Struct...
Object Struct
Object is a Zig Struct that wraps around an LVGL Object (like the Active Screen).
It defines 2 Methods...
init: Initialise the LVGL Object
createLabel: Create an LVGL Label as a child of the Object
/// LVGL Object
pub const Object = struct {
/// Pointer to LVGL Object
obj: *c.lv_obj_t,
/// Init the Object
pub fn init(obj: *c.lv_obj_t) Object {
return .{ .obj = obj };
}
/// Create a Label as a child of the Object
pub fn createLabel(self: *Object) !Label {
// Assume we won't copy from another Object
const copy: ?*const c.lv_obj_t = null;
// Create the Label
const label = c.lv_label_create(self.obj, copy);
// If successfully created...
if (label) |l| {
// Wrap as Label and return it
return Label.init(l);
} else {
// Unable to create Label
std.log.err("lv_label_create failed", .{});
return LvglError.UnknownError;
}
}
};
Label Struct
Finally we have Label, a Zig Struct that wraps around an LVGL Label.
It defines a whole bunch of Methods to set the Label Properties, Text and Position...
/// LVGL Label
pub const Label = struct {
/// Pointer to LVGL Label
obj: *c.lv_obj_t,
/// Init the Label
pub fn init(obj: *c.lv_obj_t) Label {
return .{ .obj = obj };
}
/// Set the wrapping of long lines in the label text
pub fn setLongMode(self: *Label, long_mode: c.lv_label_long_mode_t) void {
c.lv_label_set_long_mode(self.obj, long_mode);
}
/// Set the label text alignment
pub fn setAlign(self: *Label, alignment: c.lv_label_align_t) void {
c.lv_label_set_align(self.obj, alignment);
}
/// Enable or disable color codes in the label text
pub fn setRecolor(self: *Label, en: bool) void {
c.lv_label_set_recolor(self.obj, en);
}
/// Set the label text and colors
pub fn setText(self: *Label, text: [*c]const u8) void {
c.lv_label_set_text(self.obj, text);
}
/// Set the object width
pub fn setWidth(self: *Label, w: c.lv_coord_t) void {
c.lv_obj_set_width(self.obj, w);
}
/// Set the object alignment
pub fn alignObject(self: *Label, alignment: c.lv_align_t, x_ofs: c.lv_coord_t, y_ofs: c.lv_coord_t) void {
const base: ?*const c.lv_obj_t = null;
c.lv_obj_align(self.obj, base, alignment, x_ofs, y_ofs);
}
};
Let's call the wrapped LVGL API...
After Wrapping
With the wrapped LVGL API, our Zig App looks neater and safer...
/// Create the LVGL Widgets that will be rendered on the display. Calls the
/// LVGL API that has been wrapped in Zig. Based on
/// https://docs.lvgl.io/7.11/widgets/label.html#label-recoloring-and-scrolling
fn createWidgetsWrapped() !void {
// Get the Active Screen
var screen = try lvgl.getActiveScreen();
// Create a Label Widget
var label = try screen.createLabel();
// Wrap long lines in the label text
label.setLongMode(c.LV_LABEL_LONG_BREAK);
// Interpret color codes in the label text
label.setRecolor(true);
// Center align the label text
label.setAlign(c.LV_LABEL_ALIGN_CENTER);
// Set the label text and colors
label.setText(
"#ff0000 HELLO# " ++ // Red Text
"#00aa00 PINEDIO# " ++ // Green Text
"#0000ff STACK!# " // Blue Text
);
// Set the label width
label.setWidth(200);
// Align the label to the center of the screen, shift 30 pixels up
label.alignObject(c.LV_ALIGN_CENTER, 0, -30);
}
(Compare with earlier unwrapped version)
No more worries about catching Null Pointers!
(Someday LV_LABEL_LONG_BREAK and the other constants will become Enums)
Wrapping the LVGL API in Zig sounds like a lot of work?
Yep probably. Here are some ways to Auto-Generate the Zig Wrapper for LVGL...
Also remember that LVGL is Object-Oriented. Designing the right wrapper with Zig might be challenging...
Zig vs Bit Fields
Zig sounds amazing! Is there anything that Zig won't do?
Sadly Zig won't import C Structs containing Bit Fields.
Zig calls it an Opaque Type because Zig can't access the fields inside these structs.
Any struct that contains an Opaque Type also becomes an Opaque Type. So yeah this quirk snowballs quickly.
LVGL uses Bit Fields?
If we look at LVGL's Color Type lv_color_t (for 16-bit color)...
// LVGL Color Type (16-bit color)
typedef union {
struct {
// Bit Fields for RGB Color
uint16_t blue : 5;
uint16_t green : 6;
uint16_t red : 5;
} ch;
uint16_t full;
} lv_color16_t;
It uses Bit Fields to represent the RGB Colors.
Which means Zig can't access the red / green / blue fields of the struct.
(But passing a pointer to the struct is OK)
Which LVGL Structs are affected?
So far we have identified these LVGL Structs that contain Bit Fields...
Color (lv_color_t)
Display Buffer (lv_disp_buf_t)
Display Driver (lv_disp_drv_t)
Input Driver (lv_indev_drv_t)
Is there a workaround?
Our workaround is to access the structs for Color, Display Buffer, Display Driver and Input Driver inside C Functions...
And we pass the Struct Pointers to Zig.
Which explains why we see pointers to LVGL Structs in our Main Function...
Also that's why we handle Touch Input in C (instead of Zig), until we find a better solution.
Zig Outcomes
Have we gained anything by coding our LVGL App in Zig?
The parts coded in Zig will benefit from the Safety Checks enforced by Zig at runtime: Overflow, Underflow, Array Out-of-Bounds, ...
We've seen earlier that Zig works well at catching Null Pointers and Application Errors...
And that it's possible (with some effort) to create a friendlier, safer interface for LVGL...
What about the downsides of Zig?
Zig doesn't validate pointers (like with a Borrow Checker) so it isn't Memory Safe (yet)...
Zig (version 0.10.0) has a serious limitation: It won't work with C Structs containing Bit Fields...
We found a crude workaround: Handle these structs in C and pass the Struct Pointers to Zig...
But this might become a showstopper as we work with Complex LVGL Widgets. (Like LVGL Canvas)
I'll run more experiments with LVGL on Zig and report the outcome.
What's Next
I hope this article has inspired you to create LVGL Apps in Zig!
(But if you prefer to wait for Zig 1.0... That's OK too!)
Here are some tips for learning Zig...
Check out my earlier work on Zig, NuttX and LoRaWAN...
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...
Notes
- This article is the expanded version of this Twitter Thread
Appendix: Main Function
Below is our Main Function in Zig that does the following...
Initialise the LVGL Library
Initialise the Display Buffer, Display Driver and LCD Driver
Register the Display Driver with LVGL
Initialise the Touch Panel and Input Driver
Create the LVGL Widgets for display
Start the Touch Panel Calibration
Forever handle LVGL Events
We begin by importing the libraries and declaring our Main Function: lvgltest.zig
/// Import the Zig Standard Library
const std = @import("std");
/// Import our Wrapped LVGL Module
const lvgl = @import("lvgl.zig");
/// Omitted: Import the LVGL Library from C
const c = @cImport({ ... });
/// Main Function that will be called by NuttX. We render an LVGL Screen and
/// handle Touch Input.
pub export fn lvgltest_main(
_argc: c_int,
_argv: [*]const [*]const u8
) c_int {
debug("Zig LVGL Test", .{});
// Command-line args are not used
_ = _argc;
_ = _argv;
Why is argv declared as "[*]const [*]const u8"?
That's because...
-
"[*]const u8" is a Pointer to an Unknown Number of Unsigned Bytes
(Like "const uint8_t *" in C)
-
"[*]const [*]const u8" is a Pointer to an Unknown Number of the above Pointers
(Like "const uint8_t *[]" in C)
Why the "` = `something"?_
This tells the Zig Compiler that we're not using the value of "something".
The Zig Compiler helpfully stops us if we forget to use a Variable (like __argc) or the Returned Value for a Function (like for __lv_task_handler_).
Next we initialise the LVGL Library...
// Init LVGL Library
c.lv_init();
// Init Display Buffer
const disp_buf = c.get_disp_buf().?;
c.init_disp_buf(disp_buf);
// Init Display Driver
const disp_drv = c.get_disp_drv().?;
c.init_disp_drv(disp_drv, disp_buf, monitorCallback);
Because Zig won't work with C Structs containing Bit Fields, we handle them in C instead...
get_disp_buf: Get Display Buffer
init_disp_buf: Init Display Buffer
get_disp_drv: Get Display Driver
init_disp_drv: Init Display Driver
(monitorCallback is defined here)
We initialise the LCD Driver...
// Init LCD Driver
if (c.lcddev_init(disp_drv) != c.EXIT_SUCCESS) {
// If failed, try Framebuffer Driver
if (c.fbdev_init(disp_drv) != c.EXIT_SUCCESS) {
// No possible drivers left, fail
return c.EXIT_FAILURE;
}
}
// Register Display Driver with LVGL
_ = c.lv_disp_drv_register(disp_drv);
// Init Touch Panel
_ = c.tp_init();
These C Functions are specific to Apache NuttX RTOS...
lcddev_init: Init LCD Driver
fbdev_init: Init Framebuffer Driver
tp_init: Init Touch Panel
// Init Input Driver. tp_read will be called periodically
// to get the touched position and state
const indev_drv = c.get_indev_drv().?;
c.init_indev_drv(indev_drv, c.tp_read);
Again, Zig won't work with the Input Driver because the C Struct contains Bit Fields, so we handle it in C...
get_indev_drv: Get Input Driver
init_indev_drv: Init Input Driver
tp_read: Read Touch Input
We create the LVGL Widgets (the wrapped or unwrapped way)...
// Create the widgets for display
createWidgetsUnwrapped()
catch |e| {
// In case of error, quit
std.log.err("createWidgets failed: {}", .{e});
return c.EXIT_FAILURE;
};
// To call the LVGL API that's wrapped in Zig, change
// `createWidgetsUnwrapped` above to `createWidgetsWrapped`
We've seen these Zig Functions earlier...
createWidgetsUnwrapped: Create LVGL Widgets without Zig Wrapper
createWidgetsWrapped: Create LVGL Widgets with Zig Wrapper
We prepare the Touch Panel Calibration...
// Start Touch Panel calibration
c.tp_cal_create();
(tp_cal_create is defined here)
This renders (in C) the LVGL Widgets for Touch Panel Calibration, as shown in the pic above.
(Watch the calibration demo on YouTube)
(Can this be done in Zig? Needs exploration)
Finally we loop forever handling LVGL Events...
// Loop forever handing LVGL tasks
while (true) {
// Handle LVGL tasks
_ = c.lv_task_handler();
// Sleep a while
_ = c.usleep(10000);
}
return 0;
}
Our Zig App includes a Custom Logger and Panic Handler.
They are explained below...
Appendix: Compiler Options
For the LVGL App in C, Apache NuttX RTOS compiles it with this GCC Command...
## App Source Directory
cd $HOME/nuttx/apps/examples/lvgltest
## Compile lvgltest.c with GCC
riscv64-unknown-elf-gcc \
-c \
-fno-common \
-Wall \
-Wstrict-prototypes \
-Wshadow \
-Wundef \
-Os \
-fno-strict-aliasing \
-fomit-frame-pointer \
-fstack-protector-all \
-ffunction-sections \
-fdata-sections \
-g \
-march=rv32imafc \
-mabi=ilp32f \
-mno-relax \
-isystem "$HOME/nuttx/nuttx/include" \
-D__NuttX__ \
-DNDEBUG \
-DARCH_RISCV \
-pipe \
-I "$HOME/nuttx/apps/graphics/lvgl" \
-I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \
-I "$HOME/nuttx/apps/include" \
-DLV_LVGL_H_INCLUDE_SIMPLE \
-Wno-format \
-Dmain=lvgltest_main \
-lvgltest.c \
-o lvgltest.c.home.user.nuttx.apps.examples.lvgltest.o
(As observed from "make --trace
")
The above options for "-isystem
" and "-I
"...
-isystem "$HOME/nuttx/nuttx/include"
-I "$HOME/nuttx/apps/graphics/lvgl"
-I "$HOME/nuttx/apps/graphics/lvgl/lvgl"
-I "$HOME/nuttx/apps/include"
Were passed to the Zig Compiler when compiling our Zig App...
As for the above "#defines
"...
-D__NuttX__
-DNDEBUG
-DARCH_RISCV
-DLV_LVGL_H_INCLUDE_SIMPLE
We set them at the top of our Zig Program...
The GCC Options above were also passed to the Zig Compiler for Auto-Translating the LVGL App from C to Zig...
Appendix: Auto-Translate LVGL App to Zig
Zig Compiler can Auto-Translate C code to Zig. (See this)
We used the Auto-Translation as a Reference when converting our LVGL App from C to Zig.
Here's how we Auto-Translate our LVGL App lvgltest.c from C to Zig...
Take the GCC Command from the previous section
Change "riscv64-unknown-elf-gcc" to "zig translate-c"
-
Add the target...
-target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \
Remove "-march=rv32imafc"
Surround the C Flags by "-cflags ... --"
Like this...
## App Source Directory
cd $HOME/nuttx/apps/examples/lvgltest
## Auto-translate lvgltest.c from C to Zig
zig translate-c \
-target riscv32-freestanding-none \
-mcpu=baseline_rv32-d \
-cflags \
-fno-common \
-Wall \
-Wstrict-prototypes \
-Wshadow \
-Wundef \
-Os \
-fno-strict-aliasing \
-fomit-frame-pointer \
-fstack-protector-all \
-ffunction-sections \
-fdata-sections \
-g \
-mabi=ilp32f \
-mno-relax \
-Wno-format \
-- \
-isystem "$HOME/nuttx/nuttx/include" \
-D__NuttX__ \
-DNDEBUG \
-DARCH_RISCV \
-I "$HOME/nuttx/apps/graphics/lvgl" \
-I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \
-I "$HOME/nuttx/apps/include" \
-DLV_LVGL_H_INCLUDE_SIMPLE \
-Dmain=lvgltest_main \
lvgltest.c \
>lvgltest.zig
Note that target and mcpu are specific to BL602...
Zig Compiler internally uses Clang (instead of GCC) to interpret our C code.
We made 2 fixes to our C code to support Clang...
-
We inserted this...
#if defined(__NuttX__) && defined(__clang__) // Workaround for NuttX with zig cc #include <arch/types.h> #include "../../nuttx/include/limits.h" #define FAR #endif // defined(__NuttX__) && defined(__clang__)
-
And changed this...
static void monitor_cb(lv_disp_drv_t * disp_drv, uint32_t time, uint32_t px) { #ifndef __clang__ // Doesn't compile with zig cc ginfo("%" PRIu32 " px refreshed in %" PRIu32 " ms\n", px, time); #endif // __clang__ }
Here's the original C code: lvgltest.c
And the Auto-Translation from C to Zig: translated/lvgltest.zig
The Auto-Translation looks way too verbose for a Zig Program, but it's a good start for converting our LVGL App from C to Zig.
We hit some issues with Opaque Types in the Auto-Translation, this is how we fixed them...
Appendix: Zig Auto-Translation is Incomplete
The Auto-Translation from C to Zig was initially missing 2 key functions...
lvgltest_main: Main Function
create_widgets: Create Widgets Function
The Auto-Translated Zig code shows...
// lvgltest.c:129:13: warning: unable to translate function, demoted to extern
pub extern fn create_widgets() callconv(.C) void;
// lvgltest.c:227:17: warning: local variable has opaque type
// (no file):353:14: warning: unable to translate function, demoted to extern
pub extern fn lvgltest_main(arg_argc: c_int, arg_argv: [*c][*c]u8) c_int;
When we look up lvgltest.c line 227...
int lvgltest_main(int argc, FAR char *argv[]) {
// lvgltest.c:227:17: warning: local variable has opaque type
lv_disp_drv_t disp_drv;
lv_disp_buf_t disp_buf;
...
We see that Zig couldn't import the struct for LVGL Display Driver lv_disp_drv_t because it's an Opaque Type.
Let's find out why...
Appendix: Zig Opaque Types
What's an Opaque Type in Zig?
Opaque Types are Zig Structs with unknown, inaccessible fields.
When we import into Zig a C Struct that contains Bit Fields, it becomes an Opaque Type...
For example, LVGL's Color Type contains Bit Fields. It becomes an Opaque Type when we import into Zig...
// LVGL Color Type (16-bit color)
typedef union {
struct {
// Bit Fields for RGB Color
uint16_t blue : 5;
uint16_t green : 6;
uint16_t red : 5;
} ch;
uint16_t full;
} lv_color16_t;
What's wrong with Opaque Types?
Zig Compiler won't let us access the fields of an Opaque Type. But it's OK to pass a pointer to an Opaque Type.
Any struct that contains a (non-pointer) Opaque Type, also becomes an Opaque Type.
How do we discover Opaque Types?
In the previous section, Zig Compiler has identified the LVGL Display Driver lv_disp_drv_t as an Opaque Type...
// warning: local variable has opaque type
lv_disp_drv_t disp_drv;
To find out why, we search for lv_disp_drv_t in the Auto-Translated Zig code...
// nuttx/apps/graphics/lvgl/lvgl/src/lv_hal/lv_hal_disp.h:154:9:
// warning: struct demoted to opaque type - has bitfield
pub const lv_disp_drv_t = struct__disp_drv_t;
pub const struct__disp_drv_t = opaque {};
// nuttx/apps/graphics/lvgl/lvgl/src/lv_hal/lv_hal_disp.h:59:23:
// warning: struct demoted to opaque type - has bitfield
pub const lv_disp_t = struct__disp_t;
pub const struct__disp_t = opaque {};
pub const lv_disp_buf_t = opaque {};
Which says that lv_disp_drv_t contains Bit Fields.
To verify, we look up the C definitions of lv_disp_drv_t, lv_disp_t and lv_disp_buf_t from LVGL...
// LVGL Display Driver
typedef struct _disp_drv_t {
uint32_t rotated : 1;
uint32_t dpi : 10;
...
} lv_disp_drv_t;
// LVGL Display
typedef struct _disp_t {
uint8_t del_prev : 1;
uint32_t inv_p : 10;
...
} lv_disp_t;
// LVGL Display Buffer
typedef struct {
volatile uint32_t last_area : 1;
volatile uint32_t last_part : 1;
...
} lv_disp_buf_t;
We're now certain that Zig Compiler couldn't import the structs because they indeed contain Bit Fields.
Let's fix the Opaque Types, by passing them as pointers...
Fix Opaque Types
Earlier we saw that Zig couldn't import these C Structs because they contain Bit Fields...
lv_disp_drv_t: LVGL Display Driver
lv_disp_buf_t: LVGL Display Buffer
Instead of creating and accessing these structs in Zig, we do it in C instead...
// Return the static instance of Display Driver, because Zig can't
// allocate structs wth bitfields inside.
lv_disp_drv_t *get_disp_drv(void) {
static lv_disp_drv_t disp_drv;
return &disp_drv;
}
// Return the static instance of Display Buffer, because Zig can't
// allocate structs wth bitfields inside.
lv_disp_buf_t *get_disp_buf(void) {
static lv_disp_buf_t disp_buf;
return &disp_buf;
}
// Initialise the Display Driver, because Zig can't access its fields.
void init_disp_drv(lv_disp_drv_t *disp_drv,
lv_disp_buf_t *disp_buf,
void (*monitor_cb)(struct _disp_drv_t *, uint32_t, uint32_t)) {
assert(disp_drv != NULL);
assert(disp_buf != NULL);
assert(monitor_cb != NULL);
lv_disp_drv_init(disp_drv);
disp_drv->buffer = disp_buf;
disp_drv->monitor_cb = monitor_cb;
}
// Initialise the Display Buffer, because Zig can't access the fields.
void init_disp_buf(lv_disp_buf_t *disp_buf) {
assert(disp_buf != NULL);
lv_disp_buf_init(disp_buf, buffer1, buffer2, DISPLAY_BUFFER_SIZE);
}
Then we modify our C Main Function to access these structs via pointers...
int lvgltest_main(int argc, FAR char *argv[]) {
// Fetch pointers to Display Driver and Display Buffer
lv_disp_drv_t *disp_drv = get_disp_drv();
lv_disp_buf_t *disp_buf = get_disp_buf();
...
// Init Display Buffer and Display Driver as pointers
init_disp_buf(disp_buf);
init_disp_drv(disp_drv, disp_buf, monitor_cb);
...
// Init Input Driver as pointer
lv_indev_drv_t *indev_drv = get_indev_drv();
init_indev_drv(indev_drv, tp_read);
(get_indev_drv and init_indev_drv are explained in the next section)
After this modification, our Auto-Translation from C to Zig now contains the 2 missing functions...
lvgltest_main: Main Function
create_widgets: Create Widgets Function
Which we used as a reference to convert our LVGL App from C to Zig.
That's why our Zig Main Function passes pointers to the Display Buffer and Display Driver, instead of working directly with the structs...
Input Driver
LVGL Input Driver lv_indev_drv_t is another Opaque Type because it contains Bit Fields.
We fix lv_indev_drv_t the same way as other Opaque Types: We allocate and initialise the structs in C (instead of Zig)...
// Return the static instance of Input Driver, because Zig can't
// allocate structs wth bitfields inside.
lv_indev_drv_t *get_indev_drv(void) {
static lv_indev_drv_t indev_drv;
return &indev_drv;
}
// Initialise the Input Driver, because Zig can't access its fields.
void init_indev_drv(lv_indev_drv_t *indev_drv,
bool (*read_cb)(struct _lv_indev_drv_t *, lv_indev_data_t *)) {
assert(indev_drv != NULL);
assert(read_cb != NULL);
lv_indev_drv_init(indev_drv);
indev_drv->type = LV_INDEV_TYPE_POINTER;
// This function will be called periodically (by the library) to get the
// mouse position and state.
indev_drv->read_cb = read_cb;
lv_indev_drv_register(indev_drv);
}
These functions are called by our Zig Main Function during initialisation...
Color Type
We fixed all references to LVGL Color Type lv_color_t...
// LVGL Canvas Demo doesn't work with zig cc because of `lv_color_t`
#if defined(CONFIG_USE_LV_CANVAS) && !defined(__clang__)
// Set the Canvas Buffer (Warning: Might take a lot of RAM!)
static lv_color_t cbuf[LV_CANVAS_BUF_SIZE_TRUE_COLOR(CANVAS_WIDTH, CANVAS_HEIGHT)];
...
That's because lv_color_t is another Opaque Type...
// From Zig Auto-Translation
pub const lv_color_t = lv_color16_t;
pub const lv_color16_t = extern union {
ch: struct_unnamed_7,
full: u16,
};
// nuttx/apps/graphics/lvgl/lvgl/src/lv_core/../lv_draw/../lv_misc/lv_color.h:240:18:
// warning: struct demoted to opaque type - has bitfield
const struct_unnamed_7 = opaque {};
That contains Bit Fields...
// LVGL Color Type (16-bit color)
typedef union {
struct {
// Bit fields for lv_color16_t (aliased to lv_color_t)
uint16_t blue : 5;
uint16_t green : 6;
uint16_t red : 5;
} ch;
uint16_t full;
} lv_color16_t;
Hence we can't work directly with the LVGL Color Type in Zig. (But we can pass pointers to it)
Is that a problem?
Some LVGL Widgets need us to specify the LVGL Color. (Like for LVGL Canvas)
This gets tricky in Zig, since we can't manipulate LVGL Color.
Why not fake the LVGL Color Type in Zig?
// Fake the LVGL Color Type in Zig
const lv_color16_t = extern union {
ch: u16, // Bit Fields add up to 16 bits
full: u16,
};
We could, but then the LVGL Types in Zig would become out of sync with the original LVGL Definitions in C.
This might cause problems when we upgrade the LVGL Library.
Appendix: Auto-Generate Zig Wrapper
Can we auto-generate the Zig Wrapper for LVGL?
Earlier we talked about the Zig Wrapper for LVGL that will make LVGL a little safer and friendlier...
To Auto-Generate the LVGL Wrapper, we might use Type Reflection in Zig...
Or we can look up the Type Info JSON generated by Zig Compiler...
## Emit IR (LLVM), BC (LLVM) and Type Info JSON
zig build-obj \
-femit-llvm-ir \
-femit-llvm-bc \
-femit-analysis \
--verbose-cimport \
-target riscv32-freestanding-none \
-mcpu=baseline_rv32-d \
-isystem "$HOME/nuttx/nuttx/include" \
-I "$HOME/nuttx/apps/graphics/lvgl" \
-I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \
-I "$HOME/nuttx/apps/include" \
-I "$HOME/nuttx/apps/examples/lvgltest" \
lvgltest.zig
This produces the IR (LLVM), BC (LLVM) and Type Info JSON files...
lvgltest.ll
lvgltest.bc
lvgltest-analysis.json
Let's look up the Type Info for the LVGL Function lv_obj_align.
We search for lv_obj_align in lvgltest-analysis.json
"decls":
...
{
"import": 99,
"src": 1962,
"name": "lv_obj_align",
"kind": "const",
"type": 148, // lv_obj_align has Type 148
"value": 60
},
Then we look up Type 148 in lvgltest-analysis.json
$ jq '.types[148]' lvgltest-analysis.json
{
"kind": 18,
"name": "fn(?*.cimport:10:11.struct__lv_obj_t, ?*const .cimport:10:11.struct__lv_obj_t, u8, i16, i16) callconv(.C) void",
"generic": false,
"ret": 70,
"args": [
79, // First Para has Type 79
194,
95,
134,
134
]
}
The First Parameter has Type 79, so we look up lvgltest-analysis.json and follow the trail...
$ jq '.types[79]' lvgltest-analysis.json
{
"kind": 13,
"child": 120
}
## Kind 13 is `?` (Optional)
$ jq '.types[120]' lvgltest-analysis.json
{
"kind": 6,
"elem": 137
}
## Kind 6 is `*` (Pointer)
$ jq '.types[137]' lvgltest-analysis.json
{
"kind": 20,
"name": ".cimport:10:11.struct__lv_obj_t"
}
## Kind 20 is `struct`???
Which gives us the complete type of the First Parameter...
?*.cimport:10:11.struct__lv_obj_t
With this Type Info, we could generate the Zig Wrapper for all LVGL Functions.
We don't have the Parameter Names though, we might need to parse the ".cimport" file.
Object-Oriented Wrapper for LVGL
Is LVGL really Object-Oriented?
Yep the LVGL API is actually Object-Oriented since it uses Inheritance.
All LVGL Widgets (Labels, Buttons, etc) have the same Base Type: lv_obj_t.
But some LVGL Functions will work only for specific Widgets, whereas some LVGL Functions will work on any Widget...
lv_label_set_text works only for Labels
lv_obj_set_width works for any Widget
The LVGL Docs also say that LVGL is Object-Oriented...
Designing an Object-Oriented Zig Wrapper for LVGL might be challenging...
Our Zig Wrapper needs to support setWidth (and similar methods) for all LVGL Widgets.
To do this we might use Zig Interfaces and @fieldParentPtr...
Which look somewhat similar to VTables in C++...
Are there any Object-Oriented Bindings for LVGL?
The official Python Bindings for LVGL seem to be Object-Oriented. This could inspire our Object-Oriented Wrapper in Zig...
However the Python Bindings are Dynamically Typed, might be tricky implementing them as Static Types in Zig.
The LVGL Wrapper in this article was inspired by the zgt GUI Library, which wraps the GUI APIs for GTK, Win32 and WebAssembly...
Top comments (2)
Zig is also very good in the embedded field
Yep Zig works pretty well for Embedded, according to my tests :-)