├── .gitattributes ├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── LICENSE.txt ├── README.md ├── pytest.ini ├── requirements-test.txt ├── requirements.txt ├── setup.py ├── src └── ustubby │ ├── __init__.py │ └── __main__.py └── test ├── __init__.py └── test_basic.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -r requirements-test.txt 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | pip install -e . 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | .idea/ 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ryanpj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Ryan Parry-Jones 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uStubby 2 | 3 | uStubby is a library for generating micropython c extension stubs from type annotated python. 4 | 5 | According to [Link](https://micropython.org/) 6 | 7 | "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." 8 | 9 | Sometimes, pure python performance isn't enough. 10 | C extensions are a way to improve performace whilst still having the bulk of your code in micropython. 11 | 12 | Unfortunately there is a lot of boilerplate code needed to build these extensions. 13 | 14 | uStubby is designed to make this process as easy as writing python. 15 | 16 | ## Getting Started 17 | 18 | uStubby is targeted to run on Python 3.7, but should run on versions 3.6 or greater 19 | 20 | ### Installing 21 | 22 | Currently, there are no external dependencies for running uStubby. 23 | Install from PyPI with: 24 | ```bash 25 | pip install ustubby 26 | ``` 27 | Alternatively, clone the repository using git and just put it on the path to install. 28 | 29 | ## Usage 30 | This example follows generating the template as shown [here](http://docs.micropython.org/en/latest/develop/cmodules.html#basic-example) 31 | 32 | Create a python file with the module as you intend to use it from micropython. 33 | 34 | eg. example.py 35 | ```python 36 | def add_ints(a: int, b: int) -> int: 37 | """Adds two integers 38 | :param a: 39 | :param b: 40 | :return:a + b""" 41 | ``` 42 | We can then convert this into the appropriate c stub via the CLI: 43 | ```bash 44 | # This will generate the file "example.c" 45 | ustubby example.py 46 | ``` 47 | 48 | Alternatively, you can invoke the python interface in a script: 49 | 50 | ```python 51 | import ustubby 52 | import example 53 | 54 | print(ustubby.stub_module(example)) 55 | ``` 56 |
Output

57 | 58 | ```c 59 | // Include required definitions first. 60 | #include "py/obj.h" 61 | #include "py/runtime.h" 62 | #include "py/builtin.h" 63 | 64 | //Adds two integers 65 | //:param a: 66 | //:param b: 67 | //:return:a + b 68 | STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 69 | mp_int_t a = mp_obj_get_int(a_obj); 70 | mp_int_t b = mp_obj_get_int(b_obj); 71 | mp_int_t ret_val; 72 | 73 | //Your code here 74 | 75 | return mp_obj_new_int(ret_val); 76 | } 77 | MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints); 78 | 79 | STATIC const mp_rom_map_elem_t example_module_globals_table[] = { 80 | { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) }, 81 | { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) }, 82 | }; 83 | 84 | STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); 85 | const mp_obj_module_t example_user_cmodule = { 86 | .base = {&mp_type_module}, 87 | .globals = (mp_obj_dict_t*)&example_module_globals, 88 | }; 89 | 90 | MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED); 91 | ``` 92 |

93 | 94 | This will parse all the functions in the module and attach them to the same namespace in micropython. 95 | ##### Note: It will only generate the boilerplate code and not the actual code that does the work such as a + b 96 | After editing the code in the template at the place marked //Code goes here you can follow the instructions 97 | [here](http://docs.micropython.org/en/latest/develop/cmodules.html#basic-example) for modifying 98 | the Make File and building the module into your micro python deployment. 99 | 100 | You should then be able to use the module in micro python by typing 101 | ```python 102 | import example # from example.c compiled into micropython 103 | example.add_ints(1, 2) 104 | # prints 3 105 | ``` 106 | ##### Note: This example.py is the one compiled into the micropython source and not the file we created earlier 107 | 108 | ### Advanced usage 109 | If you added two more functions to the original example.py 110 | ```python 111 | def lots_of_parameters(a: int, b: float, c: tuple, d: object, e: str) -> None: 112 | """ 113 | :param a: 114 | :param b: 115 | :param c: 116 | :param d: 117 | :return: 118 | """ 119 | 120 | def readfrom_mem(addr: int = 0, memaddr: int = 0, arg: object = None, *, addrsize: int = 8) -> str: 121 | """ 122 | :param addr: 123 | :param memaddr: 124 | :param arg: 125 | :param addrsize: Keyword only arg 126 | :return: 127 | """ 128 | ``` 129 | 130 | logs_of_parameters shows the types of types you can parse in. You always need to annotate each parameter and the return. 131 | readfrom_mem shows that you can set default values for certain parameters and specify that addrsize is a keyword only 132 | argument. 133 | 134 | At the c level in micropython, there is only three ways of implementing a function. 135 | ##### Basic Case 136 | ```python 137 | def foo(a, b, c): # 0 to 3 args 138 | pass 139 | ``` 140 | ```c 141 | MP_DEFINE_CONST_FUN_OBJ_X // Where x is 0 to 3 args 142 | ``` 143 | ##### Greater than three positional args 144 | ```python 145 | def foo(*args): 146 | pass 147 | ``` 148 | ```c 149 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN 150 | ``` 151 | ##### Arbitary args 152 | ```python 153 | def foo(*args, **kwargs): 154 | pass 155 | ``` 156 | ```c 157 | MP_DEFINE_CONST_FUN_OBJ_KW 158 | ``` 159 | Each successively increasing the boiler plate to conveniently accessing the variables. 160 | 161 |
Output

162 | 163 | ```c 164 | // Include required definitions first. 165 | #include "py/obj.h" 166 | #include "py/runtime.h" 167 | #include "py/builtin.h" 168 | 169 | //Adds two integers 170 | //:param a: 171 | //:param b: 172 | //:return:a + b 173 | STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 174 | mp_int_t a = mp_obj_get_int(a_obj); 175 | mp_int_t b = mp_obj_get_int(b_obj); 176 | mp_int_t ret_val; 177 | 178 | //Your code here 179 | 180 | return mp_obj_new_int(ret_val); 181 | } 182 | MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints); 183 | // 184 | //:param a: 185 | //:param b: 186 | //:param c: 187 | //:param d: 188 | //:return: 189 | // 190 | STATIC mp_obj_t example_lots_of_parameters(size_t n_args, const mp_obj_t *args) { 191 | mp_int_t a = mp_obj_get_int(a_obj); 192 | mp_float_t b = mp_obj_get_float(b_obj); 193 | mp_obj_t *c = NULL; 194 | size_t c_len = 0; 195 | mp_obj_get_array(c_arg, &c_len, &c); 196 | mp_obj_t d args[ARG_d].u_obj; 197 | 198 | //Your code here 199 | 200 | return mp_const_none; 201 | } 202 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(example_lots_of_parameters_obj, 4, 4, example_lots_of_parameters); 203 | // 204 | //:param addr: 205 | //:param memaddr: 206 | //:param arg: 207 | //:param addrsize: Keyword only arg 208 | //:return: 209 | // 210 | STATIC mp_obj_t example_readfrom_mem(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { 211 | enum { ARG_addr, ARG_memaddr, ARG_arg, ARG_addrsize }; 212 | STATIC const mp_arg_t example_readfrom_mem_allowed_args[] = { 213 | { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } }, 214 | { MP_QSTR_memaddr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } }, 215 | { MP_QSTR_arg, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } }, 216 | { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, { .u_int = 8 } }, 217 | }; 218 | 219 | mp_arg_val_t args[MP_ARRAY_SIZE(example_readfrom_mem_allowed_args)]; 220 | mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, 221 | MP_ARRAY_SIZE(example_readfrom_mem_allowed_args), example_readfrom_mem_allowed_args, args); 222 | 223 | mp_int_t addr = args[ARG_addr].u_int; 224 | mp_int_t memaddr = args[ARG_memaddr].u_int; 225 | mp_obj_t arg = args[ARG_arg].u_obj; 226 | mp_int_t addrsize = args[ARG_addrsize].u_int; 227 | 228 | //Your code here 229 | 230 | return mp_obj_new_str(, ); 231 | } 232 | MP_DEFINE_CONST_FUN_OBJ_KW(example_readfrom_mem_obj, 1, example_readfrom_mem); 233 | 234 | STATIC const mp_rom_map_elem_t example_module_globals_table[] = { 235 | { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) }, 236 | { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) }, 237 | { MP_ROM_QSTR(MP_QSTR_lots_of_parameters), MP_ROM_PTR(&example_lots_of_parameters_obj) }, 238 | { MP_ROM_QSTR(MP_QSTR_readfrom_mem), MP_ROM_PTR(&example_readfrom_mem_obj) }, 239 | }; 240 | 241 | STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); 242 | const mp_obj_module_t example_user_cmodule = { 243 | .base = {&mp_type_module}, 244 | .globals = (mp_obj_dict_t*)&example_module_globals, 245 | }; 246 | 247 | MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED); 248 | ``` 249 |

250 | 251 | #### Adding fully implemented c functions 252 | Going one step further you can directly add c code to be substituted into the c generated code where the 253 | "//Your code here comment" is. 254 | 255 | For example, starting with a fresh example.py you could define it as. 256 | 257 | ```python 258 | def add_ints(a: int, b: int) -> int: 259 | """Adds two integers 260 | :param a: 261 | :param b: 262 | :return:a + b""" 263 | add_ints.code = " ret_val = a + b;" 264 | ``` 265 | to get a fully defined function in c 266 | 267 |
Output

268 | 269 | ```c 270 | // Include required definitions first. 271 | #include "py/obj.h" 272 | #include "py/runtime.h" 273 | #include "py/builtin.h" 274 | 275 | //Adds two integers 276 | //:param a: 277 | //:param b: 278 | //:return:a + b 279 | STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 280 | mp_int_t a = mp_obj_get_int(a_obj); 281 | mp_int_t b = mp_obj_get_int(b_obj); 282 | mp_int_t ret_val; 283 | 284 | ret_val = a + b; 285 | 286 | return mp_obj_new_int(ret_val); 287 | } 288 | MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints); 289 | 290 | STATIC const mp_rom_map_elem_t example_module_globals_table[] = { 291 | { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) }, 292 | { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) }, 293 | }; 294 | 295 | STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); 296 | const mp_obj_module_t example_user_cmodule = { 297 | .base = {&mp_type_module}, 298 | .globals = (mp_obj_dict_t*)&example_module_globals, 299 | }; 300 | 301 | MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED); 302 | ``` 303 |

304 | 305 | #### Using functions without a module definition 306 | If you don't need the fully module boiler plate, you can generate individual functions with 307 | ```python 308 | import ustubby 309 | def add_ints(a: int, b: int) -> int: 310 | """add two ints""" 311 | add_ints.code = " ret_val = a + b;" 312 | add_ints.__module__ = "new_module" 313 | 314 | print(ustubby.stub_function(add_ints)) 315 | ``` 316 | 317 | ```c 318 | //add two ints 319 | STATIC mp_obj_t new_module_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 320 | mp_int_t a = mp_obj_get_int(a_obj); 321 | mp_int_t b = mp_obj_get_int(b_obj); 322 | mp_int_t ret_val; 323 | 324 | ret_val = a + b; 325 | 326 | return mp_obj_new_int(ret_val); 327 | } 328 | MP_DEFINE_CONST_FUN_OBJ_2(new_module_add_ints_obj, new_module_add_ints); 329 | ``` 330 | 331 | #### Parsing Litex Files 332 | uStubby is also trying to support c code generation from Litex files such as 333 | ```csv 334 | #-------------------------------------------------------------------------------- 335 | # Auto-generated by Migen (5585912) & LiteX (e637aa65) on 2019-08-04 03:04:29 336 | #-------------------------------------------------------------------------------- 337 | csr_register,cas_leds_out,0x82000800,1,rw 338 | csr_register,cas_buttons_ev_status,0x82000804,1,rw 339 | csr_register,cas_buttons_ev_pending,0x82000808,1,rw 340 | csr_register,cas_buttons_ev_enable,0x8200080c,1,rw 341 | csr_register,ctrl_reset,0x82001000,1,rw 342 | csr_register,ctrl_scratch,0x82001004,4,rw 343 | csr_register,ctrl_bus_errors,0x82001014,4,ro 344 | ``` 345 | Currently only csr_register is supported. Please raise issues if you need to expand this feature. 346 | ```python 347 | import ustubby 348 | mod = ustubby.parse_csv("csr.csv") 349 | print(ustubby.stub_module(mod)) 350 | ``` 351 | 352 | ## Running the tests 353 | Install the test requirements with 354 | ```bash 355 | pip install -r requirements-test.txt 356 | ``` 357 | Install the package in editable mode 358 | ```bash 359 | pip install -e . 360 | ``` 361 | Run the tests 362 | ```bash 363 | pytest 364 | ``` 365 | 366 | ## Check out the docs 367 | 368 | TBD 369 | 370 | ## Contributing 371 | 372 | Contributions are welcome. Get in touch or create a new pull request. 373 | 374 | ## Credits 375 | Inspired by 376 | - [Extending MicroPython: Using C for Good](https://youtu.be/fUb3Urw4H-E) 377 | - [Online C Stub Generator](https://gitlab.com/oliver.robson/mpy-c-stub-gen) 378 | - [Micropython](https://micropython.org) 379 | 380 | PyCon AU 2019 Sprints 381 | 382 | ## Authors 383 | 384 | * **Ryan Parry-Jones** - *Original Developer* - [pazzarpj](https://github.com/pazzarpj) 385 | 386 | See also the list of [contributors](https://github.com/pazzarpj/micropython-ustubby/contributors) who participated in this project. 387 | 388 | ## License 389 | 390 | This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details 391 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = test 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pazzarpj/micropython-ustubby/08890e47b71d01e8e97a741d3c2db6c0a9689882/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | import re 5 | import sys 6 | import os 7 | 8 | BASE_LOCATION = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | VERSION_FILE = os.path.join(BASE_LOCATION, "src", "ustubby", "__init__.py") 11 | REQUIRES_FILE = 'requirements.txt' 12 | DEPENDENCIES_FILE = None 13 | 14 | 15 | def filter_comments(fd): 16 | no_comments = list(filter(lambda l: l.strip().startswith("#") is False, fd.readlines())) 17 | return list(filter(lambda l: l.strip().startswith("-") is False, no_comments)) 18 | 19 | 20 | def readfile(filename, func): 21 | try: 22 | with open(os.path.join(BASE_LOCATION, filename)) as f: 23 | data = func(f) 24 | except (IOError, IndexError): 25 | sys.stderr.write(u""" 26 | Can't find '%s' file. This doesn't seem to be a valid release. 27 | """ % filename) 28 | sys.exit(1) 29 | return data 30 | 31 | 32 | def get_version(): 33 | with open(VERSION_FILE, 'r') as f: 34 | data = f.read() 35 | m = re.search(r"__version__ ?= ?\"[\d.]+\"", data) 36 | res = m.group(0) 37 | if res: 38 | ret = re.search(r"(?<=\")[\d\.]+", res).group(0) 39 | if ret: 40 | return ret 41 | raise ValueError("No version for ustubby found") 42 | 43 | 44 | def get_requires(): 45 | return readfile(REQUIRES_FILE, filter_comments) 46 | 47 | 48 | def get_dependencies(): 49 | return readfile(DEPENDENCIES_FILE, filter_comments) 50 | 51 | 52 | setup( 53 | name="ustubby", 54 | author="Ryan Parry-Jones", 55 | author_email="ryanspj+github@gmail.com", 56 | description="Micropython c stub generator", 57 | long_description=open("README.md").read(), 58 | long_description_content_type="text/markdown", 59 | package_dir={'': 'src'}, 60 | packages=find_packages('src'), 61 | entry_points={ 62 | 'console_scripts': [ 63 | 'ustubby = ustubby.__main__:main' 64 | ] 65 | }, 66 | url="https://github.com/pazzarpj/micropython-ustubby", 67 | version=get_version(), 68 | python_requires='>=3.6', 69 | dependency_links=[], 70 | include_package_data=True, 71 | zip_safe=False, 72 | classifiers=[ 73 | "Development Status :: 4 - Beta", 74 | "Intended Audience :: Manufacturing", 75 | "License :: OSI Approved :: MIT License", 76 | "Programming Language :: Python :: 3 :: Only", 77 | "Programming Language :: Python :: 3.6", 78 | "Programming Language :: Python :: 3.7", 79 | "Programming Language :: Python :: 3.8", 80 | ], 81 | ) 82 | -------------------------------------------------------------------------------- /src/ustubby/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import inspect 3 | import types 4 | import csv 5 | from typing import Dict 6 | 7 | __version__ = "0.1.1" 8 | 9 | 10 | def string_template(base_str): 11 | def string_handle(*args, **kwargs): 12 | return base_str.format(*args, **kwargs) 13 | 14 | return string_handle 15 | 16 | 17 | type_handler = { 18 | int: string_template("\tmp_int_t {0} = mp_obj_get_int({0}_obj);"), 19 | float: string_template("\tmp_float_t {0} = mp_obj_get_float({0}_obj);"), 20 | bool: string_template("\tbool {0} = mp_obj_is_true({0}_obj);"), 21 | str: string_template("\tconst char* {0} = mp_obj_str_get_str({0}_obj);"), 22 | tuple: string_template( 23 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 24 | list: string_template( 25 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 26 | set: string_template( 27 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 28 | object: string_template("\tmp_obj_t {0} args[ARG_{0}].u_obj;"), 29 | "self": string_template("\tSELF_t *self = MP_OBJ_TO_PTR(self_in);"), 30 | } 31 | type_handler_arr = { 32 | int: string_template("\tmp_int_t {0} = mp_obj_get_int(args[{1}]);"), 33 | float: string_template("\tmp_float_t {0} = mp_obj_get_float(args[{1}]);"), 34 | bool: string_template("\tbool {0} = mp_obj_is_true(args[{1}]);"), 35 | str: string_template("\tconst char* {0} = mp_obj_str_get_str(args[{1}]);"), 36 | tuple: string_template( 37 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 38 | list: string_template( 39 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 40 | set: string_template( 41 | "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 42 | object: string_template("\tmp_obj_t {0} args[ARG_{0}].u_obj;") 43 | } 44 | 45 | return_type_handler = { 46 | int: "\tmp_int_t ret_val;", 47 | float: "\tmp_float_t ret_val;", 48 | bool: "\tbool ret_val;", 49 | str: "", 50 | tuple: "", 51 | # tuple: string_template( 52 | # "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 53 | # list: string_template( 54 | # "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 55 | # set: string_template( 56 | # "\tmp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 57 | None: "" 58 | } 59 | 60 | return_handler = { 61 | int: "\treturn mp_obj_new_int(ret_val);", 62 | float: "\treturn mp_obj_new_float(ret_val);", 63 | bool: "\treturn mp_obj_new_bool(ret_val);", 64 | str: "\treturn mp_obj_new_str(, );", 65 | tuple: ''' 66 | // signature: mp_obj_t mp_obj_new_tuple(size_t n, const mp_obj_t *items); 67 | mp_obj_t ret_val[] = { 68 | mp_obj_new_int(123), 69 | mp_obj_new_float(456.789), 70 | mp_obj_new_str("hello", 5), 71 | }; 72 | return mp_obj_new_tuple(3, ret_val);''', 73 | None: "\treturn mp_const_none;" 74 | } 75 | 76 | shortened_types = { 77 | int: "int", 78 | object: "obj", 79 | None: "null", 80 | bool: "bool", 81 | "self": "OBJ", 82 | } 83 | 84 | 85 | def expand_newlines(lst_in): 86 | new_list = [] 87 | for line in lst_in: 88 | new_list.extend(line.replace('\t', ' ').split('\n')) 89 | return new_list 90 | 91 | 92 | class BaseContainer: 93 | def load_c(self, input: str): 94 | """ 95 | :param input: String of c source 96 | :return: self 97 | """ 98 | return self 99 | 100 | def load_python(self, input) -> BaseContainer: 101 | """ 102 | :param input: Python Object 103 | :return: self 104 | """ 105 | return self 106 | 107 | def to_c(self): 108 | """ 109 | Parse the container into c source code 110 | :return: 111 | """ 112 | 113 | def to_python(self): 114 | """ 115 | Parse the container into python objects 116 | :return: 117 | """ 118 | 119 | 120 | class ModuleContainer: 121 | def __init__(self): 122 | self.headers = ['#include "py/obj.h"', '#include "py/runtime.h"', '#include "py/builtin.h"'] 123 | self.functions = [] 124 | 125 | 126 | class FunctionContainer(BaseContainer): 127 | return_type_handler = { 128 | int: "mp_int_t ret_val;", 129 | float: "mp_float_t ret_val;", 130 | bool: "bool ret_val;", 131 | str: None, 132 | tuple: "", 133 | # tuple: string_template( 134 | # "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 135 | # list: string_template( 136 | # "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 137 | # set: string_template( 138 | # "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 139 | None: None 140 | } 141 | return_handler = { 142 | int: "return mp_obj_new_int(ret_val);", 143 | float: "return mp_obj_new_float(ret_val);", 144 | bool: "return mp_obj_new_bool(ret_val);", 145 | str: "return mp_obj_new_str(, );", 146 | tuple: ''' 147 | // signature: mp_obj_t mp_obj_new_tuple(size_t n, const mp_obj_t *items); 148 | mp_obj_t ret_val[] = { 149 | mp_obj_new_int(123), 150 | mp_obj_new_float(456.789), 151 | mp_obj_new_str("hello", 5), 152 | }; 153 | return mp_obj_new_tuple(3, ret_val);''', 154 | None: "return mp_const_none;" 155 | } 156 | 157 | def __init__(self): 158 | self.comments = None 159 | self.name = None 160 | self.module = None 161 | self.parameters: ParametersContainer = ParametersContainer() 162 | self.code = None 163 | self.return_type = None 164 | self.return_value = None 165 | self.signature = None 166 | 167 | def load_python(self, input) -> FunctionContainer: 168 | """ 169 | :param input: Function to parse 170 | :return: 171 | """ 172 | self.comments = input.__doc__ 173 | self.name = input.__name__ 174 | self.module = input.__module__ 175 | self.signature = inspect.signature(input) 176 | self.parameters.load_python(self.signature.parameters) 177 | self.return_type = inspect.signature(input).return_annotation 178 | return self 179 | 180 | def to_c_comments(self): 181 | """ 182 | Uses single line comments as we can't know if there are string escapes such as /* in the code 183 | :param f: 184 | :return: 185 | """ 186 | if self.comments: 187 | return '\n'.join(["//" + line.strip() for line in self.comments.splitlines() if line.strip()]) 188 | 189 | def to_c_func_def(self): 190 | return f"STATIC mp_obj_t {self.module}_{self.name}" 191 | 192 | def to_c_return_val_init(self): 193 | return self.return_type_handler[self.return_type] 194 | 195 | def to_c_code_body(self): 196 | if self.code: 197 | return self.code 198 | else: 199 | return "//Your code here" 200 | 201 | def to_c_return_value(self): 202 | if self.return_value is not None: 203 | return self.return_value 204 | else: 205 | return self.return_handler.get(self.return_type) 206 | 207 | def to_c_define(self): 208 | if self.parameters.type == "positional": 209 | return f"MP_DEFINE_CONST_FUN_OBJ_{self.parameters.count}(" \ 210 | f"{self.module}_{self.name}_obj, {self.module}_{self.name});" 211 | elif self.parameters.type == "between": 212 | return f"MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(" \ 213 | f"{self.module}_{self.name}_obj, " \ 214 | f"{self.parameters.count}, {self.parameters.count}, {self.module}_{self.name});" 215 | elif self.parameters.type == "keyword": 216 | return f"MP_DEFINE_CONST_FUN_OBJ_KW({self.module}_{self.name}_obj, 1, {self.module}_{self.name});" 217 | 218 | def to_c_arg_array_def(self): 219 | if self.parameters.type == "keyword": 220 | return f"STATIC const mp_arg_t {self.module}_{self.name}_allowed_args[]" 221 | 222 | def to_c(self): 223 | # TODO work on formatter 224 | resp = self.to_c_comments() 225 | resp += "\n" 226 | resp += f"{self.to_c_func_def()}({self.parameters.to_c_input()}) {{\n" 227 | resp += " " + "\n ".join(self.parameters.to_c_init().splitlines()) + "\n" 228 | if self.to_c_return_val_init(): 229 | resp += " " + self.to_c_return_val_init() + "\n" 230 | resp += f"\n {self.to_c_code_body()}\n\n" 231 | resp += f" {self.to_c_return_value()}\n" 232 | resp += "}\n" 233 | resp += self.to_c_define() 234 | return resp 235 | 236 | 237 | class ParametersContainer(BaseContainer): 238 | type_handler = { 239 | int: string_template("mp_int_t {0} = mp_obj_get_int({0}_obj);"), 240 | float: string_template("mp_float_t {0} = mp_obj_get_float({0}_obj);"), 241 | bool: string_template("bool {0} = mp_obj_is_true({0}_obj);"), 242 | str: string_template("const char* {0} = mp_obj_str_get_str({0}_obj);"), 243 | tuple: string_template( 244 | "mp_obj_t *{0} = NULL;\nsize_t {0}_len = 0;\nmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 245 | list: string_template( 246 | "mp_obj_t *{0} = NULL;\nsize_t {0}_len = 0;\nmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 247 | set: string_template( 248 | "mp_obj_t *{0} = NULL;\nsize_t {0}_len = 0;\nmp_obj_get_array({0}_arg, &{0}_len, &{0});"), 249 | object: string_template("\tmp_obj_t {0} args[ARG_{0}].u_obj;") 250 | } 251 | type_handler_arr = { 252 | int: string_template("mp_int_t {0} = mp_obj_get_int(args[{1}]);"), 253 | float: string_template("mp_float_t {0} = mp_obj_get_float(args[{1}]);"), 254 | bool: string_template("bool {0} = mp_obj_is_true(args[{1}]);"), 255 | str: string_template("const char* {0} = mp_obj_str_get_str(args[{1}]);"), 256 | tuple: string_template( 257 | "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 258 | list: string_template( 259 | "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 260 | set: string_template( 261 | "mp_obj_t *{0} = NULL;\n\tsize_t {0}_len = 0;\n\tmp_obj_get_array(args[{1}], &{0}_len, &{0});"), 262 | object: string_template("mp_obj_t {0} args[ARG_{0}].u_obj;") 263 | } 264 | 265 | def __init__(self): 266 | self.type = "" 267 | self.count = 0 268 | self.parameters = None 269 | 270 | def load_python(self, input: Dict[str, inspect.Parameter]) -> ParametersContainer: 271 | self.parameters = input 272 | self.count = len(self.parameters) 273 | simple = all([param.kind == param.POSITIONAL_OR_KEYWORD for param in self.parameters.values()]) 274 | if simple and self.count < 4: 275 | self.type = "positional" 276 | elif simple: 277 | self.type = "between" 278 | else: 279 | self.type = "keyword" 280 | return self 281 | 282 | def to_c_enums(self): 283 | if self.type != "keyword": 284 | return None 285 | return f"enum {{ {', '.join(['ARG_' + k for k in self.parameters])} }};" 286 | 287 | def to_c_kw_allowed_args(self): 288 | if self.type == "keyword": 289 | args = [] 290 | for name, param in self.parameters.items(): 291 | if param.kind in [param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY]: 292 | arg_type = "MP_ARG_REQUIRED" 293 | else: 294 | arg_type = "MP_ARG_KW_ONLY" 295 | type_txt = shortened_types[param.annotation] 296 | if param.default is inspect._empty: 297 | default = "" 298 | elif param.default is None: 299 | default = f"{{ .u_{type_txt} = MP_OBJ_NULL }}" 300 | else: 301 | default = f"{{ .u_{type_txt} = {param.default} }}" 302 | args.append(f"{{ MP_QSTR_{name}, {arg_type} | MP_ARG_{type_txt.upper()}, {default} }},") 303 | return args 304 | # return f"\tSTATIC const mp_arg_t {f.__module__}_{f.__name__}_allowed_args[] = {{\n\t\t{args}\n\t}};" 305 | 306 | def to_c_arg_array(self): 307 | if self.type != "keyword": 308 | return None 309 | 310 | def to_c_kw_arg_unpack(self): 311 | if self.type != "keyword": 312 | return None 313 | 314 | def to_c_input(self): 315 | if self.type == "positional": 316 | return ", ".join([f"mp_obj_t {x}_obj" for x in self.parameters]) 317 | elif self.type == "between": 318 | return "size_t n_args, const mp_obj_t *args" 319 | elif self.type == "keyword": 320 | # Complex case 321 | return "size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {" 322 | 323 | def to_c_init(self): 324 | if self.type == "keyword": 325 | return "\n".join([self.to_c_enums(), 326 | self.to_c_kw_allowed_args(), 327 | "", 328 | self.to_c_arg_array(), 329 | "", 330 | self.to_c_kw_arg_unpack()]) 331 | elif len(self.parameters) > 3: 332 | return "\n".join([self.type_handler_arr[value.annotation](param, index) for index, (param, value) in enumerate(self.parameters.items())]) 333 | else: 334 | return "\n".join([self.type_handler[value.annotation](param) for param, value in self.parameters.items()]) 335 | 336 | 337 | class ReturnContainer(BaseContainer): 338 | pass 339 | 340 | 341 | def stub_function(f, self=False): 342 | """ 343 | :param self: first parameter is self 344 | """ 345 | # Function implementation 346 | stub_ret = ["", function_comments(f), function_init(f"{f.__module__}_{f.__name__}")] 347 | sig = inspect.signature(f) 348 | stub_ret[-1] += function_params(sig.parameters) 349 | if self: 350 | parameters = dict(sig.parameters) 351 | parameters['self'] = inspect.Parameter(name='self', kind=inspect._ParameterKind.POSITIONAL_ONLY, annotation='self') 352 | else: 353 | parameters = dict(sig.parameters) 354 | stub_ret.extend(parse_params(f, parameters)) 355 | ret_init = ret_val_init(sig.return_annotation) 356 | if ret_init: 357 | stub_ret.append(ret_init) 358 | stub_ret.append("") 359 | stub_ret.append(code(f)) 360 | stub_ret.append("") 361 | stub_ret.append(ret_val_return(sig.return_annotation)) 362 | stub_ret.append("}") 363 | # C Function Definition 364 | stub_ret.append(function_reference(f, f"{f.__module__}_{f.__name__}", sig.parameters)) 365 | return "\n".join(expand_newlines(stub_ret)) 366 | 367 | 368 | def module_doc(mod): 369 | s = '''// This file was developed using uStubby. 370 | // https://github.com/pazzarpj/micropython-ustubby 371 | ''' 372 | if mod.__doc__ is not None: 373 | s += '\n/*'+ mod.__doc__ + '*/\n' 374 | return s 375 | 376 | def stub_module(mod): 377 | stub_ret = [module_doc(mod), headers()] 378 | classes = [o[1] for o in inspect.getmembers(mod) if inspect.isclass(o[1])] 379 | members = [o[1] for cls in classes for o in inspect.getmembers(cls) if inspect.isfunction(o[1])] 380 | functions = [o[1] for o in inspect.getmembers(mod) if inspect.isfunction(o[1])] 381 | for func in members: 382 | stub_ret.append(stub_function(func, self=True)) 383 | # Define the functions 384 | for func in functions: 385 | stub_ret.append(stub_function(func)) 386 | # Set up the module properties 387 | stub_ret.append("") 388 | stub_ret.append(f"STATIC const mp_rom_map_elem_t {mod.__name__}_module_globals_table[] = {{") 389 | stub_ret.append(f"\t{{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_{mod.__name__}) }},") 390 | stub_ret.extend( 391 | [f"\t{{ MP_ROM_QSTR(MP_QSTR_{f.__name__}), MP_ROM_PTR(&{mod.__name__}_{f.__name__}_obj) }}," for f in 392 | functions]) 393 | stub_ret.append("};") 394 | stub_ret.append("") 395 | stub_ret.append(f"STATIC MP_DEFINE_CONST_DICT({mod.__name__}_module_globals, {mod.__name__}_module_globals_table);") 396 | # Define the module object 397 | stub_ret.append(f"const mp_obj_module_t {mod.__name__}_user_cmodule = {{") 398 | stub_ret.append(f"\t.base = {{&mp_type_module}},") 399 | stub_ret.append(f"\t.globals = (mp_obj_dict_t*)&{mod.__name__}_module_globals,") 400 | stub_ret.append("};") 401 | # Register the module 402 | stub_ret.append("") 403 | stub_ret.append( 404 | f"MP_REGISTER_MODULE(MP_QSTR_{mod.__name__}, {mod.__name__}_user_cmodule, MODULE_{mod.__name__.upper()}_ENABLED);") 405 | return "\n".join(stub_ret) 406 | 407 | 408 | def function_init(func_name): 409 | return f"STATIC mp_obj_t {func_name}(" 410 | 411 | 412 | def ret_val_init(ret_type): 413 | return return_type_handler[ret_type] 414 | 415 | 416 | def ret_val_return(ret_type): 417 | return return_handler[ret_type] 418 | 419 | 420 | def function_params(params): 421 | if len(params) == 0: 422 | return ") {" 423 | simple = all([param.kind == param.POSITIONAL_OR_KEYWORD for param in params.values()]) 424 | if simple and len(params) < 4: 425 | params = ", ".join([f"mp_obj_t {x}_obj" for x in params]) 426 | return params + ") {" 427 | elif simple: 428 | return "size_t n_args, const mp_obj_t *args) {" 429 | else: 430 | # Complex case 431 | return "size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {" 432 | 433 | 434 | def kw_enum(params): 435 | return f"\tenum {{ {', '.join(['ARG_' + k for k in params])} }};" 436 | 437 | 438 | def kw_allowed_args(f, params): 439 | args = [] 440 | for name, param in params.items(): 441 | if param.kind in [param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY]: 442 | arg_type = "MP_ARG_REQUIRED" 443 | else: 444 | arg_type = "MP_ARG_KW_ONLY" 445 | type_txt = shortened_types[param.annotation] 446 | if param.default is inspect._empty: 447 | default = "" 448 | elif param.default is None: 449 | default = f"{{ .u_{type_txt} = MP_OBJ_NULL }}" 450 | else: 451 | default = f"{{ .u_{type_txt} = {param.default} }}" 452 | args.append(f"{{ MP_QSTR_{name}, {arg_type} | MP_ARG_{type_txt.upper()}, {default} }},") 453 | args = "\n\t\t".join(args) 454 | return f"\tSTATIC const mp_arg_t {f.__module__}_{f.__name__}_allowed_args[] = {{\n\t\t{args}\n\t}};" 455 | 456 | 457 | def arg_array(f): 458 | args = f"{f.__module__}_{f.__name__}_allowed_args" 459 | return f"\tmp_arg_val_t args[MP_ARRAY_SIZE({args})];\n" \ 460 | f"\tmp_arg_parse_all(n_args - 1, pos_args + 1, kw_args,\n" \ 461 | f"\t\tMP_ARRAY_SIZE({args}), {args}, args);" 462 | 463 | 464 | def arg_unpack(params): 465 | return "\n".join( 466 | f"\tmp_{shortened_types[param.annotation]}_t {name} = args[ARG_{name}].u_{shortened_types[param.annotation]};" 467 | for name, param in params.items()) 468 | 469 | 470 | def parse_params(f, params): 471 | """ 472 | :param params: Parameter signature from inspect.signature 473 | :return: list of strings defining the parsed parameters in c 474 | """ 475 | simple = all([param.kind == param.POSITIONAL_OR_KEYWORD for param in params.values()]) 476 | if simple and len(params) > 3: 477 | return [type_handler_arr[value.annotation](param, ind) for ind, (param, value) in enumerate(params.items())] 478 | if simple: 479 | return [type_handler[value.annotation](param) for param, value in params.items()] 480 | else: 481 | return [kw_enum(params), kw_allowed_args(f, params), "", arg_array(f), "", arg_unpack(params)] 482 | 483 | 484 | def headers(): 485 | return '''// Include required definitions first. 486 | #include "py/obj.h" 487 | #include "py/runtime.h" 488 | #include "py/builtin.h"''' 489 | 490 | 491 | def function_comments(f): 492 | """ 493 | Uses single line comments as we can't know if there are string escapes such as /* in the code 494 | :param f: 495 | :return: 496 | """ 497 | try: 498 | return '\n'.join(["//" + line.strip() for line in f.__doc__.splitlines()]) 499 | except AttributeError: 500 | return "// No Comment" 501 | 502 | 503 | def code(f): 504 | try: 505 | return f.code 506 | except AttributeError: 507 | return "\t//Your code here" 508 | 509 | 510 | def function_reference(f, name, params): 511 | simple = all([param.kind == param.POSITIONAL_OR_KEYWORD for param in params.values()]) 512 | if simple and len(params) < 4: 513 | return f"MP_DEFINE_CONST_FUN_OBJ_{len(params)}({name}_obj, {name});" 514 | elif simple: 515 | return f"MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN({name}_obj, {len(params)}, {len(params)}, {name});" 516 | else: 517 | return f"MP_DEFINE_CONST_FUN_OBJ_KW({f.__module__}_{f.__name__}_obj, 1, {f.__module__}_{f.__name__});" 518 | 519 | 520 | def filter_comments(csvfile): 521 | for row in csvfile: 522 | if row.startswith('#'): 523 | continue 524 | yield row 525 | 526 | 527 | def register_func(func_name, address, length, access_control, mod="csr"): 528 | def write_func(value: int) -> None: 529 | pass 530 | 531 | def read_func() -> int: 532 | pass 533 | 534 | write_func.__name__ = f"{func_name}_write" 535 | write_func.__qualname__ = f"{func_name}_write" 536 | write_func.__module__ = mod 537 | write_func.__doc__ = f"""writes a value to {func_name} @ register {address}""" 538 | write_func.code = f"\tret_val = {func_name}_read();" 539 | 540 | read_func.__name__ = f"{func_name}_read" 541 | read_func.__qualname__ = f"{func_name}_read" 542 | read_func.__doc__ = f""":return: value from {func_name} @ register {address}""" 543 | read_func.__module__ = mod 544 | read_func.code = f"\t{func_name}_write(value);" 545 | 546 | if access_control == "ro": 547 | return [read_func] 548 | elif access_control == "rw": 549 | return [read_func, write_func] 550 | 551 | 552 | csr_types = { 553 | "csr_register": register_func 554 | } 555 | 556 | 557 | def parse_csv(path, module_name="csr"): 558 | mod = types.ModuleType(module_name) 559 | with open(path) as f: 560 | reader = csv.reader(filter_comments(f)) 561 | for csr_type, func_name, address, length, access_control in reader: 562 | if csr_type in csr_types: 563 | funcs = csr_types[csr_type](func_name, address, length, access_control, mod) 564 | for func in funcs: 565 | setattr(mod, func.__name__, func) 566 | return mod 567 | -------------------------------------------------------------------------------- /src/ustubby/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import importlib 3 | import sys 4 | from pathlib import Path 5 | 6 | import ustubby 7 | 8 | 9 | def main() -> int: 10 | parser = argparse.ArgumentParser(description="Converts a python file into micropython c extension stubs.") 11 | parser.add_argument("input", type=Path, 12 | help="Python file to convert.") 13 | parser.add_argument("-o", "--output", type=Path, default=None, 14 | help="Output C file. Defaults to \"${input}.c\".") 15 | parser.add_argument("--overwrite", action="store_true", 16 | help="Overwrite output file if it already exists.") 17 | args = parser.parse_args() 18 | 19 | ######################################## 20 | # Preprocess and error-check arguments # 21 | ######################################## 22 | if not args.input.exists(): 23 | print(f"{args.input} does not exist.") 24 | return 1 25 | 26 | if args.input.suffix != ".py": 27 | print(f"{args.input} is not a \".py\" file.") 28 | return 1 29 | 30 | args.input = args.input.expanduser().resolve() 31 | 32 | if args.output is None: 33 | args.output = args.input.with_suffix(".c") 34 | 35 | if args.output.suffix != ".c": 36 | print(f"{args.output} is not a \".c\" file.") 37 | return 1 38 | 39 | if args.output.exists() and not args.overwrite: 40 | print(f"{args.output} already exists.") 41 | return 1 42 | 43 | ################### 44 | # Execute ustubby # 45 | ################### 46 | sys.path.insert(0, str(args.input.parent)) 47 | 48 | module = importlib.import_module(args.input.stem) 49 | 50 | c_output = ustubby.stub_module(module) 51 | 52 | args.output.parent.mkdir(exist_ok=True, parents=True) 53 | args.output.write_text(c_output) 54 | 55 | return 0 56 | 57 | if __name__ == "__main__": 58 | sys.exit(main()) 59 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pazzarpj/micropython-ustubby/08890e47b71d01e8e97a741d3c2db6c0a9689882/test/__init__.py -------------------------------------------------------------------------------- /test/test_basic.py: -------------------------------------------------------------------------------- 1 | import ustubby 2 | 3 | 4 | def test_basic_example(): 5 | def add_ints(a: int, b: int) -> int: 6 | """Adds two integers 7 | :param a: 8 | :param b: 9 | :return:a + b""" 10 | 11 | add_ints.__module__ = "example" 12 | lines = """ 13 | //Adds two integers 14 | //:param a: 15 | //:param b: 16 | //:return:a + b 17 | STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 18 | mp_int_t a = mp_obj_get_int(a_obj); 19 | mp_int_t b = mp_obj_get_int(b_obj); 20 | mp_int_t ret_val; 21 | 22 | //Your code here 23 | 24 | return mp_obj_new_int(ret_val); 25 | } 26 | MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);""".splitlines() 27 | 28 | call_lines = ustubby.stub_function(add_ints).splitlines() 29 | for index, line in enumerate(lines): 30 | assert call_lines[index] == line 31 | 32 | 33 | def test_basic_example_load_function(): 34 | def add_ints(a: int, b: int) -> int: 35 | """Adds two integers 36 | :param a: 37 | :param b: 38 | :return:a + b""" 39 | 40 | add_ints.__module__ = "example" 41 | func = ustubby.FunctionContainer().load_python(add_ints) 42 | assert func.to_c_comments() == """//Adds two integers\n//:param a:\n//:param b:\n//:return:a + b""" 43 | assert func.to_c_func_def() == "STATIC mp_obj_t example_add_ints" 44 | assert func.to_c_return_val_init() == "mp_int_t ret_val;" 45 | assert func.to_c_code_body() == "//Your code here" 46 | assert func.to_c_return_value() == "return mp_obj_new_int(ret_val);" 47 | assert func.to_c_define() == "MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);" 48 | assert func.to_c_arg_array_def() is None 49 | 50 | 51 | def test_basic_example_load_function_e2e(): 52 | def add_ints(a: int, b: int) -> int: 53 | """Adds two integers 54 | :param a: 55 | :param b: 56 | :return:a + b""" 57 | 58 | add_ints.__module__ = "example" 59 | lines = """//Adds two integers 60 | //:param a: 61 | //:param b: 62 | //:return:a + b 63 | STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) { 64 | mp_int_t a = mp_obj_get_int(a_obj); 65 | mp_int_t b = mp_obj_get_int(b_obj); 66 | mp_int_t ret_val; 67 | 68 | //Your code here 69 | 70 | return mp_obj_new_int(ret_val); 71 | } 72 | MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);""".splitlines() 73 | func = ustubby.FunctionContainer().load_python(add_ints) 74 | print(func.to_c()) 75 | call_lines = func.to_c().splitlines() 76 | for index, line in enumerate(lines): 77 | assert call_lines[index] == line 78 | 79 | 80 | def test_readfrom_mem(): 81 | def readfrom_mem(addr: int = 0, memaddr: int = 0, arg: object = None, *, addrsize: int = 8) -> str: 82 | """ 83 | :param addr: 84 | :param memaddr: 85 | :param arg: 86 | :param addrsize: 87 | :return: 88 | """ 89 | 90 | readfrom_mem.__module__ = "example" 91 | lines = """ 92 | // 93 | //:param addr: 94 | //:param memaddr: 95 | //:param arg: 96 | //:param addrsize: 97 | //:return: 98 | // 99 | STATIC mp_obj_t example_readfrom_mem(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { 100 | enum { ARG_addr, ARG_memaddr, ARG_arg, ARG_addrsize }; 101 | STATIC const mp_arg_t example_readfrom_mem_allowed_args[] = { 102 | { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } }, 103 | { MP_QSTR_memaddr, MP_ARG_REQUIRED | MP_ARG_INT, { .u_int = 0 } }, 104 | { MP_QSTR_arg, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_obj = MP_OBJ_NULL } }, 105 | { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, { .u_int = 8 } }, 106 | }; 107 | 108 | mp_arg_val_t args[MP_ARRAY_SIZE(example_readfrom_mem_allowed_args)]; 109 | mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, 110 | MP_ARRAY_SIZE(example_readfrom_mem_allowed_args), example_readfrom_mem_allowed_args, args); 111 | 112 | mp_int_t addr = args[ARG_addr].u_int; 113 | mp_int_t memaddr = args[ARG_memaddr].u_int; 114 | mp_obj_t arg = args[ARG_arg].u_obj; 115 | mp_int_t addrsize = args[ARG_addrsize].u_int; 116 | 117 | //Your code here 118 | 119 | return mp_obj_new_str(, ); 120 | } 121 | MP_DEFINE_CONST_FUN_OBJ_KW(example_readfrom_mem_obj, 1, example_readfrom_mem);""".splitlines() 122 | call_lines = ustubby.stub_function(readfrom_mem).splitlines() 123 | for index, line in enumerate(call_lines): 124 | assert line == lines[index] 125 | 126 | 127 | def test_readfrom_mem_load_function(): 128 | def readfrom_mem(addr: int = 0, memaddr: int = 0, arg: object = None, *, addrsize: int = 8) -> str: 129 | """ 130 | :param addr: 131 | :param memaddr: 132 | :param arg: 133 | :param addrsize: 134 | :return: 135 | """ 136 | 137 | readfrom_mem.__module__ = "example" 138 | func = ustubby.FunctionContainer().load_python(readfrom_mem) 139 | assert func.to_c_comments() == """//:param addr:\n//:param memaddr:\n//:param arg:\n//:param addrsize:\n//:return:""" 140 | assert func.to_c_func_def() == "STATIC mp_obj_t example_readfrom_mem" 141 | assert func.to_c_return_val_init() is None 142 | assert func.to_c_code_body() == "//Your code here" 143 | assert func.to_c_return_value() == "return mp_obj_new_str(, );" 144 | assert func.to_c_define() == "MP_DEFINE_CONST_FUN_OBJ_KW(example_readfrom_mem_obj, 1, example_readfrom_mem);" 145 | assert func.to_c_arg_array_def() == "STATIC const mp_arg_t example_readfrom_mem_allowed_args[]" 146 | 147 | 148 | def test_many_positional_arguments(): 149 | def MahonyAHRSupdate(gx: float, gy: float, gz: float, ax: float, ay: float, az: float, mx: float, my: float, 150 | mz: float) -> None: 151 | """ 152 | :param gx: 153 | :param gy: 154 | :param gz: 155 | :param ax: 156 | :param ay: 157 | :param az: 158 | :param mx: 159 | :param my: 160 | :param mz: 161 | :return: 162 | """ 163 | 164 | MahonyAHRSupdate.__module__ = "MahonyAHRS" 165 | lines = """ 166 | // 167 | //:param gx: 168 | //:param gy: 169 | //:param gz: 170 | //:param ax: 171 | //:param ay: 172 | //:param az: 173 | //:param mx: 174 | //:param my: 175 | //:param mz: 176 | //:return: 177 | // 178 | STATIC mp_obj_t MahonyAHRS_MahonyAHRSupdate(size_t n_args, const mp_obj_t *args) { 179 | mp_float_t gx = mp_obj_get_float(args[0]); 180 | mp_float_t gy = mp_obj_get_float(args[1]); 181 | mp_float_t gz = mp_obj_get_float(args[2]); 182 | mp_float_t ax = mp_obj_get_float(args[3]); 183 | mp_float_t ay = mp_obj_get_float(args[4]); 184 | mp_float_t az = mp_obj_get_float(args[5]); 185 | mp_float_t mx = mp_obj_get_float(args[6]); 186 | mp_float_t my = mp_obj_get_float(args[7]); 187 | mp_float_t mz = mp_obj_get_float(args[8]); 188 | 189 | //Your code here 190 | 191 | return mp_const_none; 192 | } 193 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(MahonyAHRS_MahonyAHRSupdate_obj, 9, 9, MahonyAHRS_MahonyAHRSupdate);""".splitlines() 194 | call_lines = ustubby.stub_function(MahonyAHRSupdate).splitlines() 195 | for index, line in enumerate(call_lines): 196 | assert line == lines[index] 197 | 198 | 199 | def test_many_positional_arguments_function(): 200 | def MahonyAHRSupdate(gx: float, gy: float, gz: float, ax: float, ay: float, az: float, mx: float, my: float, 201 | mz: float) -> None: 202 | """ 203 | :param gx: 204 | :param gy: 205 | :param gz: 206 | :param ax: 207 | :param ay: 208 | :param az: 209 | :param mx: 210 | :param my: 211 | :param mz: 212 | :return: 213 | """ 214 | 215 | MahonyAHRSupdate.__module__ = "MahonyAHRS" 216 | lines = """//:param gx: 217 | //:param gy: 218 | //:param gz: 219 | //:param ax: 220 | //:param ay: 221 | //:param az: 222 | //:param mx: 223 | //:param my: 224 | //:param mz: 225 | //:return: 226 | STATIC mp_obj_t MahonyAHRS_MahonyAHRSupdate(size_t n_args, const mp_obj_t *args) { 227 | mp_float_t gx = mp_obj_get_float(args[0]); 228 | mp_float_t gy = mp_obj_get_float(args[1]); 229 | mp_float_t gz = mp_obj_get_float(args[2]); 230 | mp_float_t ax = mp_obj_get_float(args[3]); 231 | mp_float_t ay = mp_obj_get_float(args[4]); 232 | mp_float_t az = mp_obj_get_float(args[5]); 233 | mp_float_t mx = mp_obj_get_float(args[6]); 234 | mp_float_t my = mp_obj_get_float(args[7]); 235 | mp_float_t mz = mp_obj_get_float(args[8]); 236 | 237 | //Your code here 238 | 239 | return mp_const_none; 240 | } 241 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(MahonyAHRS_MahonyAHRSupdate_obj, 9, 9, MahonyAHRS_MahonyAHRSupdate);""".splitlines() 242 | func = ustubby.FunctionContainer().load_python(MahonyAHRSupdate) 243 | call_lines = func.to_c().splitlines() 244 | for index, line in enumerate(lines): 245 | assert call_lines[index] == line 246 | 247 | 248 | def test_zero_arguments(): 249 | def get_beta() -> float: 250 | """ 251 | def get_beta() -> float: 252 | """ 253 | 254 | get_beta.__module__ = "madgwick" 255 | lines = """ 256 | // 257 | //def get_beta() -> float: 258 | // 259 | STATIC mp_obj_t madgwick_get_beta() { 260 | mp_float_t ret_val; 261 | 262 | //Your code here 263 | 264 | return mp_obj_new_float(ret_val); 265 | } 266 | MP_DEFINE_CONST_FUN_OBJ_0(madgwick_get_beta_obj, madgwick_get_beta);""".splitlines() 267 | call_lines = ustubby.stub_function(get_beta).splitlines() 268 | for index, line in enumerate(call_lines): 269 | assert line == lines[index] 270 | --------------------------------------------------------------------------------