examples: Add example code for user C modules, both C and C++.

Add working example code to provide a starting point for users with files
that they can just copy, and include the modules in the coverage test to
verify the complete user C module build functionality.  The cexample module
uses the code originally found in cmodules.rst, which has been updated to
reflect this and partially rewritten with more complete information.
bound-method-equality
stijn 2020-10-21 11:13:47 +02:00 committed by Damien George
parent fad4079778
commit 25c4563f26
10 changed files with 205 additions and 99 deletions

View File

@ -49,8 +49,9 @@ A MicroPython user C module is a directory with the following files:
expanded copy of ``$(USERMOD_DIR)`` to ``SRC_USERMOD``, eg expanded copy of ``$(USERMOD_DIR)`` to ``SRC_USERMOD``, eg
``SRC_USERMOD += $(EXAMPLE_MOD_DIR)/example.c`` ``SRC_USERMOD += $(EXAMPLE_MOD_DIR)/example.c``
If you have custom ``CFLAGS`` settings or include folders to define, these If you have custom compiler options (like ``-I`` to add directories to search
should be added to ``CFLAGS_USERMOD``, or ``CXXFLAGS_USERMOD``. for header files), these should be added to ``CFLAGS_USERMOD`` for C code
and to ``CXXFLAGS_USERMOD`` for C++ code.
See below for full usage example. See below for full usage example.
@ -58,124 +59,113 @@ A MicroPython user C module is a directory with the following files:
Basic example Basic example
------------- -------------
This simple module named ``example`` provides a single function This simple module named ``cexample`` provides a single function
``example.add_ints(a, b)`` which adds the two integer args together and returns ``cexample.add_ints(a, b)`` which adds the two integer args together and returns
the result. the result. It can be found in the MicroPython source tree and has
a source file and a Makefile fragment with content as descibed above::
Directory:: micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
└── micropython.mk
example/ Refer to the comments in these 2 files for additional explanation.
├── example.c Next to the ``cexample`` module there's also ``cppexample`` which
└── micropython.mk works in the same way but shows one way of mixing C and C++ code
in MicroPython.
``example.c``
.. code-block:: c
// Include required definitions first.
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
// This is the function which will be called from Python as example.add_ints(a, b).
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
// Extract the ints from the micropython input objects
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
// Calculate the addition and convert to MicroPython object.
return mp_obj_new_int(a + b);
}
// Define a Python reference to the function above
STATIC MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
// Define all properties of the example module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
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);
// Define module object.
const mp_obj_module_t example_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&example_module_globals,
};
// Register the module to make it available in Python
MP_REGISTER_MODULE(MP_QSTR_example, example_user_cmodule, MODULE_EXAMPLE_ENABLED);
``micropython.mk``
.. code-block:: make
EXAMPLE_MOD_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(EXAMPLE_MOD_DIR)/example.c
# We can add our module folder to include paths if needed
# This is not actually needed in this example.
CFLAGS_USERMOD += -I$(EXAMPLE_MOD_DIR)
Finally you will need to define ``MODULE_EXAMPLE_ENABLED`` to 1. This
can be done by adding ``CFLAGS_EXTRA=-DMODULE_EXAMPLE_ENABLED=1`` to
the ``make`` command, or editing ``mpconfigport.h`` or
``mpconfigboard.h`` to add
.. code-block:: c
#define MODULE_EXAMPLE_ENABLED (1)
Note that the exact method depends on the port as they have different
structures. If not done correctly it will compile but importing will
fail to find the module.
Compiling the cmodule into MicroPython Compiling the cmodule into MicroPython
-------------------------------------- --------------------------------------
To build such a module, compile MicroPython (see `getting started To build such a module, compile MicroPython (see `getting started
<https://github.com/micropython/micropython/wiki/Getting-Started>`_) with an <https://github.com/micropython/micropython/wiki/Getting-Started>`_),
extra ``make`` flag named ``USER_C_MODULES`` set to the directory containing applying 2 modifications:
all modules you want included (not to the module itself). For example:
- an extra ``make`` flag named ``USER_C_MODULES`` set to the directory
containing all modules you want included (not to the module itself).
For building the example modules which come with MicroPython,
set ``USER_C_MODULES`` to the ``examples/usercmodule`` directory.
For your own projects it's more convenient to keep custom code out of
the main source tree so a typical project directory structure will look
like this::
my_project/
├── modules/
│ └──example1/
│ ├──example1.c
│ └──micropython.mk
│ └──example2/
│ ├──example2.c
│ └──micropython.mk
└── micropython/
├──ports/
... ├──stm32/
...
Directory:: with ``USER_C_MODULES`` set to the ``my_project/modules`` directory.
my_project/ - all modules found in this directory will be compiled, but only those
├── modules/ which are explicitly enabled will be availabe for importing. Enabling a
│ └──example/ module is done by setting the preprocessor define from its module
│ ├──example.c registration to 1. For example if the source code defines the module with
│ └──micropython.mk
└── micropython/
├──ports/
... ├──stm32/
...
Building for stm32 port: .. code-block:: c
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule, MODULE_CEXAMPLE_ENABLED);
then ``MODULE_CEXAMPLE_ENABLED`` has to be set to 1 to make the module available.
This can be done by adding ``CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1`` to
the ``make`` command, or editing ``mpconfigport.h`` or ``mpconfigboard.h``
to add
.. code-block:: c
#define MODULE_CEXAMPLE_ENABLED (1)
Note that the exact method depends on the port as they have different
structures. If not done correctly it will compile but importing will
fail to find the module.
To sum up, here's how the ``cexample`` module from the ``examples/usercmodule``
directory can be built for the unix port:
.. code-block:: bash
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 all
The build output will show the modules found::
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Or for your own project with a directory structure as shown above,
including both modules and building the stm32 port for example:
.. code-block:: bash .. code-block:: bash
cd my_project/micropython/ports/stm32 cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules CFLAGS_EXTRA=-DMODULE_EXAMPLE_ENABLED=1 all make USER_C_MODULES=../../../modules \
CFLAGS_EXTRA="-DMODULE_EXAMPLE1_ENABLED=1 -DMODULE_EXAMPLE2_ENABLED=1" all
Module usage in MicroPython Module usage in MicroPython
--------------------------- ---------------------------
Once built into your copy of MicroPython, the module implemented Once built into your copy of MicroPython, the module
in ``example.c`` above can now be accessed in Python just can now be accessed in Python just like any other builtin module, e.g.
like any other builtin module, eg
.. code-block:: python .. code-block:: python
import example import cexample
print(example.add_ints(1, 3)) print(cexample.add_ints(1, 3))
# should display 4 # should display 4

