Porting PineTime Watch Face from C to Rust On RIOT with LVGL

Rust on RIOT on PineTime Smart Watch

This article is presented in CINEMASCOPE... Rotate your phone to view the C and Rust source code side by side... Or better yet, read this article on a desktop computer

We'll learn step by step to convert this Embedded C code (based on LVGL) to Embedded Rust on RIOT...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(home_time_widget_t *ht) {

   // Create a label for time (00:00)
   lv_obj_t *scr = lv_obj_create(NULL, NULL);
   lv_obj_t *label1 = lv_label_create(scr, NULL);

   lv_label_set_text(label1, "00:00");
   lv_obj_set_width(label1, 240);
   lv_obj_set_height(label1, 200);
   ht->lv_time = label1;
   ...
   return scr;
}
fn create_widgets(widgets: &mut WatchFaceWidgets) ->
   LvglResult<()> {

   // Create a label for time (00:00)
   let scr = widgets.screen;
   let label1 = label::create(scr, ptr::null()) ? ;

   label::set_text(label1, strn!("00:00")) ? ;
   obj::set_width(label1, 240) ? ;
   obj::set_height(label1, 200) ? ;
   widgets.time_label = label1;
   ...
   Ok(())
}
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

We'll also learn how Rust handles memory safety when calling C functions...

