LVGL is a popular Graphics Library for Microcontrollers. (In C)
Zig Compiler works great for compiling C Libraries into WebAssembly. (Based on Clang Compiler)
Can we preview an LVGL App in the Web Browser... With WebAssembly and Zig Compiler? Let's find out!
Why are we doing this?
Right now we're creating a Feature Phone UI (in Zig) for Apache NuttX RTOS (Real-Time Operating System) on Pine64 PinePhone.
Would be awesome if we could prototype the Feature Phone UI in our Web Browser... To make the UI Coding a little easier!
Doesn't LVGL support WebAssembly already?
Today LVGL runs in a Web Browser by compiling with Emscripten and SDL.
Maybe we can do better with newer tools like Zig Compiler? In this article we'll...
Run a Zig LVGL App on PinePhone (with NuttX RTOS)
Explain how Zig works with WebAssembly (and C Libraries)
Compile LVGL Library from C to WebAssembly (with Zig Compiler)
Test it with our LVGL App (in Zig)
Render Simple LVGL UIs (in Web Browser)
Later we might render LVGL UI Controls (with Touch Input)
Maybe someday we'll code and test our LVGL Apps in a Web Browser, thanks to Zig Compiler and WebAssembly!
Mandelbrot Set rendered with Zig and WebAssembly
WebAssembly with Zig
Why Zig? How does it work with WebAssembly?
Zig Programming Language is a Low-Level Systems Language (like C and Rust) that works surprisingly well with WebAssembly.
(And Embedded Devices like PinePhone)
The pic above shows a WebAssembly App that we created with Zig, JavaScript and HTML...
-
Our Zig Program exports a function that computes the Mandelbrot Set pixels: mandelbrot.zig
/// Compute the Pixel Color at (px,py) for Mandelbrot Set export fn get_pixel_color(px: i32, py: i32) u8 { var iterations: u8 = 0; var x0 = @intToFloat(f32, px); var y0 = @intToFloat(f32, py); ... while ((xsquare + ysquare < 4.0) and (iterations < MAX_ITER)) : (iterations += 1) { tmp = xsquare - ysquare + x0; y = 2 * x * y + y0; x = tmp; xsquare = x * x; ysquare = y * y; } return iterations; }
-
Our JavaScript calls the Zig Function above to compute the Mandelbrot Set: game.js
// Load our WebAssembly Module `mandelbrot.wasm` // https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming let Game = await WebAssembly.instantiateStreaming( fetch("mandelbrot.wasm"), importObject ); ... // For every Pixel in our HTML Canvas... for (let x = 0; x < canvas.width; x++) { for (let y = 0; y < canvas.height; y++) { // Get the Pixel Color from Zig const color = Game.instance.exports .get_pixel_color(x, y); // Render the Pixel in our HTML Canvas if (color < 10) { context.fillStyle = "red"; } else if (color < 128) { context.fillStyle = "grey"; } else { context.fillStyle = "white"; } context.fillRect(x, y, x + 1, y + 1); } }
And it renders the pixels in a HTML Canvas.
-
Our HTML Page defines the HTML Canvas and loads the above JavaScript: demo.html
<html> <body> <!-- HTML Canvas for rendering Mandelbrot Set --> <canvas id="game_canvas" width="640" height="480"></canvas> </body> <!-- Load our JavaScript --> <script src="game.js"></script> </html>
That's all we need to create a WebAssembly App with Zig!
What's mandelbrot.wasm?
mandelbrot.wasm is the WebAssembly Module for our Zig Program, compiled by the Zig Compiler...
## Download and compile the Zig Program for our Mandelbrot Demo
git clone --recursive https://github.com/lupyuen/pinephone-lvgl-zig
cd pinephone-lvgl-zig/demo
zig build-lib \
mandelbrot.zig \
-target wasm32-freestanding \
-dynamic \
-rdynamic
wasm32-freestanding tells the Zig Compiler to compile our Zig Program into a WebAssembly Module.
How do we run this?
Start a Local Web Server. (Like Web Server for Chrome)
Browse to demo/demo.html. And we'll see the Mandelbrot Set in our Web Browser! (Pic above)
Zig Calls JavaScript
Can Zig call out to JavaScript?
Yep Zig and JavaScript will happily interoperate both ways!
In our Zig Program, this is how we import a JavaScript Function and call it: mandelbrot.zig
// Import Print Function from JavaScript into Zig
extern fn print(i32) void;
// Print a number to JavaScript Console. Warning: This is slow!
if (iterations == 1) { print(iterations); }
In our JavaScript, we export the print function as we load the WebAssembly Module: game.js
// Export JavaScript Functions to Zig
let importObject = {
// JavaScript Environment exported to Zig
env: {
// JavaScript Print Function exported to Zig
print: function(x) { console.log(x); }
}
};
// Load our WebAssembly Module
// and export our Print Function to Zig
let Game = await WebAssembly.instantiateStreaming(
fetch("mandelbrot.wasm"), // Load our WebAssembly Module
importObject // Export our Print Function to Zig
);
This works OK for printing numbers to the JavaScript Console.
Will this work for passing Strings and Buffers?
It gets complicated... We need to snoop the WebAssembly Memory.
We'll come back to this when we talk about WebAssembly Logging.
Zig LVGL App on PinePhone with Apache NuttX RTOS
LVGL App in Zig
Will Zig work with LVGL?
Yep we tested an LVGL App in Zig with PinePhone and Apache NuttX RTOS (pic above): lvgltest.zig
/// LVGL App in Zig that renders a Text Label
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_WRAP);
// Interpret color codes in the label text
label.setRecolor(true);
// Center align the label text
label.setAlign(c.LV_TEXT_ALIGN_CENTER);
// Set the label text and colors
label.setText(
"#ff0000 HELLO# " ++ // Red Text
"#00aa00 LVGL ON# " ++ // Green Text
"#0000ff PINEPHONE!# " // 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);
}
(lvgl is our LVGL Wrapper for Zig)
To compile our Zig LVGL App for PinePhone and NuttX RTOS...
## Compile the Zig App `lvgltest.zig`
## for PinePhone (Armv8-A with Cortex-A53)
zig build-obj \
-target aarch64-freestanding-none \
-mcpu cortex_a53 \
-isystem "../nuttx/include" \
-I "../apps/include" \
-I "../apps/graphics/lvgl" \
... \
lvgltest.zig
## Copy the Compiled Zig App to NuttX RTOS
## and overwrite `lv_demo_widgets.*.o`
cp lvgltest.o \
../apps/graphics/lvgl/lvgl/demos/widgets/lv_demo_widgets.*.o
## Omitted: Link the Compiled Zig App with NuttX RTOS
Zig Compiler produces an Object File lvgltest.o that looks exactly like an ordinary C Object File...
Which links perfectly fine into Apache NuttX RTOS.
And our LVGL Zig App runs OK on PinePhone! (Pic above)
LVGL App in WebAssembly
But will our Zig LVGL App run in a Web Browser with WebAssembly?
Let's find out! We shall...
Compile our Zig LVGL App to WebAssembly
-
Compile LVGL Library from C to WebAssembly
(With Zig Compiler)
Render the LVGL Display in JavaScript
Will our Zig LVGL App compile to WebAssembly?
Let's take the earlier steps to compile our Zig LVGL App. To compile for WebAssembly, we change...
"zig build-obj" to "zig build-lib"
Target becomes "wasm32-freestanding"
Add "-dynamic" and "-rdynamic"
Remove "-mcpu"
Like this...
## Compile the Zig App `lvglwasm.zig`
## for WebAssembly
zig build-lib \
-target wasm32-freestanding \
-dynamic \
-rdynamic \
-isystem "../nuttx/include" \
-I "../apps/include" \
-I "../apps/graphics/lvgl" \
...\
lvglwasm.zig
And we cloned lvgltest.zig to lvglwasm.zig, because we'll tweak it for WebAssembly.
We removed our Custom Panic Handler, the default one works fine for WebAssembly.
What happens when we run this?
The command above produces the Compiled WebAssembly lvglwasm.wasm.
We start a Local Web Server. (Like Web Server for Chrome)
And browse to our HTML lvglwasm.html
- Which calls our JavaScript lvglwasm.js
(To load the Compiled WebAssembly)
- Which calls our Zig Function lv_demo_widgets
(To render the LVGL Widgets)
- That's exported to WebAssembly by our Zig App lvglwasm.zig
But the WebAssembly won't load in our Web Browser!
Uncaught (in promise) LinkError:
WebAssembly.instantiate():
Import #1 module="env" function="lv_label_create" error:
function import requires a callable
That's because we haven't linked lv_label_create from the LVGL Library.
Let's compile the LVGL Library to WebAssembly...
Compile LVGL to WebAssembly with Zig Compiler
Will Zig Compiler compile C Libraries? Like LVGL?
Yep! This is how we call Zig Compiler to compile lv_label_create and lv_label.c from the LVGL Library...
## Compile LVGL from C to WebAssembly
zig cc \
-target wasm32-freestanding \
-dynamic \
-rdynamic \
-lc \
-DFAR= \
-DLV_MEM_CUSTOM=1 \
-DLV_FONT_MONTSERRAT_20=1 \
-DLV_FONT_DEFAULT_MONTSERRAT_20=1 \
-DLV_USE_LOG=1 \
-DLV_LOG_LEVEL=LV_LOG_LEVEL_TRACE \
"-DLV_ASSERT_HANDLER={void lv_assert_handler(void); lv_assert_handler();}" \
... \
lvgl/src/widgets/lv_label.c \
-o ../../../pinephone-lvgl-zig/lv_label.o
This compiles lv_label.c from C to WebAssembly and generates lv_label.o.
We changed these options...
- "zig build-lib" becomes "zig cc"
(Because we're compiling C, not Zig)
- Add "-lc"
(Because we're calling C Standard Library)
- Add "-DFAR="
(Because we won't need Far Pointers)
- Add "-DLV_MEM_CUSTOM=1"
(Because we're calling malloc instead of LVGL's TLSF Allocator)
- Set the Default Font to Montserrat 20...
-DLV_FONT_MONTSERRAT_20=1 \
-DLV_FONT_DEFAULT_MONTSERRAT_20=1 \
(Remember to compile LVGL Fonts!)
- Enable Detailed Logging...
-DLV_USE_LOG=1 \
-DLV_LOG_LEVEL=LV_LOG_LEVEL_TRACE \
- Handle Assertion Failure...
"-DLV_ASSERT_HANDLER={void lv_assert_handler(void); lv_assert_handler();} \"
- Emit the WebAssembly Object File...
-o ../../../pinephone-lvgl-zig/lv_label.o
This works because Zig Compiler calls Clang Compiler to compile LVGL Library from C to WebAssembly.
So we link lv_label.o with our Zig LVGL App?
Yep we ask Zig Compiler to link the Compiled WebAssembly lv_label.o with our Zig LVGL App lvglwasm.zig...
## Compile the Zig App `lvglwasm.zig` for WebAssembly
## and link with `lv_label.o` from LVGL Library
zig build-lib \
-target wasm32-freestanding \
-dynamic \
-rdynamic \
-lc \
-DFAR= \
-DLV_MEM_CUSTOM=1 \
-DLV_FONT_MONTSERRAT_20=1 \
-DLV_FONT_DEFAULT_MONTSERRAT_20=1 \
-DLV_USE_LOG=1 \
-DLV_LOG_LEVEL=LV_LOG_LEVEL_TRACE \
"-DLV_ASSERT_HANDLER={void lv_assert_handler(void); lv_assert_handler();}" \
... \
lvglwasm.zig \
lv_label.o
When we browse to our HTML lvglwasm.html, we see this in the JavaScript Console...
Uncaught (in promise) LinkError:
WebAssembly.instantiate():
Import #0 module="env" function="lv_obj_clear_flag" error:
function import requires a callable
lv_label_create is no longer missing, because Zig Compiler has linked lv_label.o into our Zig LVGL App.
(Yep Zig Compiler works great for linking WebAssembly Object Files with our Zig App!)
Now we need to compile lv_obj_clear_flag (and the other LVGL Files) from C to WebAssembly...
Compile Entire LVGL Library to WebAssembly
Compile the entire LVGL Library to WebAssembly? Sounds so tedious!
Yeah through sheer tenacity we tracked down lv_obj_clear_flag and all the Missing LVGL Functions called by our Zig LVGL App...
widgets/lv_label.c
core/lv_obj.c
misc/lv_mem.c
core/lv_event.c
core/lv_obj_style.c
core/lv_obj_pos.c
misc/lv_txt.c
draw/lv_draw_label.c
core/lv_obj_draw.c
misc/lv_area.c
core/lv_obj_scroll.c
font/lv_font.c
core/lv_obj_class.c
(Many many more)
So we wrote a script to compile the above LVGL Source Files from C to WebAssembly: build.sh
## Compile our LVGL Display Driver from C to WebAssembly with Zig Compiler
compile_lvgl ../../../../../pinephone-lvgl-zig/display.c display.o
## Compile LVGL Library from C to WebAssembly with Zig Compiler
compile_lvgl font/lv_font_montserrat_14.c lv_font_montserrat_14.o
compile_lvgl font/lv_font_montserrat_20.c lv_font_montserrat_20.o
compile_lvgl widgets/lv_label.c lv_label.o
compile_lvgl core/lv_obj.c lv_obj.o
compile_lvgl misc/lv_mem.c lv_mem.o
## Many many more
(compile_lvgl is defined here)
(More about display.c later)
And link the Compiled LVGL WebAssemblies with our Zig LVGL App: build.sh
## Compile the Zig App `lvglwasm.zig` for WebAssembly
## and link with LVGL Library compiled for WebAssembly
zig build-lib \
-target wasm32-freestanding \
... \
lvglwasm.zig \
display.o \
lv_font_montserrat_14.o \
lv_font_montserrat_20.o \
lv_label.o \
lv_mem.o \
...
We're done with LVGL Library in WebAssembly! (Almost)
Now what happens when we run it?
JavaScript Console says that strlen is missing...
Uncaught (in promise) LinkError:
WebAssembly.instantiate():
Import #0 module="env" function="strlen" error:
function import requires a callable
Which comes from the C Standard Library. Here's the workaround...
Is it really OK to compile only the necessary LVGL Source Files?
Instead of compiling ALL the LVGL Source Files?
Be careful! We might miss out some Undefined Variables... Zig Compiler blissfully assumes they're at WebAssembly Address 0. And remember to compile the LVGL Fonts!
Thus we really ought to compile ALL the LVGL Source Files.
(Maybe we should disassemble the Compiled WebAssembly and look for other Undefined Variables at WebAssembly Address 0)
LVGL Porting Layer for WebAssembly
Anything else we need for LVGL in WebAssembly?
LVGL expects a millis function that returns the number of Elapsed Milliseconds...
Uncaught (in promise) LinkError:
WebAssembly.instantiate():
Import #0 module="env" function="millis" error:
function import requires a callable
We implement millis in Zig: lvglwasm.zig
/// TODO: Return the number of elapsed milliseconds
export fn millis() u32 {
elapsed_ms += 1;
return elapsed_ms;
}
/// Number of elapsed milliseconds
var elapsed_ms: u32 = 0;
/// On Assertion Failure, ask Zig to print a Stack Trace and halt
export fn lv_assert_handler() void {
@panic("*** lv_assert_handler: ASSERTION FAILED");
}
/// Custom Logger for LVGL that writes to JavaScript Console
export fn custom_logger(buf: [*c]const u8) void {
wasmlog.Console.log("{s}", .{buf});
}
(We should reimplement millis in JavaScript, though it might be slow)
In the code above, we defined lv_assert_handler and custom_logger to handle Assertions and Logging in LVGL.
Let's talk about LVGL Logging...
WebAssembly Logger for LVGL
printf won't work in WebAssembly...
How will we trace the LVGL Execution?
We set the Custom Logger for LVGL, so that we can print Log Messages to the JavaScript Console: lvglwasm.zig
/// Main Function for our Zig LVGL App
pub export fn lv_demo_widgets() void {
// Set the Custom Logger for LVGL
c.lv_log_register_print_cb(custom_logger);
// Init LVGL
c.lv_init();
("c.
" refers to functions imported from C to Zig)
custom_logger is defined in our Zig Program: lvglwasm.zig
/// Custom Logger for LVGL that writes to JavaScript Console
export fn custom_logger(buf: [*c]const u8) void {
wasmlog.Console.log("{s}", .{buf});
}
wasmlog is our Zig Logger for WebAssembly: wasmlog.zig
Which calls JavaScript Functions jsConsoleLogWrite and jsConsoleLogFlush to write logs to the JavaScript Console: lvglwasm.js
// Export JavaScript Functions to Zig
const importObject = {
// JavaScript Functions exported to Zig
env: {
// Write to JavaScript Console from Zig
// https://github.com/daneelsan/zig-wasm-logger/blob/master/script.js
jsConsoleLogWrite: function(ptr, len) {
console_log_buffer += wasm.getString(ptr, len);
},
// Flush JavaScript Console from Zig
// https://github.com/daneelsan/zig-wasm-logger/blob/master/script.js
jsConsoleLogFlush: function() {
console.log(console_log_buffer);
console_log_buffer = "";
},
(Thanks to daneelsan/zig-wasm-logger)
What's wasm.getString?
wasm.getString is our JavaScript Function that reads the WebAssembly Memory into a JavaScript Array: lvglwasm.js
// WebAssembly Helper Functions in JavaScript
const wasm = {
// WebAssembly Instance
instance: undefined,
// Init the WebAssembly Instance
init: function (obj) {
this.instance = obj.instance;
},
// Fetch the Zig String from a WebAssembly Pointer
getString: function (ptr, len) {
const memory = this.instance.exports.memory;
const text_decoder = new TextDecoder();
return text_decoder.decode(
new Uint8Array(memory.buffer, ptr, len)
);
},
};
(TextDecoder converts bytes to text)
(Remember earlier we spoke about snooping WebAssembly Memory with a WebAssembly Pointer? This is how we do it)
Now we can see the LVGL Log Messages in the JavaScript Console yay! (Pic above)
[Warn] lv_disp_get_scr_act:
no display registered to get its active screen
(in lv_disp.c line #54)
Let's initialise the LVGL Display...
Initialise LVGL Display
What happens when LVGL runs?
According to the LVGL Docs, this is how we initialise and operate LVGL...
Call lv_init
Register the LVGL Display (and Input Devices)
-
Call lv_tick_inc(x) every x milliseconds (in an Interrupt) to report the Elapsed Time to LVGL
(Not required, because LVGL calls millis to fetch the Elapsed Time)
Call lv_timer_handler every few milliseconds to handle LVGL Tasks
To register the LVGL Display, we follow these steps...
Easy peasy for Zig right?
Sadly we can't do it in Zig...
// Nope, can't allocate LVGL Display Driver in Zig!
// `lv_disp_drv_t` is an Opaque Type
var disp_drv = c.lv_disp_drv_t{};
c.lv_disp_drv_init(&disp_drv);
Because LVGL Display Driver lv_disp_drv_t is an Opaque Type.
(Same for the LVGL Draw Buffer lv_disp_draw_buf_t)
What's an Opaque Type in Zig?
When we import a C Struct into Zig and it contains Bit Fields...
Zig Compiler won't let us access the fields of the C Struct. And we can't allocate the C Struct either.
lv_disp_drv_t contains Bit Fields, hence it's an Opaque Type and inaccessible in Zig. (See this)
Bummer. How to fix Opaque Types in Zig?
Our workaround is to write C Functions to allocate and initialise the Opaque Types...
Which gives us this LVGL Display Interface for Zig: display.c
Finally with the workaround, here's how we initialise the LVGL Display in Zig: lvglwasm.zig
/// Main Function for our Zig LVGL App
pub export fn lv_demo_widgets() void {
// Create the Memory Allocator for malloc
memory_allocator = std.heap.FixedBufferAllocator
.init(&memory_buffer);
// Set the Custom Logger for LVGL
c.lv_log_register_print_cb(custom_logger);
// Init LVGL
c.lv_init();
// Fetch pointers to Display Driver and Display Buffer,
// exported by our C Functions
const disp_drv = c.get_disp_drv();
const disp_buf = c.get_disp_buf();
// Init Display Buffer and Display Driver as pointers,
// by calling our C Functions
c.init_disp_buf(disp_buf);
c.init_disp_drv(
disp_drv, // Display Driver
disp_buf, // Display Buffer
flushDisplay, // Callback Function to Flush Display
720, // Horizontal Resolution
1280 // Vertical Resolution
);
// Register the Display Driver as a pointer
const disp = c.lv_disp_drv_register(disp_drv);
// Create the widgets for display
createWidgetsWrapped() catch |e| {
// In case of error, quit
std.log.err("createWidgetsWrapped failed: {}", .{e});
return;
};
// Up Next: Handle LVGL Tasks
(memory_allocator is explained here)
Now we handle LVGL Tasks...
Handle LVGL Tasks
Earlier we talked about handling LVGL Tasks...
-
Call lv_tick_inc(x) every x milliseconds (in an Interrupt) to report the Elapsed Time to LVGL
(Not required, because LVGL calls millis to fetch the Elapsed Time)
Call lv_timer_handler every few milliseconds to handle LVGL Tasks
This is how we call lv_timer_handler in Zig: lvglwasm.zig
/// Main Function for our Zig LVGL App
pub export fn lv_demo_widgets() void {
// Omitted: Init LVGL Display
// Create the widgets for display
createWidgetsWrapped() catch |e| {
// In case of error, quit
std.log.err("createWidgetsWrapped failed: {}", .{e});
return;
};
// Handle LVGL Tasks
// TODO: Call this from Web Browser JavaScript,
// so that our Web Browser won't block
var i: usize = 0;
while (i < 5) : (i += 1) {
_ = c.lv_timer_handler();
}
We're ready to render the LVGL Display in our HTML Page!
Something doesn't look right...
Yeah we should have called lv_timer_handler from our JavaScript.
(Triggered by a JavaScript Timer or requestAnimationFrame)
But for our quick demo, this will do. For now!
Render LVGL Display in Zig
Finally we render our LVGL Display in the Web Browser... Spanning C, Zig and JavaScript! (Pic above)
Earlier we saw this LVGL Initialisation in our Zig App: lvglwasm.zig
// Init LVGL
c.lv_init();
// Fetch pointers to Display Driver and Display Buffer,
// exported by our C Functions
const disp_drv = c.get_disp_drv();
const disp_buf = c.get_disp_buf();
// Init Display Buffer and Display Driver as pointers,
// by calling our C Functions
c.init_disp_buf(disp_buf);
c.init_disp_drv(
disp_drv, // Display Driver
disp_buf, // Display Buffer
flushDisplay, // Callback Function to Flush Display
720, // Horizontal Resolution
1280 // Vertical Resolution
);
What's inside init_disp_buf?
init_disp_buf tells LVGL to render the display pixels to our LVGL Canvas Buffer: display.c
// Init the LVGL Display Buffer in C, because Zig
// can't access the fields of the Opaque Type
void init_disp_buf(lv_disp_draw_buf_t *disp_buf) {
lv_disp_draw_buf_init(
disp_buf, // LVGL Display Buffer
canvas_buffer, // Render the pixels to our LVGL Canvas Buffer
NULL, // No Secondary Buffer
BUFFER_SIZE // Buffer the entire display (720 x 1280 pixels)
);
}
(canvas_buffer is defined here)
Then our Zig App initialises the LVGL Display Driver: lvglwasm.zig
// Init Display Driver as pointer,
// by calling our C Function
c.init_disp_drv(
disp_drv, // Display Driver
disp_buf, // Display Buffer
flushDisplay, // Callback Function to Flush Display
720, // Horizontal Resolution
1280 // Vertical Resolution
);
(init_disp_drv is defined here)
This tells LVGL to call flushDisplay (in Zig) when the LVGL Display Canvas is ready to be rendered: lvglwasm.zig
/// LVGL calls this Callback Function to flush our display
export fn flushDisplay(
disp_drv: ?*c.lv_disp_drv_t, // LVGL Display Driver
area: [*c]const c.lv_area_t, // LVGL Display Area
color_p: [*c]c.lv_color_t // LVGL Display Buffer
) void {
// Call the Web Browser JavaScript
// to render the LVGL Canvas Buffer
render();
// Notify LVGL that the display has been flushed.
// Remember to call `lv_disp_flush_ready`
// or Web Browser will hang on reload!
c.lv_disp_flush_ready(disp_drv);
}
flushDisplay (in Zig) calls render (in JavaScript) to render the LVGL Display Canvas.
We bubble up from Zig to JavaScript...
Zig LVGL App rendered in Web Browser with WebAssembly
Render LVGL Display in JavaScript
Phew OK. What happens in our JavaScript?
Earlier we saw that flushDisplay (in Zig) calls render (in JavaScript) to render the LVGL Display Canvas.
render (in JavaScript) draws the LVGL Canvas Buffer to our HTML Canvas: lvglwasm.js
// Render the LVGL Canvas from Zig to HTML
// https://github.com/daneelsan/minimal-zig-wasm-canvas/blob/master/script.js
render: function() { // TODO: Add width and height
// Get the WebAssembly Pointer to the LVGL Canvas Buffer
const bufferOffset = wasm.instance.exports.getCanvasBuffer();
// Load the WebAssembly Pointer into a JavaScript Image Data
const memory = wasm.instance.exports.memory;
const ptr = bufferOffset;
const len = (canvas.width * canvas.height) * 4;
const imageDataArray = new Uint8Array(memory.buffer, ptr, len)
imageData.data.set(imageDataArray);
// Render the Image Data to the HTML Canvas
context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(imageData, 0, 0);
}
(imageData and context are defined here)
How does it fetch the LVGL Canvas Buffer?
The JavaScript above calls getCanvasBuffer (in Zig) and get_canvas_buffer (in C) to fetch the LVGL Canvas Buffer: display.c
// Canvas Buffer for rendering LVGL Display
// TODO: Swap the RGB Bytes in LVGL, the colours are inverted for HTML Canvas
#define HOR_RES 720 // Horizontal Resolution
#define VER_RES 1280 // Vertical Resolution
#define BUFFER_ROWS VER_RES // Number of rows to buffer
#define BUFFER_SIZE (HOR_RES * BUFFER_ROWS)
static lv_color_t canvas_buffer[BUFFER_SIZE];
// Return a pointer to the LVGL Canvas Buffer
lv_color_t *get_canvas_buffer(void) {
return canvas_buffer;
}
And the LVGL Display renders OK in our HTML Canvas yay! (Pic above)
(Thanks to daneelsan/minimal-zig-wasm-canvas)
What's Next
Up Next: Feature Phone UI for PinePhone! To make our Feature Phone clickable, we'll pass Mouse Events from JavaScript to LVGL.
(Through an LVGL Input Device Driver)
We'll experiment with Live Reloading: Whenever we save our Zig LVGL App, it auto-recompiles and auto-reloads the WebAssembly HTML.
Which makes UI Prototyping a lot quicker in LVGL. Stay Tuned for updates!
Meanwhile please check out the other articles on NuttX for PinePhone...
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/lvgl3.md
Appendix: C Standard Library is Missing
strlen is missing from our Zig WebAssembly...
But strlen should come from the C Standard Library! (musl)
Not sure why strlen is missing, but we fixed it (temporarily) by copying from the Zig Library Source Code: lvglwasm.zig
// C Standard Library from zig-macos-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/zig/c.zig
export fn strlen(s: [*:0]const u8) callconv(.C) usize {
return std.mem.len(s);
}
// Also memset, memcpy, strcpy...
(Maybe because we didn't export strlen in our Zig Main Program lvglwasm.zig?)
What if we change the target to wasm32-freestanding-musl?
Nope doesn't help, same problem.
What if we use "zig build-exe" instead of "zig build-lib"?
Sorry "zig build-exe" is meant for building WASI Executables. (See this)
"zig build-exe" is not supposed to work for WebAssembly in the Web Browser. (See this)
Appendix: LVGL Memory Allocation
What happens if we omit "-DLV_MEM_CUSTOM=1"?
By default, LVGL uses the Two-Level Segregate Fit (TLSF) Allocator for Heap Memory.
But TLSF Allocator fails inside block_next...
main: start
loop: start
lv_demo_widgets: start
before lv_init
[Info] lv_init: begin (in lv_obj.c line #102)
[Trace] lv_mem_alloc: allocating 76 bytes (in lv_mem.c line #127)
[Trace] lv_mem_alloc: allocated at 0x1a700 (in lv_mem.c line #160)
[Trace] lv_mem_alloc: allocating 28 bytes (in lv_mem.c line #127)
[Trace] lv_mem_alloc: allocated at 0x1a750 (in lv_mem.c line #160)
[Warn] lv_init: Log level is set to 'Trace' which makes LVGL much slower (in lv_obj.c line #176)
[Trace] lv_mem_realloc: reallocating 0x14 with 8 size (in lv_mem.c line #196)
[Error] block_next: Asserted at expression: !block_is_last(block) (in lv_tlsf.c line #459)
004a5b4a:0x29ab2 Uncaught (in promise) RuntimeError: unreachable
at std.builtin.default_panic (004a5b4a:0x29ab2)
at lv_assert_handler (004a5b4a:0x2ac6c)
at block_next (004a5b4a:0xd5b3)
at lv_tlsf_realloc (004a5b4a:0xe226)
at lv_mem_realloc (004a5b4a:0x20f1)
at lv_layout_register (004a5b4a:0x75d8)
at lv_flex_init (004a5b4a:0x16afe)
at lv_extra_init (004a5b4a:0x16ae5)
at lv_init (004a5b4a:0x3f28)
at lv_demo_widgets (004a5b4a:0x29bb9)
Thus we set "-DLV_MEM_CUSTOM=1" to call malloc instead of LVGL's TLSF Allocator.
(block_next calls offset_to_block, which calls tlsf_cast. Maybe the Pointer Cast doesn't work for Clang WebAssembly?)
But Zig doesn't support malloc for WebAssembly!
We call Zig's FixedBufferAllocator to implement malloc: lvglwasm.zig
/// Main Function for our Zig LVGL App
pub export fn lv_demo_widgets() void {
// Create the Memory Allocator for malloc
memory_allocator = std.heap.FixedBufferAllocator
.init(&memory_buffer);
Here's our (incomplete) implementation of malloc: lvglwasm.zig
/// Zig replacement for malloc
export fn malloc(size: usize) ?*anyopaque {
// TODO: Save the slice length
const mem = memory_allocator.allocator().alloc(u8, size) catch {
@panic("*** malloc error: out of memory");
};
return mem.ptr;
}
/// Zig replacement for realloc
export fn realloc(old_mem: [*c]u8, size: usize) ?*anyopaque {
// TODO: Call realloc instead
const mem = memory_allocator.allocator().alloc(u8, size) catch {
@panic("*** realloc error: out of memory");
};
_ = memcpy(mem.ptr, old_mem, size);
if (old_mem != null) {
// TODO: How to free without the slice length?
// memory_allocator.allocator().free(old_mem[0..???]);
}
return mem.ptr;
}
/// Zig replacement for free
export fn free(mem: [*c]u8) void {
if (mem == null) {
@panic("*** free error: pointer is null");
}
// TODO: How to free without the slice length?
// memory_allocator.allocator().free(mem[0..???]);
}
/// Memory Allocator for malloc
var memory_allocator: std.heap.FixedBufferAllocator = undefined;
/// Memory Buffer for malloc
var memory_buffer = std.mem.zeroes([1024 * 1024]u8);
(Remember to copy the old memory in realloc!)
(If we ever remove "-DLV_MEM_CUSTOM=1", remember to set "-DLV_MEM_SIZE=1000000")
Appendix: LVGL Fonts
Remember to compile the LVGL Fonts! Or our LVGL Text Label won't be rendered...
## Compile LVGL Fonts from C to WebAssembly with Zig Compiler
compile_lvgl font/lv_font_montserrat_14.c lv_font_montserrat_14
compile_lvgl font/lv_font_montserrat_20.c lv_font_montserrat_20
## Compile the Zig LVGL App for WebAssembly
## and link with LVGL Fonts
zig build-lib \
-DLV_FONT_MONTSERRAT_14=1 \
-DLV_FONT_MONTSERRAT_20=1 \
-DLV_FONT_DEFAULT_MONTSERRAT_20=1 \
-DLV_USE_FONT_PLACEHOLDER=1 \
...
lv_font_montserrat_14.o \
lv_font_montserrat_20.o \
Appendix: LVGL Screen Not Found
Why does LVGL say "No Screen Found" in lv_obj_get_disp?
[Info] lv_init: begin (in lv_obj.c line #102)
[Trace] lv_init: finished (in lv_obj.c line #183)
before lv_disp_drv_register
[Warn] lv_obj_get_disp: No screen found (in lv_obj_tree.c line #290)
[Info] lv_obj_create: begin (in lv_obj.c line #206)
[Trace] lv_obj_class_create_obj: Creating object with 0x12014 class on 0 parent (in lv_obj_class.c line #45)
[Warn] lv_obj_get_disp: No screen found (in lv_obj_tree.c line #290)
That's because the Display Linked List _lv_disp_ll is allocated by LV_ITERATE_ROOTS in _lv_gc_clear_roots...
And we forgot to compile _lv_gc_clear_roots in lv_gc.c. Duh!
(Zig Compiler assumes that Undefined Variables like _lv_disp_ll are at WebAssembly Address 0)
After compiling _lv_gc_clear_roots and lv_gc.c, the "No Screen Found" error no longer appears.
(Maybe we should disassemble the Compiled WebAssembly and look for other Undefined Variables at WebAssembly Address 0)
TODO: For easier debugging, how to disassemble Compiled WebAssembly with cross-reference to Source Code? Similar to "objdump --source"? Maybe with wabt or binaryen?
Top comments (0)