View File

@ -0,0 +1,34 @@
// Include MicroPython API.
#include "py/runtime.h"
// This is the function which will be called from Python as cexample.add_ints(a, b).
STATIC mp_obj_t example_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
// Extract the ints from the micropython input objects.
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
// Calculate the addition and convert to MicroPython object.
return mp_obj_new_int(a + b);
}
// Define a Python reference to the function above.
STATIC MP_DEFINE_CONST_FUN_OBJ_2(example_add_ints_obj, example_add_ints);
// Define all properties of the module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) },
{ 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);
// Define module object.
const mp_obj_module_t example_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&example_module_globals,
};
// Register the module to make it available in Python.
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule, MODULE_CEXAMPLE_ENABLED);

View File

@ -0,0 +1,9 @@
EXAMPLE_MOD_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(EXAMPLE_MOD_DIR)/examplemodule.c
# We can add our module folder to include paths if needed
# This is not actually needed in this example.
CFLAGS_USERMOD += -I$(EXAMPLE_MOD_DIR)
CEXAMPLE_MOD_DIR := $(USERMOD_DIR)

View File

@ -0,0 +1,17 @@
extern "C" {
#include <examplemodule.h>
// Here we implement the function using C++ code, but since it's
// declaration has to be compatible with C everything goes in extern "C" scope.
mp_obj_t cppfunc(mp_obj_t a_obj, mp_obj_t b_obj) {
// Prove we have (at least) C++11 features.
const auto a = mp_obj_get_int(a_obj);
const auto b = mp_obj_get_int(b_obj);
const auto sum = [&]() {
return mp_obj_new_int(a + b);
} ();
// Prove we're being scanned for QSTRs.
mp_obj_t tup[] = {sum, MP_ROM_QSTR(MP_QSTR_hellocpp)};
return mp_obj_new_tuple(2, tup);
}
}