Original C CodeConverted Rust Code
int set_time_label(home_time_widget_t *ht) {

   // Create a string buffer on stack
   char time[6];

   // Format the time
   int res = snprintf(time,
     sizeof(time),
     "%02u:%02u",
     ht->time.hour,
     ht->time.minute);

  if (res != sizeof(time) - 1) {
    LOG_ERROR("overflow");
    return -1;
  }

  // Set the label
  lv_label_set_text(ht->lv_time, time);

  // Return OK
  return 0;
}
fn set_time_label(
   widgets: &WatchFaceWidgets,
   state: &WatchFaceState) ->
   LvglResult<()> {

   // Create a static string buffer
   static mut TIME_BUF: String =
     new_string();

   unsafe {
     // Format the time
     TIME_BUF.clear();
     write!(&mut TIME_BUF,
       "{:02}:{:02}\0",
       state.time.hour,
       state.time.minute)
       .expect("overflow");

     // Set the label
     label::set_text(widgets.time_label,
       &to_strn(&TIME_BUF) ? ;
   }

   // Return OK
   Ok(())
}
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

1 Function Declaration

Here's a C function that calls the LVGL library to create a Label Widget. The Label Widget displays the time of the day (like 23:59). This code was taken from the bosmoment / PineTime-apps port of RIOT to the PineTime Smart Watch.

lv_obj_t *screen_time_create(home_time_widget_t *ht) {
    //  Create a label for time (00:00)
    lv_obj_t *scr = lv_obj_create(NULL, NULL);
    lv_obj_t *label1 = lv_label_create(scr, NULL);

    lv_label_set_text(label1, "00:00");
    lv_obj_set_width(label1, 240);
    lv_obj_set_height(label1, 200);
    ht->lv_time = label1;
    return scr;
}

From widgets/home_time/screen_time.c

Functions whose names start with lv_ (like lv_obj_create) are defined in the LVGL library. lv_obj_t is a C Struct exposed by the LVGL library. home_time_widget_t is a custom C Struct defined by the RIOT application.

Let's start by converting this function declaration from C to Rust...

lv_obj_t *screen_time_create(home_time_widget_t *ht) { ...

This function accepts a pointer and returns another pointer. In Rust, functions are defined with the fn keyword...

fn screen_time_create( ...

The return type lv_obj_t goes to the end of the function declaration, marked by ->...

fn screen_time_create(ht: *mut home_time_widget_t) 
    -> *mut lv_obj_t { ...

Note that the names and types have been flipped, also for pointers...

Original C CodeConverted Rust Code
lv_obj_t **mut lv_obj_t
home_time_widget_t *htht: *mut home_time_widget_t
lv_obj_t *screen_time_create(...)fn screen_time_create(...)
  -> *mut lv_obj_t

As we convert code from C to Rust, we'll find ourselves doing a lot of this Name/Type Flipping.

Rust is strict about Mutability of variables (whether a variable's value may be modified). *mut declares that the pointer refers to an object that is Mutable (i.e. may be modified). For objects that may not be modified, we write *const (similar to C).

Here's the C function declaration converted to Rust...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(
  home_time_widget_t *ht)
fn screen_time_create(
  ht: *mut home_time_widget_t)
  -> *mut lv_obj_t
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

2 Variable Declaration

Now let's convert this variable declaration from C to Rust...

lv_obj_t *scr = lv_obj_create( ... ); 

scr is a pointer to a C Struct lv_obj_t. scr is set to the value returned by the C function LVGL lv_obj_create (which creates a LVGL Screen).

In Rust, variables are declared with the let keyword, followed by the variable name and type...

let scr: *mut lv_obj_t = lv_obj_create( ... );

(Yep we did the Name/Type Flipping again)

Here's a really cool thing about Rust... Types are optional in variable declarations!

We may drop the type *mut lv_obj_t, resulting in this perfectly valid Rust declaration...

let scr = lv_obj_create( ... );

What is this type dropping magic? Won't Rust complain about the missing type?

If we think about it... lv_obj_create is a C function already declared somewhere. The Rust Compiler already knows that lv_obj_create returns a value of type *mut lv_obj_t.

Thus the Rust Compiler uses Type Inference to deduce that scr must have type *mut lv_obj_t!

This saves us a lot of rewriting when we convert C code to Rust.

Here's how it looks when we convert to Rust the two variable declarations from our C function...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(
  home_time_widget_t *ht) {
fn screen_time_create(
  ht: *mut home_time_widget_t)
  -> *mut lv_obj_t {
  // Create a label for time (00:00)  // Create a label for time (00:00)
  lv_obj_t *scr = lv_obj_create( ... );  let scr = lv_obj_create( ... );
  lv_obj_t *label1 = lv_label_create(scr, ... );  let label1 = lv_label_create(scr, ... );
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

The parameters are missing from the above code... Let's learn to convert NULL to Rust.

3 Null Pointers

NULL is an unfortunate fact of life for C coders. In our C code we pass two NULL pointers to lv_obj_create...

//  In C: Call lv_obj_create passing 2 NULL pointers
lv_obj_t *scr = lv_obj_create(NULL, NULL); 

Both NULLs look the same to C... But not to Rust! Let's look at the function declaration in C...

//  In C: Function declaration for lv_obj_create
lv_obj_t * lv_obj_create(lv_obj_t *parent, const lv_obj_t *copy);

From lvgl/lv_core/lv_obj.h

See the difference? The first parameter is a non-const pointer (i.e. it's Mutable), whereas the second parameter is a const pointer.

Here's how we pass the two NULL pointers in Rust...

//  In Rust: Call lv_obj_create passing 2 NULL pointers: 1 mutable, 1 const
let scr = lv_obj_create(ptr::null_mut(), ptr::null());

null_mut creates a NULL Mutable pointer, null creates a Non-Mutable const NULL pointer.

ptr references the Rust Core Library, which we import like this...

//  In Rust: Import the Rust Core Library for pointer handling
use core::ptr;

When we insert the NULL parameters into the converted Rust code, we get this...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(
  home_time_widget_t *ht) {
fn screen_time_create(
  ht: *mut home_time_widget_t)
  -> *mut lv_obj_t {
  // Create a label for time (00:00)  // Create a label for time (00:00)
  lv_obj_t *scr = lv_obj_create(  let scr = lv_obj_create(
      NULL,      ptr::null_mut(),
      NULL      ptr::null()
  );  );
  lv_obj_t *label1 = lv_label_create(      let label1 = lv_label_create(
      scr,      scr,
      NULL      ptr::null()
  );  );
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

4 Import C Functions into Rust

Let's look back at the C code that we're convering to Rust...

//  In C: Create a label for time (00:00)
lv_obj_t *scr = lv_obj_create(NULL, NULL);
lv_obj_t *label1 = lv_label_create(scr, NULL);

//  Set the text, width and height of the label
lv_label_set_text(label1, "00:00");
lv_obj_set_width(label1, 240);
lv_obj_set_height(label1, 200);

The lv_... functions called above come from the LVGL library. Here are the function declarations in C...

//  In C: LVGL Function Declarations
lv_obj_t * lv_obj_create(lv_obj_t *parent, const lv_obj_t *copy);
lv_obj_t * lv_label_create(lv_obj_t *par, const lv_obj_t *copy);
void lv_label_set_text(lv_obj_t *label, const char *text);
void lv_obj_set_width(lv_obj_t *obj, int16_t w);
void lv_obj_set_height(lv_obj_t *obj, int16_t h);

From lvgl/lv_core/lv_obj.h, lvgl/lv_objx/lv_label.h

To call these C functions from Rust, we need to import them with extern "C" like this...

//  In Rust: Import LVGL Functions
extern "C" {
    fn lv_obj_create(parent: *mut lv_obj_t, copy: *const lv_obj_t) -> *mut lv_obj_t;
    fn lv_label_create(par: *mut lv_obj_t, copy: *const lv_obj_t) -> *mut lv_obj_t;
    fn lv_label_set_text(label: *mut lv_obj_t, text: *const u8);
    fn lv_obj_set_width(obj: *mut lv_obj_t, w: i16);
    fn lv_obj_set_height(obj: *mut lv_obj_t, h: i16);
}

From rust/lvgl/src/core/obj.rs, rust/lvgl/src/objx/label.rs

See the Name/Type Flipping? We did it again!

Take note of the *mut and *const pointers... Rust is very picky about Mutability!

What's *const u8? It's complicated... We'll talk about strings in a while.

Once the C functions have been imported, we may call them in Rust like this...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(
  home_time_widget_t *ht) {
fn screen_time_create(
  ht: *mut home_time_widget_t)
  -> *mut lv_obj_t {
  // Create a label for time (00:00)  // Create a label for time (00:00)
  lv_obj_t *scr = lv_obj_create(  let scr = lv_obj_create(
      NULL, NULL      ptr::null_mut(), ptr::null()
  );  );
  lv_obj_t *label1 = lv_label_create(      let label1 = lv_label_create(
      scr, NULL      scr, ptr::null()
  );  );
  // Set the text, width and height  // Set the text, width and height
  lv_label_set_text(  lv_label_set_text(
      label1, "00:00"      label1, // TODO
  );  );
  lv_obj_set_width(  lv_obj_set_width(
      label1, 240      label1, 240
  );  );
  lv_obj_set_height(  lv_obj_set_height(
      label1, 200      label1, 200
  );  );
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

5 Numeric Types

Something interesting happened when we took this C function declaration...

//  In C: Function declaration for lv_obj_set_width
void lv_obj_set_width(lv_obj_t *obj, int16_t w);

And imported it into Rust...

//  In Rust: Import lv_obj_set_width function from C
extern "C" {
    fn lv_obj_set_width(obj: *mut lv_obj_t, w: i16);
}

Look at the second parameter... How did int16_t in C (16-bit signed integer) become i16 in Rust?

You might have guessed... Numeric Types in Rust have no-nonsense, super-compact names!

So int16_t gets shortened to i16. uint16_t (unsigned 16-bit integer) gets shortened to u16.

Numeric Types are such a joy to write! And there's no need to #include <stdint.h>

C Numeric Type   Rust Numeric Type
int8_ti8
uint8_tu8
int16_ti16
uint16_tu16
int32_ti32
uint32_tu32
int64_ti64
uint64_tu64
floatf32
doublef64

In Rust we use u8 to refer to a byte.

6 Pass Strings from Rust to C

Rust has a powerful String type for manipulating strings (stored in heap memory)... But we'll look at a simpler way to pass strings from Rust to C.

This is our original C code...

//  In C: Declare function lv_label_set_text
void lv_label_set_text(lv_obj_t *label, const char *text);
...
//  Set the text of the label to "00:00"
lv_label_set_text(label1, "00:00");

Here's how we pass the string "00:00" from Rust to C...

//  In Rust: Import function lv_label_set_text from C
extern "C" {
    fn lv_label_set_text(label: *mut lv_obj_t, text: *const u8);
}
...
//  Set the text of the label to "00:00"
lv_label_set_text(
    label1,
    b"00:00\0".as_ptr()
);

Remember that u8 in Rust means unsigned byte, so *const u8 in Rust is similar to const char * in C.

Let's compare the C string and its Rust equivalent...

C String   Rust Equivalent
"00:00"    b"00:00\0".as_ptr()

The b"..." notation creates a Rust Byte String. A Byte String is an array of bytes, similar to strings in C.

Unlike C, strings in Rust don't have a terminating null. So we manually added the null: \0

In C, arrays and pointers are interchangeable, so char * behaves like char[]... But not in Rust!

Rust arrays have an internal counter that remembers the length of the array. Which explains why Rust strings don't have a terminating null... Rust internally tracks the length of each string.

To convert a Rust array to a pointer, we use as_ptr() as shown above.

What happens if we forget to add the terminating null \0? Catastrophe!

The C function lv_label_set_text will get very confused without the terminating null. So the above Byte String notation b"..." is prone to problems.

Later we'll see an easier, safer way to write strings... With a Rust Macro.

//  In Rust: Set the label text with a macro
lv_label_set_text(
    label1,
    strn!("00:00")
);

7 Pointer Dereferencing

In C we write -> to dereference a pointer and access a Struct field...

//  In C: Dereference the pointer ht and set the lv_time field
ht->lv_time = label1;

Rust doesn't have a combined operator for dereferencing pointers and accessing Struct fields. Instead, we use the * and . operators, which have the same meanings as in C...

//  In Rust: Dereference the pointer ht and set the lv_time field
(*ht).lv_time = label1;

8 Return Value

In C we use the return keyword to set the return value of the current function...

lv_obj_t *screen_time_create(home_time_widget_t *ht) {
    ...
    //  In C: Return scr as the value of the function
    return scr;
}

In Rust the return keyword works the same way...

fn screen_time_create(ht: *mut home_time_widget_t) -> *mut lv_obj_t { 
    ...
    //  In Rust: Return scr as the value of the function
    return scr;
}

Another way to set the return value in Rust: Just write the value as the last expression of the function...

fn screen_time_create(ht: *mut home_time_widget_t) -> *mut lv_obj_t { 
    ...
    //  In Rust: Return scr as the value of the function. Note: No semicolon ";" at the end
    scr
}

If we use this convention, the last expression of the function should not end with a semicolon.

9 C to Rust Conversion: First Version

Following the steps above, we'll get this line-by-line conversion from C to Rust...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(
  home_time_widget_t *ht) {
fn screen_time_create(
  ht: *mut home_time_widget_t)
  -> *mut lv_obj_t {
  // Create a label for time (00:00)  // Create a label for time (00:00)
  lv_obj_t *scr = lv_obj_create(  let scr = lv_obj_create(
      NULL, NULL      ptr::null_mut(), ptr::null()
  );  );
  lv_obj_t *label1 = lv_label_create(      let label1 = lv_label_create(
      scr, NULL      scr, ptr::null()
  );  );
  // Set the text, width and height  // Set the text, width and height
  lv_label_set_text(  lv_label_set_text(
      label1, "00:00"      label1, b"00:00\0".as_ptr()
  );  );
  lv_obj_set_width(  lv_obj_set_width(
      label1, 240      label1, 240
  );  );
  lv_obj_set_height(  lv_obj_set_height(
      label1, 200      label1, 200
  );  );
  ht->lv_time = label1;  (*ht).lv_time = label1;
  return scr;  scr
}}
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

The importing of C functions into Rust has been omitted from the code above. Now let's learn to import C Structs and Enums into Rust.

10 Import C Structs into Rust

home_time_widget_t is a C Struct that's passed as a parameter into our Rust function. Here's how we import home_time_widget_t into Rust...

Original C CodeConverted Rust Code
typedef struct _home_time_widget {#[repr(C)]
struct home_time_widget_t {
  widget_t widget;  widget: widget_t,
  control_event_handler_t handler;  handler: control_event_handler_t,
  lv_obj_t *screen;  screen: *mut lv_obj_t,
  lv_obj_t *lv_time;  lv_time: *mut lv_obj_t,
  lv_obj_t *lv_date;  lv_date: *mut lv_obj_t,
  lv_obj_t *lv_ble;  lv_ble: *mut lv_obj_t,
  lv_obj_t *lv_power;  lv_power: *mut lv_obj_t,
  bleman_ble_state_t ble_state;  ble_state: bleman_ble_state_t,
  controller_time_spec_t time;  time: controller_time_spec_t,
  uint32_t millivolts;  millivolts: u32,
  bool charging;  charging: bool,
  bool powered;  powered: bool,
} home_time_widget_t;}
From widgets/home_time/include/home_time.hFrom rust/app/src/watch_face.rs

Note the Name/Type Flipping. Also semicolons ";" have been replaced by commas ",".

We'll need to import the C types widget_t, control_event_handler_t, lv_obj_t, bleman_ble_state_t and controller_time_spec_t the same way.

What's #[repr(C)]?

The Rust Compiler is really clever in laying out Struct fields to save storage space. Unfortunately this optimised layout is not compatible with C... Rust would not be able to access correctly the Struct fields passed from C.

To fix this, we specify #[repr(C)]. This tells the Rust Compiler that the Struct uses the C layout for fields instead of the Rust layout.

11 Import C Enums into Rust

The Struct above contains a C Enum bleman_ble_state_t. Here's how we import bleman_ble_state_t into Rust...

Original C CodeConverted Rust Code
typedef enum {#[repr(u8)]
#[derive(PartialEq)]
enum bleman_ble_state_t {
  BLEMAN_BLE_STATE_INACTIVE,  BLEMAN_BLE_STATE_INACTIVE = 0,
  BLEMAN_BLE_STATE_ADVERTISING,  BLEMAN_BLE_STATE_ADVERTISING = 1,
  BLEMAN_BLE_STATE_DISCONNECTED,  BLEMAN_BLE_STATE_DISCONNECTED = 2,
  BLEMAN_BLE_STATE_CONNECTED,  BLEMAN_BLE_STATE_CONNECTED = 3,
} bleman_ble_state_t;}
From modules/bleman/include/bleman.hFrom rust/app/src/watch_face.rs

Note that we specified in Rust the Enum values 0, 1, 2, 3 to avoid any possible ambiguity.

What's #[repr(u8)]?

Recall that u8 refers to an unsigned byte. When we specify #[repr(u8)], we tell the Rust Compiler that this Enum uses 8 bits to store the value of the Enum.

Thus the code above assumes that the C Enum value passed into our Rust function is 8 bits wide.

What's the size of a C Enum? 8 bits, 16 bits, 32 bits, ...?

That depends on the values in the C Enum. Check this article for details: "How Big Is An Enum?"

What's #[derive(PartialEq)]?

#[derive(PartialEq)] is needed so that we may compare Enum values like this...

//  In Rust: Compare an enum value
if state.ble_state == bleman_ble_state_t::BLEMAN_BLE_STATE_DISCONNECTED { ...

From rust/app/src/watch_face.rs

Note that Enum values are prefixed by the Enum type name, like bleman_ble_state_t::...

Importing of C functions and types looks tedious and error-prone... Is there a better way to import C functions and types into Rust?

Yes! Later we'll look at an automated way to import C functions and types: bindgen

12 Unsafe Code in Embedded Rust

Earlier we took this C code...

//  In C: Declare function lv_label_set_text
void lv_label_set_text(lv_obj_t *label, const char *text);
...
//  Set the text of the label to "00:00"
lv_label_set_text(label1, "00:00");

And converted it to Rust...

//  In Rust: Import function lv_label_set_text from C
extern "C" {
    fn lv_label_set_text(label: *mut lv_obj_t, text: *const u8);
}
...
//  Set the text of the label to "00:00"
lv_label_set_text(
    label1,
    b"00:00\0".as_ptr()
);

Recall that b"00:00\0".as_ptr() is the Rust Byte String equivalent of "00:00" in C. This is the string that's passed by the above Rust code to the C function lv_label_set_text.

What happens when we remove \0 from the Rust Byte String?

lv_label_set_text will receive an invalid string that's not terminated by null.

lv_label_set_text may get stuck forever searching for the terminating null. Or it may attempt to copy a ridiculously huge string and corrupt the system memory.

Surely the Rust Compiler can verify that all Rust Byte Strings as null terminated... Right?

Well if we look at the calling contract that we have agreed with C...

//  In C: Declare function lv_label_set_text
void lv_label_set_text(lv_obj_t *label, const char *text);

It doesn't say that text requires a terminating null... Legally we may pass in any const char * pointer!

Calling lv_label_set_text is an example of Unsafe Code in Rust. That's the Rust Compiler saying...

I'm sorry, Dave. I'm afraid I can't do that. I won't let you call function lv_label_set_text because I'm not sure whether the C function will cause memory corruption or cause the system to crash. I'm not even sure if the function lv_label_set_text will ever return!

To override HAL... er... the Rust Compiler, we need to wrap the Unsafe Code with the unsafe keyword...

//  In Rust: Set the text of the label to "00:00"
unsafe {
    lv_label_set_text(
        label1,
        b"00:00\0".as_ptr()
    );
}

This needs to be done for every C function that we call from Rust. Which will look incredibly messy.

Later we'll see the fix for this: Safe Wrappers.

13 Import C Types and Functions into Rust with bindgen

Earlier we used this Rust code to import C functions from the LVGL library into Rust...

//  In Rust: Import LVGL Functions
extern "C" {
    fn lv_obj_create(parent: *mut lv_obj_t, copy: *const lv_obj_t) -> *mut lv_obj_t;
    fn lv_label_create(par: *mut lv_obj_t, copy: *const lv_obj_t) -> *mut lv_obj_t;
    fn lv_label_set_text(label: *mut lv_obj_t, text: *const u8);
    fn lv_obj_set_width(obj: *mut lv_obj_t, w: i16);
    fn lv_obj_set_height(obj: *mut lv_obj_t, h: i16);
}

From rust/lvgl/src/core/obj.rs, rust/lvgl/src/objx/label.rs

The above Rust code was automatically generated by a command-line tool named bindgen. We install bindgen and run it like this...

cargo install bindgen
bindgen lv_obj.h -o obj.rs

bindgen takes a C Header File (like lv_obj.h from LVGL) and generates the Rust code (like in obj.rs above) to import the C types and functions declared in the Header File.

Thus bindgen is a tool that generates Rust Bindings for C types and functions...

What if the C Header File includes other Header Files?

Yep that makes bindgen more complicated... Because bindgen can't generate bindings unless it knows the definition of every C type referenced by our Header File.

Here's how we specify the Include Folders for the Header Files...

bindgen lv_obj.h -o obj.rs \
    -- \
    -Ibaselibc/include/ \
    -Iapps/pinetime/bin/pkg/pinetime/ \
    -Iapps/pinetime \
    -DRIOT_BOARD=BOARD_PINETIME \
    -DRIOT_CPU=CPU_NRF52 \
    -DRIOT_MCU=MCU_NRF52 \
    -std=c99 \
    -fno-common

From scripts/gen-bindings.sh

After --, we add the same gcc options we would use for compiling the Embedded C code (for RIOT in this case)...

Take a peek at the complete list of bindgen options we used to create Rust Bindings for the LVGL library: gen-bindings.sh

How did we get that awfully long list of bindgen options?

When we build the Embedded C code with make --trace, we'll see the options passed to gcc. These are the options that we should pass to bindgen as well.

14 Whitelist and Blacklist C Types and Functions in bindgen

To build Watch Faces on PineTime Smart Watch, we need to call two groups of functions in LVGL...

  1. Base Object Functions lv_obj_*: Set the width and height of Widgets (like Labels). Also to create the Screen object. Defined in lv_obj.h

  2. Label Functions lv_label_*: Create Label Widgets and set the text of the Labels. Defined in lv_label.h

To call both groups of functions from Rust, we need to run bindgen twice...

# Generate Rust Bindings for LVGL Base Object Functions lv_obj_*
bindgen lv_obj.h   -o obj.rs   -- -Ibaselibc/include/ ...

# Generate Rust Bindings for LVGL Label Functions lv_label_*
bindgen lv_label.h -o label.rs -- -Ibaselibc/include/ ...

There's a problem with duplicate definitions... Do you see the problem?

lv_label.h includes lv_obj.h. So bindgen helpfully creates Rust Bindings for the Base Object Functions twice: In obj.rs and again in label.rs

The Rust Compiler is not gonna like this. To solve this, we Whitelist and Blacklist the items that we should include (Whitelist) and exclude (Blacklist)...

# Generate Rust Bindings for LVGL Base Object Functions lv_obj_*
bindgen lv_obj.h   -o obj.rs \
    --whitelist-function '(?i)lv_.*' \
    --whitelist-type     '(?i)lv_.*' \
    --whitelist-var      '(?i)lv_.*' \
    -- -Ibaselibc/include/ ...

# Generate Rust Bindings for LVGL Label Functions lv_label_*
bindgen lv_label.h -o label.rs \
    --whitelist-function '(?i)lv_label.*' \
    --whitelist-type     '(?i)lv_label.*' \
    --whitelist-var      '(?i)lv_label.*' \
    --blacklist-item     _lv_obj_t \
    --blacklist-item     lv_style_t \
    -- -Ibaselibc/include/ ...

whitelist-function, whitelist-type and whitelist-var tells bindgen to generate bindings only for C functions, types and variables that match a pattern.

blacklist-item tells bindgen to suppress bindings for functions, types and variables with that name.

(?i) tells bindgen to ignore the case and match both uppercase and lowercase versions of the name. More about Rust Regular Expressions

When we write...

bindgen lv_label.h -o label.rs \
    --blacklist-item _lv_obj_t

We tell bindgen not to create Rust Bindings for _lv_obj_t even though it's included by lv_label.h. This solves our problem of duplicate Rust Bindings. And the Rust Compiler loves us for doing that!

No more duplicate Rust Bindings!

When using bindgen in real projects we'll need to add more command-line options. Here's how we actually used bindgen to create the Rust Bindings in our PineTime Watch Face project...

# Generate Rust Bindings for LVGL Base Object Functions lv_obj_*
bindgen --verbose --use-core --ctypes-prefix ::cty --with-derive-default --no-derive-copy --no-derive-debug --no-layout-tests --raw-line use --raw-line 'super::*;' --whitelist-function '(?i)lv_.*' --whitelist-type '(?i)lv_.*' --whitelist-var '(?i)lv_.*' -o rust/lvgl/src/core/obj.tmp apps/pinetime/bin/pkg/pinetime/lvgl/src/lv_core/lv_obj.h -- -Ibaselibc/include/ ...

# Generate Rust Bindings for LVGL Label Functions lv_label_*
bindgen --verbose --use-core --ctypes-prefix ::cty --with-derive-default --no-derive-copy --no-derive-debug --no-layout-tests --raw-line use --raw-line 'super::*;' --whitelist-function '(?i)lv_label.*' --whitelist-type '(?i)lv_label.*' --whitelist-var '(?i)lv_label.*' --blacklist-item _lv_obj_t --blacklist-item lv_style_t -o rust/lvgl/src/objx/label.tmp apps/pinetime/bin/pkg/pinetime/lvgl/src/lv_objx/lv_label.h -- -Ibaselibc/include/ ...

The shell script used to create the Rust Bindings is here: gen-bindings.sh

Here's the output log for the script: gen-bindings.log

15 Safe Wrappers for Imported C Functions

To display the current time in our PineTime Watch Face, we need to call lv_label_set_text imported from the LVGL library...

//  In Rust: Import function lv_label_set_text from C
extern "C" {
    fn lv_label_set_text(label: *mut lv_obj_t, text: *const u8);
}
...
//  Set the text of the label to "00:00"
unsafe {
    lv_label_set_text(
        label1,
        b"00:00\0".as_ptr()
    );
}

It's not surprising that the Rust Compiler considers this code unsafe... If we forget to add the terminating null \0, lv_label_set_text might behave strangely and cause our watch to crash!

Can we exploit the power of Type Checking in the Rust Compiler to make this code safer?

Yes we can! Check this out...

//  In Rust: Wrapper function to set the text of a label
fn set_text(label: *mut lv_obj_t, text: &Strn) {
    text.validate();  //  Validate that the string is null-terminated
    unsafe {
        lv_label_set_text(
            label,
            text.as_ptr()
        );
    }
}
...
//  Set the text of the label to "00:00", the safe way
set_text(
    label1,
    strn!("00:00")
);

From logs/liblvgl-expanded.rs

set_text is a Wrapper Function that provides a safe way to call lv_label_set_text

Now we simply call set_text instead of lv_label_set_text... No more unsafe code!

Instead of passing unsafe C pointers to the text string, we now pass an Strn object.

Strn is a Rust Struct that we have defined to pass null-terminated strings to C functions. We create an Strn object with the Rust Macro strn!...

strn!("00:00")

Note the validation done in the Wrapper Function...

//  Wrapper function to set the text of a label
fn set_text(label: *mut lv_obj_t, text: &Strn) {
    text.validate();  //  Validate that the string is null-terminated
    ...

The Wrapper Function always checks to ensure that the string is null-terminated before calling the C function. Crashing Watches Averted!

But do we need to write this Wrapper Function ourselves for every C function?

Not necessary... The Safe Wrappers may be automatically generated! Let's learn how with a Rust Procedural Macro.

16 Generate Safe Wrappers with Rust Procedural Macro

As we have seen, to create a PineTime Watch Face we need to...

  1. Run bindgen to import the LVGL function lv_label_set_text from C into Rust

  2. Create a Safe Wrapper function in Rust to call lv_label_set_text safely

Trick Question: What's the difference between this Rust Binding code generated by bindgen...

//  In Rust: Import function lv_label_set_text from C to set the text of a label
#[lvgl_macros::safe_wrap(attr)]
extern "C" {
    pub fn lv_label_set_text(
        label: *mut lv_obj_t, 
        text:  *const ::cty::c_char
    );
}

From rust/lvgl/src/objx/label.rs

And this Safe Wrapper function (that calls lv_label_set_text safely)?

//  In Rust: Safe Wrapper function to set the text of a label
pub fn set_text(
    label: *mut lv_obj_t, 
    text:  &Strn
) -> LvglResult< () > {
    extern "C" {
        pub fn lv_label_set_text(
            label: *mut lv_obj_t,
            text:  *const ::cty::c_char
        );
    }
    text.validate();  //  Validate that the string is null-terminated
    unsafe {
        lv_label_set_text(
            label as *mut lv_obj_t,
            text.as_ptr() as *const ::cty::c_char
        );
        Ok(())  //  Return OK
    }
}

From logs/liblvgl-expanded.rs

Answer: They are exactly the same!

The magic happens in this line of code...

#[lvgl_macros::safe_wrap(attr)]

This activates a Rust Procedural Macro safe_wrap that we have written. The Rust Compiler calls our Rust function safe_wrap during compilation (instead of runtime). safe_wrap is defined here

Unlike C Macros, Rust Macros are allowed to inspect the Rust code passed to the macro... And alter the code!

So this whole chunk of Rust code...

extern "C" {
    pub fn lv_label_set_text(
        label: *mut lv_obj_t, 
        text:  *const ::cty::c_char
        ...

Gets passed into our safe_wrap function for us to manipulate!

  1. safe_wrap inspects the imported function name (lv_label_set_text), parameter types (lv_obj_t, c_char) and return type (none)

  2. Then safe_wrap replaces the chunk of code by the Safe Wrapper function set_text, populated with the right parameter types and return type

  3. *const ::cty::c_char (pointer to a C string, which may or may not be null-terminated) is replaced by the safer &Strn (reference to a null-terminated string object)

That's how we automatically generate Safe Wrapper functions (described in the previous section)... For every imported LVGL function.

safe_wrap is inserted into the Rust Bindings by the gen-bindings.sh script.

What's LvglResult< () > and Ok(())?

We'll find out in the next section: Rust Error Handling.

17 Return Errors with the Rust Result Enum

Error Handling in C is kinda messy. Here's a problem that we see often in C...

//  In C: Declare lv_obj_create function that creates a LVGL object
lv_obj_t *lv_obj_create(lv_obj_t *parent, const lv_obj_t *copy);
...
//  Create a screen object
lv_obj_t *screen = lv_obj_create(NULL, NULL); 
//  Get the coordinates of the screen object
lv_area_t coords = screen->coords;
//  Oops! This crashes if screen is NULL

This C code failed to check the value returned by lv_obj_create. The program crashes if the returned screen is NULL.

In Rust, we use the Result Enum to ensure that all returned values are checked.

Here's a Safe Wrapper Function create that exposes a safer version of lv_obj_create. The Safe Wrapper Function uses the Result Enum to enforce checking of returned values...

//  In Rust: Import from C the lv_obj_create function that creates a LVGL object
extern "C" {
    pub fn lv_obj_create(parent: *mut lv_obj_t, copy: *const lv_obj_t)
        -> *mut lv_obj_t;
}

//  Safe Wrapper function to create a LVGL object
pub fn create(parent: *mut lv_obj_t, copy: *const lv_obj_t) 
    -> LvglResult< *mut lv_obj_t > {  //  Returns a lv_obj_t pointer wrapped in a Result Enum
    unsafe {
        //  Create the object by calling the imported C function
        let result = lv_obj_create(parent, copy);
        //  If result is null, return an error
        if result.is_null() { Err( LvglError::SYS_EUNKNOWN ) }
        //  Otherwise return the wrapped result
        else { Ok( result ) }
    }
}

Based on logs/liblvgl-expanded.rs

What happens in the create Safe Wrapper Function?

  1. Note that the return type of the create function has been changed from *mut lv_obj_t (mutable pointer to lv_obj_t) to...

    LvglResult< *mut lv_obj_t >

    LvglResult is a Result Enum that we have created to wrap safely all values returned by the LVGL C library.

    LvglResult< *mut lv_obj_t > says that the returned LvglResult Enum will wrap a mutable pointer to lv_obj_t.

  2. Unlike C Enums, Rust Enums like LvglResult can have values inside. The expansion of LvglResult< *mut lv_obj_t > looks something like this...

    enum LvglResult< *mut lv_obj_t > {
        Ok( *mut lv_obj_t ),
        Err( LvglError ),
    }  //  This is not valid Rust syntax
  3. The LvglResult Enum has two variants: Ok and Err. To return an error, we return the Err variant with an error code inside (like SYS_EUNKNOWN)...

    //  Create the object by calling the imported C function
    let result = lv_obj_create(parent, copy);
    //  If result is null, return an error
    if result.is_null() { Err( LvglError::SYS_EUNKNOWN ) }

    Here we return an Err if the call to lv_obj_create returns NULL.

  4. To return a valid result, we return the Ok variant with the result value inside...

    //  Otherwise return the wrapped result
    else { Ok( result ) }

    Here we return the result of the call to lv_obj_create, since it's not NULL.

  5. The if else syntax used above looks odd if you're new to Rust...

    if condition { true_value } 
    else { false_value }

    In Rust, if else evaluates to a value. So the above Rust code is equivalent to this C code with the Ternary Operator...

    condition ? true_value : false_value
    

In summary: The create function calls the C function lv_obj_create. If the C function returns NULL, create returns Err. Otherwise create returns Ok with the result value inside.

All calls to the create function must be checked for errors. Let's find out how the Rust Compiler enforces the error checking...

18 Check Errors with the Rust Result Enum

Let's learn how the Rust Compiler forces us to check for errors returned by C functions. We'll use this Safe Wrapper function that we have created in the last section...

//  In Rust: Safe Wrapper function to create a LVGL object
pub fn create(parent: *mut lv_obj_t, copy: *const lv_obj_t) 
    -> LvglResult< *mut lv_obj_t > {  //  Returns an lv_obj_t pointer wrapped in a Result Enum
    ...

The create function returns LvglResult< *mut lv_obj_t > which is a Result Enum that's either...

  1. Ok with an lv_obj_t pointer wrapped inside, or

  2. Err with an error code wrapped inside

Let's try calling create without checking the result...

//  In Rust: Create a LVGL screen object
let screen = create(ptr::null_mut(), ptr::null());
//  Get a reference to the coordinates of the screen object
let coords = &(*screen).coords;
//  Oops! Rust Compiler says result cannot be dereferenced

The C Compiler would happily accept code like this... But not Rust!

screen has become a Result Enum that can't be used directly. The Rust Compiler insists that we check for error in screen before unwrapping it, like this...

//  In Rust: We specify `unsafe` to dereference the pointer in `screen`
unsafe {
    //  Create a LVGL screen object and unwrap it
    let screen = create(ptr::null_mut(), ptr::null())
        .expect("no screen");  //  If error, show "no screen" and stop
    //  Get a reference to the coordinates of the screen object
    let coords = &(*screen).coords;

By adding .expect after create, we check for error before unwrapping the pointer inside the result.

If create returns an error, the program stops with the error "no screen"

There's a simpler way to handle errors in Rust... With the Try Operator "?"

//  In Rust: Create a LVGL screen object and check for error
fn create_screen() -> LvglResult< () > {  //  Returns Ok (with nothing inside) or Err
    //  We specify `unsafe` to dereference the pointer in `screen`
    unsafe {
        //  Create a LVGL screen object and unwrap it
        let screen = create(ptr::null_mut(), ptr::null()) ? ;  //  If error, stop and return the Err
        //  Get a reference to the coordinates of the screen object
        let coords = &(*screen).coords;
        ...
        Ok( () )  //  Return Ok with nothing inside
    }
}

Note that .expect has been replaced by "?"...

//  Create a LVGL screen object and unwrap it
let screen = create(ptr::null_mut(), ptr::null()) ? ;  //  If error, stop and return the Err

If create returns Ok, the result is unwrapped and assigned to screen.

But if create returns Err, the result is returned to the caller of create_screen immediately.

That's why "?" works only inside a function that returns a Result Enum...

//  Create a LVGL screen object and check for error
fn create_screen() -> LvglResult< () > {  //  Returns Ok (with nothing inside) or Err

The () in LvglResult< () > means "nothing". Thus create_screen returns either...

  1. Ok with nothing () wrapped inside (because we don't need the return value), or

  2. Err with an error code wrapped inside

If we look at the Safe Wrappers created by our safe_wrap macro, it's now obvious why we see so many LvglResult< () > and Ok( () ) inside... That's how we handle errors in Rust.

19 C to Rust Conversion: Final Version

Now that we understand unsafe code, Safe Wrappers, Result Enums and "?", this Rust code should make sense...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(home_time_widget_t *ht) {

   // Create a label for time (00:00)
   lv_obj_t *scr = lv_obj_create(NULL, NULL);
   lv_obj_t *label1 = lv_label_create(scr, NULL);

   lv_label_set_text(label1, "00:00");
   lv_obj_set_width(label1, 240);
   lv_obj_set_height(label1, 200);
   ht->lv_time = label1;
   ...
   return scr;
}
fn create_widgets(widgets: &mut WatchFaceWidgets) ->
   LvglResult<()> {

   // Create a label for time (00:00)
   let scr = widgets.screen;
   let label1 = label::create(scr, ptr::null()) ? ;

   label::set_text(label1, strn!("00:00")) ? ;
   obj::set_width(label1, 240) ? ;
   obj::set_height(label1, 200) ? ;
   widgets.time_label = label1;
   ...
   Ok(())
}
From widgets/home_time/screen_time.cFrom rust/app/src/watch_face.rs

create, set_text, set_width and set_height are Safe Wrapper functions, automatically generated by our safe_wrap macro.

For clarity, we have segregated the Safe Wrappers by module, hence we see module names like obj:: and label::

Note that parameter type and return type have been changed...

Original C CodeConverted Rust Code
lv_obj_t *screen_time_create(home_time_widget_t *ht) {
   // Create a label for time (00:00)
   lv_obj_t *scr = lv_obj_create(NULL, NULL);
fn create_widgets(widgets: &mut WatchFaceWidgets) ->
   LvglResult<()> {
   // Create a label for time (00:00)
   let scr = widgets.screen;

We'll learn in a while why this was done: To make the code easier to maintain.

Rust on RIOT

20 Heapless Strings in Rust

Let's look at the C code for displaying the current time on PineTime Smart Watch. It calls snprintf to format the current time into a string buffer on the stack. Then it calls lv_label_set_text to set the text on the LittlebGL Label...

/// In C: Populate the LVGL Time Label with the current time
static int set_time_label(home_time_widget_t *ht) {
    //  Create a string buffer on the stack with max size 6 to format the time
    char time[6];
    //  Format the time HH:MM into the string buffer
    int res = snprintf(
        time, 
        sizeof(time), 
        "%02u:%02u", 
        ht->time.hour,
        ht->time.minute
    );
    if (res != sizeof(time) - 1) {
        LOG_ERROR("[home_time]: error formatting time string %*s\n", res, time);
        return -1;  //  Return error to caller
    }
    //  Display the formatted time on the LVGL label
    lv_label_set_text(
        ht->lv_time, 
        time
    );
    return 0;  //  Return Ok
}

From widgets/home_time/screen_time.c

Here's the equivalent code in Rust...

/// In Rust: Populate the LVGL Time Label with the current time
fn set_time_label(widgets: &WatchFaceWidgets, state: &WatchFaceState) -> LvglResult<()> {  //  If error, return Err with error code inside
    //  Create a heapless string buffer on the stack with max size 6 to format the time
    type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
    let mut time_buf: heapless::String::<TimeBufSize> = 
        heapless::String::new();
    //  Format the time HH:MM into the string buffer
    write!(                 //  Macro writes a formatted string...
        &mut time_buf,      //  Into this buffer...
        "{:02}:{:02}\0",    //  With this format... (Must terminate Rust strings with null)
        state.time.hour,    //  With this hour value...
        state.time.minute   //  And this minute value
    ).expect("time fail");  //  Fail if the buffer is too small
    //  Display the formatted time on the LVGL label
    label::set_text(
        widgets.time_label, 
        &Strn::new( time_buf.as_bytes() )  //  Verifies that the string is null-terminated
    ) ? ;   //  If error, return Err to caller
    Ok(())  //  Return Ok
}

Based on rust/app/src/watch_face.rs

Why do we use heapless::String instead of the usual String type in Rust?

The usual String type in Rust uses Heap Memory... It allocates memory dynamically to store strings. But we don't allow Heap Memory in our Rust program.

heapless::String is a Heapless String that doesn't use Heap Memory. It uses a fixed-size array stored on the stack (like above) or stored in Static Memory.

Why can't we use Heap Memory?

When writing embedded programs, it's good to budget in advance the memory needed to run the program and preallocate the memory needed from Static Memory. So that our program won't run out of Heap Memory while running and fail.

Heap Fragmentation may also cause our programs to behave erratically.

Why does the usual String type in Rust use Heap Memory?

Using strings safely in C is hard... We have to watch the string size very carefully and make sure the strings don't overflow.

Rust makes string programming easier and safer... Rust Strings will grow dynamically when they run out of space! Unfortunately this means that Rust Strings need Heap Memory to make them grow. Which is a problem for embedded programs.

Here's how we allocate a Heapless String on the stack...

//  Create a heapless string buffer on the stack with max size 6 to format the time
type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
let mut time_buf: heapless::String::<TimeBufSize> = 
    heapless::String::new();

let mut works like let, except that it declares a mutable variable on the stack whose value may change.

//  Format the time HH:MM into the string buffer
write!(                 //  Macro writes a formatted string...
    &mut time_buf,      //  Into this buffer...
    "{:02}:{:02}\0",    //  With this format... (Must terminate Rust strings with null)
    state.time.hour,    //  With this hour value...
    state.time.minute   //  And this minute value
).expect("time fail");  //  Fail if the buffer is too small

write! is a Rust Macro that writes formatted strings into a string buffer. It's a macro, not a function, so that the paramaters are validated against the specified format at compile-time.

The Rust function set_time_label above looks OK. But there's a problem with this line of code...

//  Display the formatted time on the LVGL label
label::set_text(
    widgets.time_label, 
    &Strn::new( time_buf.as_bytes() )  //  Verifies that the string is null-terminated
) ? ;   //  If error, return Err to caller

Do you see the problem?

The problem becomes obvious when we learn in a while about the Lifetime of Rust variables.

Rust on RIOT: C vs Rust

21 Lifetime of Rust Variables

In the last section we attempted to display the current time on PineTime Smart Watch inside a LVGL Widget (which we have imported from C). We allocated a Heapless String on the stack...

//  In Rust: Create a heapless string buffer on the stack with max size 6 to format the time
type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
let mut time_buf: heapless::String::<TimeBufSize> = 
    heapless::String::new();

Then we formatted the current time into the Heapless String...

//  In Rust: Format the time HH:MM into the string buffer
write!(                 //  Macro writes a formatted string...
    &mut time_buf,      //  Into this buffer...
    "{:02}:{:02}\0",    //  With this format... (Must terminate Rust strings with null)
    state.time.hour,    //  With this hour value...
    state.time.minute   //  And this minute value
).expect("time fail");  //  Fail if the buffer is too small

And we passed the formatted time in the Heapless String to set_text to set the label text...

//  In Rust: Display the formatted time on the LVGL label
label::set_text(
    widgets.time_label, 
    &Strn::new( time_buf.as_bytes() )  //  Verifies that the string is null-terminated
) ? ;   //  If error, return Err to caller

set_text is a Safe Wrapper for the LVGL function lv_label_set_text that we have imported from C into Rust.

When we compile this code, the Rust Compiler draws a neat line diagram to point out a cryptic error...

error[E0597]: `time_buf` does not live long enough
  --> rust/app/src/watch_face.rs:25:52
   |
25 |     label::set_text(widgets.time_label, &Strn::new(time_buf.as_bytes())) ? ;
   |                                                    ^^^^^^^^-----------
   |                                                    |
   |                                                    borrowed value does not live long enough
   |                                                    argument requires that `time_buf` is borrowed for `'static`
26 |     Ok(())
27 | }
   | - `time_buf` dropped here while still borrowed

Borrowed value does not live long enough... What is the meaning of this?

Let's look at the declaration of the C function lv_label_set_text (from which set_text was derived)...

//  In C: Declare function lv_label_set_text to set the text of a label
void lv_label_set_text(lv_obj_t *label, const char *text);

lv_label_set_text (same for set_text) sets the text on a LVGL label to a string text that's passed to the function.

Question: What happens to the string in text AFTER the function lv_label_set_text returns?

What if lv_label_set_text has lazily copied the string pointer (instead of the string contents)?

Will some other LVGL function read the string pointer later?

Well this will be a problem... If our string buffer was allocated on the stack!

Stack variables will magically disappear when we return from the function. If a LVGL function attempts to read the string buffer previously allocated on the stack... Strange things will happen!

But that's exactly what we did: Allocate the string buffer on the stack...

//  In Rust: Create a heapless string buffer on the stack with max size 6 to format the time
type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
let mut time_buf: heapless::String::<TimeBufSize> = 
    heapless::String::new();

So the Rust Compiler helpfully warns us that somebody could be using later the string buffer that we have passed to C. And that it's not safe to pass a string buffer on the stack. Let's reword it like this...

  1. Our string buffer lives on the stack. It disappears when the function returns.

  2. Thus our string buffer has a very short Lifetime... It's not meant to be used for a long time.

  3. But we passed the string buffer to the C function lv_label_set_text (via the Safe Wrapper set_text)

  4. The Rust Compiler doesn't know the expected Lifetime of the string buffer used by lv_label_set_text... The string buffer may still be used for a long time afterwards

  5. Hence the Rust Compiler warns that the string buffer might not live long enough to satisfy lv_label_set_text

The Rust Compiler is really that clever! These are typical bugs that we tend to miss in C... Passing values on the stack when we're not supposed to. Which won't happen in Rust since the Lifetimes of variables will have to be stated clearly.

FYI: The Lifetime of lv_label_set_text is stated verbally in the LVGL docs... lv_label_set_text will copy the contents of the string buffer, instead of copying the string pointer. Therefore the string buffer passed to lv_label_set_text is expected to have a short Lifetime.

There are two solutions to our Lifetime problem...

  1. Tell the Rust Compiler the expected Lifetime of the string buffer in lv_label_set_text. (Using Lifetime specifiers like 'static, which is kinda complicated for newbies)

  2. Or make our string buffer live forever! When we turn our Stack Variable into a Static Variable, the string buffer outlives lv_label_set_text. And makes the Rust Compiler very happy!

We'll learn about Static Variables next...

Safer Rust on RIOT

22 Static Variables in Rust

Creating Static Variables in C is easy...

/// In C: Populate the LVGL Time Label with the current time
static int set_time_label(home_time_widget_t *ht) {
    //  Create a string buffer in static memory with max size 6 to format the time
    static char time[6];

Based on widgets/home_time/screen_time.c

Here we allocate a 6-byte string buffer in Static Memory to format the time for display on PineTime.

What's the initial value of time?

Static Memory (also known as BSS) is implicitly initialised with null bytes. So time is initially set to 6 bytes of null. Which also represents an empty string "" in C (since C strings are terminated by null).

Let's do the same in Rust... Watch how Rust cares about our code safety. Here's our original Rust code that allocates the string buffer on the stack...

/// In Rust (Stack Version): Populate the LVGL Time Label with the current time
fn set_time_label(widgets: &WatchFaceWidgets, state: &WatchFaceState) -> LvglResult<()> {  //  If error, return Err with error code inside
    //  Create a heapless string buffer on the stack with max size 6 to format the time
    type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
    let mut time_buf: heapless::String::<TimeBufSize> = 
        heapless::String::new();

And now we allocate the string buffer in Static Memory...

/// In Rust (Static Version): Populate the LVGL Time Label with the current time
fn set_time_label(widgets: &WatchFaceWidgets, state: &WatchFaceState) -> LvglResult<()> {  //  If error, return Err with error code inside
    //  Create a heapless string buffer in static memory with max size 6 to format the time
    type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
    static mut TIME_BUF: heapless::String::<TimeBufSize> = 
        heapless::String( heapless::i::String::new() );    

let mut has been changed to static mut. This looks very similar to C, piece of cake!

Then comes the initialisation...

//  In Rust: Initialise the string buffer
static mut TIME_BUF: heapless::String::<TimeBufSize> = 
    heapless::String( heapless::i::String::new() );    

In Rust, all Static Variables must be initialised explicitly... Rust doesn't allow implicit initialisation like in C!

This prevents initialisation errors that we see in C (phew!)

Note that the initial value has been changed from heapless::String::new() to...

heapless::String( heapless::i::String::new() )

That's the proper way to initialise a Heapless String Static Variable, according to the docs. (And if you think carefully, there's a very good reason why the value looks different)

Here's the entire function that creates a string buffer in Static Memory and uses the buffer...

/// In Rust (Static Version): Populate the LVGL Time Label with the current time
fn set_time_label(widgets: &WatchFaceWidgets, state: &WatchFaceState) -> LvglResult<()> {  //  If error, return Err with error code inside
    //  Create a heapless string buffer in static memory with max size 6 to format the time
    type TimeBufSize = heapless::consts::U6;  //  Size of the string buffer
    static mut TIME_BUF: heapless::String::<TimeBufSize> = 
        heapless::String( heapless::i::String::new() );    
    //  This code is unsafe because multiple threads may be updating the string buffer
    unsafe {
        TIME_BUF.clear();       //  Erase the string buffer
        //  Format the time HH:MM into the string buffer
        write!(                 //  Macro writes a formatted string...
            &mut TIME_BUF,      //  Into this buffer...
            "{:02}:{:02}\0",    //  With this format... (Must terminate Rust strings with null)
            state.time.hour,    //  With this hour value...
            state.time.minute   //  And this minute value
        ).expect("time fail");  //  Fail if the buffer is too small
        //  Display the formatted time on the LVGL label
        label::set_text(
            widgets.time_label, 
            &Strn::new( TIME_BUF.as_bytes() )  //  Verifies that the string is null-terminated
        ) ? ;  //  If error, return Err to caller
    }          //  End of unsafe code
    Ok(())     //  Return Ok
}

From rust/app/src/watch_face.rs

Why is the code marked unsafe?

Unlike C, Rust is fully aware of multithreading... Using multiple threads to run code simultaneously.

When two threads read and write to the same Static Variable (like TIME_BUF), we will get inconsistent results (unless we do some locking).

Thus we need to flag the code as unsafe to say...

Dear Rust Compiler: Thank you for warning us that Mutable Statics like TIME_BUF can be mutated by multiple threads and cause undefined behavior. We promise to take responsibility for any unsafe consequences. We hope you're happy now.

But is this code really unsafe? Will we have multiple threads running the same code concurrently?

Actually the code above will only be executed by a single thread... RIOT assures this on our PineTime Smart Watch.

The Rust Compiler doesn't know anything about RIOT. That's why we need to flag the code as unsafe and tell the compiler that it's really OK.

If there's a possibility that multiple threads will run the code, we will need to use the Thread Synchronisation functions provided by RIOT.

23 Simplify Strings

In the code that we have seen, declaring and creating string buffers look cumbersome...

//  In Rust: Initialise the string buffer
static mut TIME_BUF: heapless::String::<TimeBufSize> = 
    heapless::String( heapless::i::String::new() );    

Let's simplify this! For a watch face we probably don't need to handle strings longer than 64 characters. Let's define our own String type limited to 64 characters...

/// Limit Strings to 64 chars (which may include multiple color codes like "#ffffff")
type String = heapless::String::<heapless::consts::U64>;

From rust/app/src/watch_face.rs

Note that the Rust Standard Library already defines a String type that uses Heap Memory. But it should be OK for us to redefine the String type because...

  1. We're using the Rust Core Library (no_std) instead of the Rust Standard Library. Rust Core Library doesn't define the String type.

  2. Our String type works with the same functions in the standard String type, thanks to heapless

Let's add some helper functions...

/// Create a new String
const fn new_string() -> String {
    heapless::String(heapless::i::String::new())
}

/// Convert a static String to null-terminated Strn
fn to_strn(str: &'static String) -> Strn {
    Strn::new(str.as_bytes())
}

From rust/app/src/watch_face.rs

Now our Rust code becomes a lot simpler...

/// Populate the Time and Date Labels with the time and date. Called by screen_time_update_screen() above.
pub fn set_time_label(widgets: &WatchFaceWidgets, state: &WatchFaceState) -> LvglResult<()> {
    //  Create a string buffer to format the time
    static mut TIME_BUF: String = new_string();
    //  Format the time and set the label
    unsafe {  //  Unsafe because TIME_BUF is a mutable static
        TIME_BUF.clear();
        write!(
            &mut TIME_BUF, 
            "{:02}:{:02}\0",  //  Must terminate Rust strings with null
            state.time.hour,
            state.time.minute
        ).expect("time fail");
        label::set_text(
            widgets.time_label, 
            &to_strn(&TIME_BUF)
        ) ? ;
    }
    Ok(())
}

From rust/app/src/watch_face.rs

24 VSCode Development and Debugging

The repository pinetime-rust-riot has been configured to work with VSCode. Just open the VSCode Workspace workspace.code-workspace

The VSCode Workspace contains tasks for building and flashing RIOT to PineTime on macOS and Linux. Refer to the tasks Build Application and Flash Application in .vscode/tasks.json

The VSCode Debugger (with the Cortex Debug Extension) has been configured to flash and debug RIOT on PineTime with ST-Link v2. See the debugger configuration in .vscode/launch.json

Video Demo of VSCode Debugger with RIOT on PineTime

25 RIOT Development on Windows

Building RIOT natively on Windows (without WSL and MinGW) can be difficult. We recommend building RIOT in the GitHub Cloud with GitHub Actions. Then download the built binary Pinetime.elf for flashing and debugging with VSCode.

Check out the GitHub Actions Workflow for Rust on RIOT: .github/workflows/main.yml

Rust on RIOT WebAssembly Simulator

26 WebAssembly Simulator

For quicker development, we have a WebAssembly Simulator that will preview Rust on RIOT Watch Faces in a web browser...

Online Demo of WebAssembly Simulator

The repository pinetime-rust-riot auto-builds the WebAssembly Simulator with GitHub Actions whenever source files are changed.

To set up the WebAssembly Simulator for Rust on RIOT, see pinetime-rust-riot/README.md

The WebAssembly Simulator is built according to the following GitHub Actions Workflow...

.github/workflows/simulate.yml

More about WebAssembly Simulator for Rust on RIOT

Rust on RIOT Design

27 LVGL and RIOT Bindings for Rust

Today our Rust code uses a custom-generated Rust Safe Wrapper for the LVGL Libray. The wrapper supports a subset of the LVGL functions. In future we should migrate the LVGL wrapper to this wrapper that is properly maintained...

github.com/rafaelcaricio/lvgl-rs

To call the RIOT API from Rust, we could use one of the following Rust Wrappers for RIOT...

gitlab.com/etonomy/riot-sys

gitlab.com/etonomy/riot-wrappers

Druid with LVGL

28 Declarative User Interfaces with Rust and LVGL

Can we create Declarative User Interfaces in Rust and LVGL?

So instead of specifying precise row and column pixel positions of each widget... We just lay them out in a table like HTML?

We're extending the Druid UI library so that it renders with LVGL widgets...

github.com/AppKaki/druid-lvgl

Remote PineTime

29 What's Next

How to test your firmware without a PineTime? We now have a Remote PineTime that you may flash your firmware remotely (by sending Telegram Bot commands) and watch the live video stream (on YouTube).

Check out Remote PineTime

In this article I have demonstrated that it's not that hard to convert C code to Rust... Even for coding watch faces on smart watches.

Rust on RIOT has great potential to become the safer, modern replacement for Arduino!

But there's much to be done to fill in the gaps... To make Rust on RIOT really usable by beginners. Please chat with me on Matrix / Discord / Telegram / IRC if you're keen to help!

PineTime Community

30 References

For more about Rust on RIOT, check out the presentation at RIOT Summit 2020...

Safer, Simpler Embedded Programs with Rust on RIOT

Video Presentation at RIOT Summit

Check out my articles on PineTime and IoT...

My Articles

My RSS Feed