uStubby is a library for generating micropython c extension stubs from type annotated python.
According to Link
"MicroPython is a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library and is optimised to run on microcontrollers and in constrained environments."
Sometimes, pure python performance isn't enough. C extensions are a way to improve performace whilst still having the bulk of your code in micropython.
Unfortunately there is a lot of boilerplate code needed to build these extensions.
uStubby is designed to make this process as easy as writing python.
uStubby is targeted to run on Python 3.7, but should run on versions 3.6 or greater
Currently, there are no external dependencies for running uStubby. Install from PyPI with:
pip install ustubby
Alternatively, clone the repository using git and just put it on the path to install.
This example follows generating the template as shown here
Create a python file with the module as you intend to use it from micropython.
eg. example.py
def add_ints(a: int, b: int) -> int:
"""Adds two integers
:param a:
:param b:
:return:a + b"""
We can then convert this into the appropriate c stub via the CLI:
# This will generate the file "example.c"
ustubby example.py
Alternatively, you can invoke the python interface in a script:
import ustubby
import example
print(ustubby.stub_module(example))
Output
// Include required definitions first.
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
//Adds two integers
//:param a:
//:param b:
//:return:a + b
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
mp_int_t a = mp_obj_get_int(a_obj);
mp_int_t b = mp_obj_get_int(b_obj);
mp_int_t ret_val;
//Your code here
return mp_obj_new_int(ret_val);
}
MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table);
const mp_obj_module_t example_user_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t*)&example_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED);
This will parse all the functions in the module and attach them to the same namespace in micropython.
Note: It will only generate the boilerplate code and not the actual code that does the work such as a + b
After editing the code in the template at the place marked //Code goes here you can follow the instructions here for modifying the Make File and building the module into your micro python deployment.
You should then be able to use the module in micro python by typing
import example # from example.c compiled into micropython
example.add_ints(1, 2)
# prints 3
Note: This example.py is the one compiled into the micropython source and not the file we created earlier
If you added two more functions to the original example.py
def lots_of_parameters(a: int, b: float, c: tuple, d: object, e: str) -> None:
"""
:param a:
:param b:
:param c:
:param d:
:return:
"""
def readfrom_mem(addr: int = 0, memaddr: int = 0, arg: object = None, *, addrsize: int = 8) -> str:
"""
:param addr:
:param memaddr:
:param arg:
:param addrsize: Keyword only arg
:return:
"""
logs_of_parameters shows the types of types you can parse in. You always need to annotate each parameter and the return. readfrom_mem shows that you can set default values for certain parameters and specify that addrsize is a keyword only argument.
At the c level in micropython, there is only three ways of implementing a function.
def foo(a, b, c): # 0 to 3 args
pass
MP_DEFINE_CONST_FUN_OBJ_X // Where x is 0 to 3 args
def foo(*args):
pass
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN
def foo(*args, **kwargs):
pass
MP_DEFINE_CONST_FUN_OBJ_KW
Each successively increasing the boiler plate to conveniently accessing the variables.
Output
// Include required definitions first.
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
//Adds two integers
//:param a:
//:param b:
//:return:a + b
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
mp_int_t a = mp_obj_get_int(a_obj);
mp_int_t b = mp_obj_get_int(b_obj);
mp_int_t ret_val;
//Your code here
return mp_obj_new_int(ret_val);
}
MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
//
//:param a:
//:param b:
//:param c:
//:param d:
//:return:
//
STATIC mp_obj_t example_lots_of_parameters(size_t n_args, const mp_obj_t *args) {
mp_int_t a = mp_obj_get_int(a_obj);
mp_float_t b = mp_obj_get_float(b_obj);
mp_obj_t *c = NULL;
size_t c_len = 0;
mp_obj_get_array(c_arg, &c_len, &c);
mp_obj_t d args[ARG_d].u_obj;
//Your code here
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(example_lots_of_parameters_obj, 4, 4, example_lots_of_parameters);
//
//:param addr:
//:param memaddr:
//:param arg:
//:param addrsize: Keyword only arg
//:return:
//
STATIC mp_obj_t example_readfrom_mem(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_addr, ARG_memaddr, ARG_arg, ARG_addrsize };
STATIC const mp_arg_t example_readfrom_mem_allowed_args[] = {
{ MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } },
{ MP_QSTR_memaddr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } },
{ MP_QSTR_arg, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } },
{ MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, { .u_int = 8 } },
};
mp_arg_val_t args[MP_ARRAY_SIZE(example_readfrom_mem_allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,
MP_ARRAY_SIZE(example_readfrom_mem_allowed_args), example_readfrom_mem_allowed_args, args);
mp_int_t addr = args[ARG_addr].u_int;
mp_int_t memaddr = args[ARG_memaddr].u_int;
mp_obj_t arg = args[ARG_arg].u_obj;
mp_int_t addrsize = args[ARG_addrsize].u_int;
//Your code here
return mp_obj_new_str(<ret_val_ptr>, <ret_val_len>);
}
MP_DEFINE_CONST_FUN_OBJ_KW(example_readfrom_mem_obj, 1, example_readfrom_mem);
STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) },
{ MP_ROM_QSTR(MP_QSTR_lots_of_parameters), MP_ROM_PTR(&example_lots_of_parameters_obj) },
{ MP_ROM_QSTR(MP_QSTR_readfrom_mem), MP_ROM_PTR(&example_readfrom_mem_obj) },
};
STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table);
const mp_obj_module_t example_user_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t*)&example_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED);
Going one step further you can directly add c code to be substituted into the c generated code where the "//Your code here comment" is.
For example, starting with a fresh example.py you could define it as.
def add_ints(a: int, b: int) -> int:
"""Adds two integers
:param a:
:param b:
:return:a + b"""
add_ints.code = " ret_val = a + b;"
to get a fully defined function in c
Output
// Include required definitions first.
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
//Adds two integers
//:param a:
//:param b:
//:return:a + b
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
mp_int_t a = mp_obj_get_int(a_obj);
mp_int_t b = mp_obj_get_int(b_obj);
mp_int_t ret_val;
ret_val = a + b;
return mp_obj_new_int(ret_val);
}
MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table);
const mp_obj_module_t example_user_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t*)&example_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED);
If you don't need the fully module boiler plate, you can generate individual functions with
import ustubby
def add_ints(a: int, b: int) -> int:
"""add two ints"""
add_ints.code = " ret_val = a + b;"
add_ints.__module__ = "new_module"
print(ustubby.stub_function(add_ints))
//add two ints
STATIC mp_obj_t new_module_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
mp_int_t a = mp_obj_get_int(a_obj);
mp_int_t b = mp_obj_get_int(b_obj);
mp_int_t ret_val;
ret_val = a + b;
return mp_obj_new_int(ret_val);
}
MP_DEFINE_CONST_FUN_OBJ_2(new_module_add_ints_obj, new_module_add_ints);
uStubby is also trying to support c code generation from Litex files such as
#--------------------------------------------------------------------------------
# Auto-generated by Migen (5585912) & LiteX (e637aa65) on 2019-08-04 03:04:29
#--------------------------------------------------------------------------------
csr_register,cas_leds_out,0x82000800,1,rw
csr_register,cas_buttons_ev_status,0x82000804,1,rw
csr_register,cas_buttons_ev_pending,0x82000808,1,rw
csr_register,cas_buttons_ev_enable,0x8200080c,1,rw
csr_register,ctrl_reset,0x82001000,1,rw
csr_register,ctrl_scratch,0x82001004,4,rw
csr_register,ctrl_bus_errors,0x82001014,4,ro
Currently only csr_register is supported. Please raise issues if you need to expand this feature.
import ustubby
mod = ustubby.parse_csv("csr.csv")
print(ustubby.stub_module(mod))
Install the test requirements with
pip install -r requirements-test.txt
Install the package in editable mode
pip install -e .
Run the tests
pytest
TBD
Contributions are welcome. Get in touch or create a new pull request.
Inspired by
PyCon AU 2019 Sprints
- Ryan Parry-Jones - Original Developer - pazzarpj
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE.txt file for details