docs(micropython): add coding conventions section(#3774)

Co-authored-by: Gabor Kiss-Vamosi <kisvegabor@gmail.com>
This commit is contained in:
Amir Gonnen 2022-11-02 19:33:45 +02:00 committed by GitHub
parent be1e1fca3a
commit 35e1402f1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -18,7 +18,7 @@ Using Micropython, you can write Python3 code and run it even on a bare metal ar
## Why Micropython + LVGL?
Currently, Micropython [does not have a good high-level GUI library](https://forum.micropython.org/viewtopic.php?f=18&t=5543) by default. LVGL is an [Object-Oriented Component Based](https://blog.lvgl.io/2018-12-13/extend-lvgl-objects) high-level GUI library, which seems to be a natural candidate to map into a higher level language, such as Python. LVGL is implemented in C and its APIs are in C.
Micropython [does not have a good native high-level GUI library](https://forum.micropython.org/viewtopic.php?f=18&t=5543). LVGL is an [Object-Oriented Component Based](https://blog.lvgl.io/2018-12-13/extend-lvgl-objects) high-level GUI library, which seems to be a natural candidate to map into a higher level language, such as Python. LVGL is implemented in C and its APIs are in C.
### Here are some advantages of using LVGL in Micropython:
@ -39,8 +39,7 @@ This goes well with [CircuitPython vision](https://learn.adafruit.com/welcome-to
## So what does it look like?
> TL;DR:
> It's very much like the C API, but Object-Oriented for LVGL components.
**TL;DR:** It's very much like the C API, but Object-Oriented for LVGL components.
Let's dive right into an example!
@ -51,9 +50,9 @@ import lvgl as lv
lv.init()
scr = lv.obj()
btn = lv.btn(scr)
btn.align(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
btn.align(lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("Button")
label.set_text('Hello World!')
lv.scr_load(scr)
```
@ -66,9 +65,9 @@ It's a fully functional LVGL + Micropython that runs entirely in the browser and
[Click here to experiment on the online simulator](https://sim.lvgl.io/)
[Hello World](https://sim.lvgl.io/v7/micropython/ports/javascript/bundle_out/index.html?script=https://gist.githubusercontent.com/amirgon/51299ce9b6448328a855826149482ae6/raw/0f235c6d40462fd2f0e55364b874f14fe3fd613c/lvgl_hello_world.py&script_startup=https://gist.githubusercontent.com/amirgon/7bf15a66ba6d959bbf90d10f3da571be/raw/8684b5fa55318c184b1310663b187aaab5c65be6/init_lv_mp_js.py)
Note: the online simulator is available for lvgl v6 and v7.
Many [LVGL examples](https://docs.lvgl.io/master/examples.html) are available also for Micropython.
Just click the
<img src="https://user-images.githubusercontent.com/11742638/198729010-35a12e49-4945-414a-8c3e-d32bc95da940.png" align=middle /> link!
### PC Simulator
@ -76,17 +75,119 @@ Micropython is ported to many platforms. One notable port is "unix", which allow
[Click here to know more information about building and running the unix port](https://github.com/lvgl/lv_micropython)
### Embedded platform
### Embedded Platforms
In the end, the goal is to run it all on an embedded platform.
Both Micropython and LVGL can be used on many embedded architectures, such as stm32, ESP32 etc.
You would also need display and input drivers. We have some sample drivers (ESP32+ILI9341, as well as some other examples), but chances are you would want to create your own input/display drivers for your specific hardware.
Drivers can be implemented either in C as a Micropython module, or in pure Micropython!
Both Micropython and LVGL can be used on many embedded architectures. [lv_micropython](https://github.com/lvgl/lv_micropython) is a fork of Micropython+LVGL and currently supports Linux, ESP32, STM32 and RP2. It can be ported to any other platform supported by Micropython.
You would also need display and input drivers. You can either use one of the existing drivers provided with lv_micropython, or you can create your own input/display drivers for your specific hardware.
Drivers can be implemented either in C as a Micropython module, or in pure Python!
lv_micropython already contains these drivers:
- Display drivers:
- SDL on Linux
- ESP32 specific: ILI9341, ILI9488, GC9A01, ST7789, ST7735
- Generic (pure Python): ILI9341, ST7789, ST7735
- Input drivers:
- SDL, XPT2046, FT6X36, ESP32 ADC with resistive touch
## Where can I find more information?
- In this [Blog Post](https://blog.lvgl.io/2019-02-20/micropython-bindings)
- `lv_micropython` [README](https://github.com/lvgl/lv_micropython)
- `lv_binding_micropython` [README](https://github.com/lvgl/lv_binding_micropython)
- The [LVGL micropython forum](https://forum.lvgl.io/c/micropython) (Feel free to ask anything!)
- At Micropython: [docs](http://docs.micropython.org/en/latest/) and [forum](https://forum.micropython.org/)
- [Blog Post](https://blog.lvgl.io/2019-02-20/micropython-bindings), a little outdated.
# The Micropython Binding is auto generated!
LVGL is a git submodule inside [lv_micropython](https://github.com/lvgl/lv_micropython) (LVGL is a git submodule of [lv_binding_micropython](https://github.com/lvgl/lv_binding_micropython) which is itself a submodule of [lv_micropython](https://github.com/lvgl/lv_micropython)).
When building lv_micropython, the public LVGL C API is scanned and Micropython API is auto-generated. That means that lv_micropython provides LVGL API for **any** LVGL version, and generally does not require code changes as LVGL evolves.
## LVGL C API Coding Conventions
To support the auto-generation of the Python API, the LVGL C API must follow some coding conventions:
- Use `enum`s instead of macros. If inevitable to use `define`s export them with `LV_EXPORT_CONST_INT(defined_value)` right after the `define`.
- In function arguments use `type name[]` declaration for array parameters instead of `type * name`
- Use typed pointers instead of `void *` pointers
- Widget constructor must follow the `lv_<widget_name>_create(lv_obj_t * parent)` pattern.
- Widget members function must start with `lv_<modul_name>` and should receive `lv_obj_t *` as first argument which is a pointer to widget object itself.
- `struct` APIs should follow the widgets' conventions. That is to receive a pointer to the `struct` as the first argument, and the prefix of the `struct` name should be used as the prefix of the function name too (e.g. `lv_disp_set_default(lv_disp_t * disp)`)
- Functions and `struct`s which are not part of the public API must begin with underscore in order to mark them as "private".
- Argument must be named in H files too.
- Do not `malloc` into a static or global variables. Instead declare the variable in `LV_ITERATE_ROOTS` list in `lv_gc.h` and mark the variable with `GC_ROOT(variable)` when it's used.
**See [Memory Management](#memory-management)**
- To register and use callbacks one of the followings needs to be followed. **See [Callbacks](#callbacks)**
- Pass a pointer to a `struct` as the first argument of both the registration function and the callback. That `struct` must contain `void * user_data` field.
- The last argument of the registration function must be `void * user_data` and the same `user_data` needs to be passed as the last argument of the callback.
Most of these rules are simple and straightforward but there are two related concepts that worth a deeper look: **Memory Management** and **Callbacks**.
## Memory Management
When LVGL runs in Micropython, all dynamic memory allocations (`lv_malloc`) are handled by Micropython's memory manager which is [garbage-collected](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)) (GC).
To prevent GC from collecting memory prematurely, all dynamic allocated RAM must be reachable by GC.
GC is aware of most allocations, except from pointers on the [Data Segment](https://en.wikipedia.org/wiki/Data_segment):
- Pointers which are global variables
- Pointers which are static global variables
- Pointers which are static local variables
Such pointers need to be defined in a special way to make them reachable by GC
### Identify The Problem
Problem happens when an allocated memory's pointer (return value of `lv_malloc`) is stored only in either **global**, **static global** or **static local** pointer variable and not as part of a previously allocated `struct` or other variable.
### Solve The Problem
- Replace the global/static local var with `LV_GC_ROOT(_var)`
- Include `lv_gc.h` on files that use `LV_GC_ROOT`
- Add `_var` to `LV_ITERATE_ROOTS` on `lv_gc.h`
### Example
https://github.com/lvgl/lvgl/commit/adced46eccfa0437f84aa51aedca4895cc3c679c
### More Information
- [In the README](https://github.com/lvgl/lv_binding_micropython#memory-management)
- [In the Blog](https://blog.lvgl.io/2019-02-20/micropython-bindings#i-need-to-allocate-a-littlevgl-struct-such-as-style-color-etc-how-can-i-do-that-how-do-i-allocatedeallocate-memory-for-it)
## Callbacks
In C a callback is just a function pointer.
But in Micropython we need to register a *Micropython callable object* for each callback.
Therefore in the Micropython binding we need to register both a function pointer and a Micropython object for every callback.
Therefore we defined a **callback convention** for the LVGL C API that expects lvgl headers to be defined in a certain way. Callbacks that are declared according to the convention would allow the binding to register a Micropython object next to the function pointer when registering a callback, and access that object when the callback is called.
The basic idea is that we have `void * user_data` field that is used automatically by the Micropython Binding to save the *Micropython callable object* for a callback. This field must be provided when registering the function pointer, and provided to the callback function itself.
Although called "user_data", the user is not expectd to read/write that field. Instead, the Micropython glue code uses `user_data` to automatically keep track of the Micropython callable object. The glue code updates it when the callback is registered, and uses it when the callback is called in order to invoke a call to the original callable object.
There are a few options for defining a callback in LVGL C API:
- Option 1: `user_data` in a struct
- There's a struct that contains a field called `void * user_data`
- A pointer to that struct is provided as the **first** argument of a callback registration function
- A pointer to that struct is provided as the **first** argument of the callback itself
- Option 2: `user_data` as a function argument
- A parameter called `void * user_data` is provided to the registration function as the **last** argument
- The callback itself recieves `void *` as the **last** argument
- Option 3: both callback and `user_data` are struct fields
- The API exposes a struct with both function pointer member and `user_data` member
- The function pointer member receives the same struct as its **first** argument
In practice it's also possible to mix these options, for example provide a struct pointer when registering a callback (option 1) and provide `user_data` argument when calling the callback (options 2), **as long as the same `user_data` that was registered is passed to the callback when it's called**.
### Examples
- [lv_anim_t](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/misc/lv_anim.h#L73-L100) contains `user_data` field.
[lv_anim_set_path_cb](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/misc/lv_anim.h#L197) registers `path_cb` callback. Both `lv_anim_set_path_cb` and [`lv_anim_path_cb_t`](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/misc/lv_anim.h#L46) recieve `lv_anim_t` as their first argument
- [`path_cb` field](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/misc/lv_anim.h#L83) can also be assigned directly in the Python code because it's a member of `lv_anim_t` which contains `user_data` field, and [`lv_anim_path_cb_t`](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/misc/lv_anim.h#L46) recieve `lv_anim_t` as its first argument.
- [`lv_imgfont_create`](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/others/imgfont/lv_imgfont.h#L43) registers `path_cb` and recieves `user_data` as the last argument. The callback [`lv_imgfont_get_path_cb_t`](https://github.com/lvgl/lvgl/blob/5d50fbc066938d1a4eb43a8366cf83fbd4ce29f2/src/others/imgfont/lv_imgfont.h#L29-L31) also receieves the `user_data` as the last argument.
### More Information
- In the [Blog](https://blog.lvgl.io/2019-08-05/micropython-pure-display-driver#using-callbacks) and in the [README](https://github.com/lvgl/lv_binding_micropython#callbacks)
- [[v6.0] Callback conventions #1036](https://github.com/lvgl/lvgl/issues/1036)
- Various discussions: [here](https://github.com/lvgl/lvgl/pull/3294#issuecomment-1184895335) and [here](https://github.com/lvgl/lvgl/issues/1763#issuecomment-762247629) and [here](https://github.com/lvgl/lvgl/issues/316#issuecomment-467221587)