View File

@ -0,0 +1,25 @@
#include <examplemodule.h>
// Define a Python reference to the function we'll make available.
// See example.cpp for the definition.
STATIC MP_DEFINE_CONST_FUN_OBJ_2(cppfunc_obj, cppfunc);
// Define all properties of the module.
// Table entries are key/value pairs of the attribute name (a string)
// and the MicroPython object reference.
// All identifiers and strings are written as MP_QSTR_xxx and will be
// optimized to word-sized integers by the build system (interned strings).
STATIC const mp_rom_map_elem_t cppexample_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cppexample) },
{ MP_ROM_QSTR(MP_QSTR_cppfunc), MP_ROM_PTR(&cppfunc_obj) },
};
STATIC MP_DEFINE_CONST_DICT(cppexample_module_globals, cppexample_module_globals_table);
// Define module object.
const mp_obj_module_t cppexample_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&cppexample_module_globals,
};
// Register the module to make it available in Python.
MP_REGISTER_MODULE(MP_QSTR_cppexample, cppexample_user_cmodule, MODULE_CPPEXAMPLE_ENABLED);

View File

@ -0,0 +1,5 @@
// Include MicroPython API.
#include "py/runtime.h"
// Declare the function we'll make available in Python as cppexample.cppfunc().
extern mp_obj_t cppfunc(mp_obj_t a_obj, mp_obj_t b_obj);

View File

@ -0,0 +1,12 @@
CPPEXAMPLE_MOD_DIR := $(USERMOD_DIR)
# Add our source files to the respective variables.
SRC_USERMOD += $(CPPEXAMPLE_MOD_DIR)/examplemodule.c
SRC_USERMOD_CXX += $(CPPEXAMPLE_MOD_DIR)/example.cpp
# Add our module directory to the include path.
CFLAGS_USERMOD += -I$(CPPEXAMPLE_MOD_DIR)
CXXFLAGS_USERMOD += -I$(CPPEXAMPLE_MOD_DIR)
# We use C++ features so have to link against the standard library.
LDFLAGS_USERMOD += -lstdc++

View File

@ -7,11 +7,13 @@ CFLAGS += \
-fprofile-arcs -ftest-coverage \ -fprofile-arcs -ftest-coverage \
-Wformat -Wmissing-declarations -Wmissing-prototypes \ -Wformat -Wmissing-declarations -Wmissing-prototypes \
-Wold-style-definition -Wpointer-arith -Wshadow -Wuninitialized -Wunused-parameter \ -Wold-style-definition -Wpointer-arith -Wshadow -Wuninitialized -Wunused-parameter \
-DMICROPY_UNIX_COVERAGE -DMICROPY_UNIX_COVERAGE \
-DMODULE_CEXAMPLE_ENABLED=1 -DMODULE_CPPEXAMPLE_ENABLED=1
LDFLAGS += -fprofile-arcs -ftest-coverage LDFLAGS += -fprofile-arcs -ftest-coverage
FROZEN_MANIFEST ?= $(VARIANT_DIR)/manifest.py FROZEN_MANIFEST ?= $(VARIANT_DIR)/manifest.py
USER_C_MODULES = $(TOP)/examples/usercmodule
MICROPY_ROM_TEXT_COMPRESSION = 1 MICROPY_ROM_TEXT_COMPRESSION = 1
MICROPY_VFS_FAT = 1 MICROPY_VFS_FAT = 1

View File

@ -49,6 +49,16 @@ print(buf.write(bytearray(16)))
# function defined in C++ code # function defined in C++ code
print("cpp", extra_cpp_coverage()) print("cpp", extra_cpp_coverage())
# test user C module
import cexample
print(cexample.add_ints(3, 2))
# test user C module mixed with C++ code
import cppexample
print(cppexample.cppfunc(1, 2))
# test basic import of frozen scripts # test basic import of frozen scripts
import frzstr1 import frzstr1

View File

@ -145,6 +145,8 @@ OSError
None None
None None
cpp None cpp None
5
(3, 'hellocpp')
frzstr1 frzstr1
frzstr1.py frzstr1.py
frzmpy1 frzmpy1