Skip to content

Library for building micropython c extensions using python

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
LICENSE.txt
Notifications You must be signed in to change notification settings

pazzarpj/micropython-ustubby

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

uStubby

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.

Getting Started

uStubby is targeted to run on Python 3.7, but should run on versions 3.6 or greater

Installing

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.

Usage

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

Advanced usage

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.

Basic Case
def foo(a, b, c): # 0 to 3 args
    pass
MP_DEFINE_CONST_FUN_OBJ_X // Where x is 0 to 3 args
Greater than three positional args
def foo(*args):
    pass
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN
Arbitary args
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);

Adding fully implemented c functions

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);

Using functions without a module definition

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);

Parsing Litex Files

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))

Running the tests

Install the test requirements with

pip install -r requirements-test.txt

Install the package in editable mode

pip install -e .

Run the tests

pytest

Check out the docs

TBD

Contributing

Contributions are welcome. Get in touch or create a new pull request.

Credits

Inspired by

PyCon AU 2019 Sprints

Authors

  • Ryan Parry-Jones - Original Developer - pazzarpj

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE.txt file for details

About

Library for building micropython c extensions using python

Resources

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
LICENSE.txt

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages