Welcome to micropython-usermod’s documentation!¶
Introduction¶
So, you have somehow bumped into micropython, fallen in love with it in an instance, broken your piggy bank, and run off, head over heels, to order a pyboard. You have probably paid extra for the expedited shipping. Once the pyboard arrived, you became excited like a puppy with a bone. You played with the hardware, learnt how to access the accelerometer, switch, LEDs, and temperature sensor, and you successfully communicated with other devices via the I2C, SPI, USART, or CAN interfaces. You have plugged the board in a computer, and driven someone crazy by emulating a seemingly disoriented mouse on it. You have even tried to divide by zero, just to see if the chip would go up in flames (this was vicious, by the way), and noticed that the interpreter smartly prevented such things from happening. You have written your own python functions, even compiled them into frozen modules, and burnt the whole damn thing onto the microcontroller. Then you have toyed with the on-board assembler, because you hoped that you could gain some astronomical factors in speed. (But you couldn’t.)
And yet, after all this, you feel somewhat unsatisfied. You find that you want to access the periphery in a special way, or you need some fancy function that, when implemented in python itself, seems to consume too much RAM, and takes an eternity to execute, and assembly, with its limitations, is just far too awkward for it. Or perhaps, you are simply dead against making your code easily readable by writing everything in python, and you want to hide the magic, just for the heck of it. But you still want to retain the elegance of python.
If, after thorough introspection and soul-searching, you have discovered these latter symptoms in yourself, you have two choices: either you despair, scrap your idea, and move on, or you learn how the heavy lifting behind the micropython facade is done, and spin your own functions, classes, and methods in C. As it turns out, it is not that hard, once you get the hang of it. The sole trick is to get the hang of it. And this is, where this document intends to play a role.
On the following pages, I would like to show how new functionality can be added and exposed to the python interpreter. I will try to discuss all aspects of micropython in an approachable way. Each concept will be presented in an implementation, stripped to the bare minimum, that you can compile right away, and try yourself. (The code here has been tested against micropython v.1.11.) At the end of each chapter, I will list the discussed code in its entirety, and I also include a link the the source, so that copying and pasting does not involve copious amounts of work. Moreover, I include a small demonstration, so that we can actually see that our code works. The code, as well as the source of this document are also available under https://github.com/v923z/micropython-usermod. The simplest way of getting started is probably cloning the repository with
git clone https://github.com/v923z/micropython-usermod.git
As for the source: all that you see here originates from a single jupyter notebook. That’s right, the documentation, the C source, the compilation, and the testing. You can find the notebook at https://github.com/v923z/micropython-usermod/blob/master/docs/micropython-usermod.ipynb. And should you wonder, everything is under the MIT licence.
I start out with a very simple module and slowly build upon it. At the very end of the discussion, I will outline my version of a general-purpose math library, similar to numpy. In fact, it was when I was working on this math module that I realised that a decent programming guide to micropython is sorely missing, hence this document. Obviously, numpy is a gigantic library, and we are not going to implement all aspects of it. But we will be able to define efficiently stored arrays on which we can do vectorised computations, work with matrices, invert and contract them, fit polynomials to measurement data, and get the Fourier transform of an arbitrary sequence. I do hope that you find the agenda convincing enough!
One last comment: I believe, all examples in this document could be implemented with little effort in python itself, and I am definitely not advocating the inclusion of such trivial cases in the firmware. I chose these examples on two grounds: First, they are all simple, almost primitive, but for this very reason, they demonstrate a single idea without distraction. Second, having a piece of parallel python code is useful insofar as it tells us what to expect, and it also encourages us to implement the C version such that it results in pythonic functions.
Code blocks¶
You’ll encounter various kinds of code blocks in this document. These have various scopes, which are listed here:
- if a code block begins with an exclamation mark, the content is meant to be executed on the command line.
- if the code block looks like a piece of python code, it should be run in a python interpreter.
- if the heading of the code block is
%%micropython
, then, well, you guessed it right, the content should be passed to the micropython interpreter of your port of choice. - other code segments can be C code, or a makefile. These should be easy to recognise, because both of these have a header with a link to the location of the file.
The micropython code base¶
Since we are going to test our code mainly on the unix port, we set that as the current working directory.
!cd ../../micropython/ports/unix/
/home/v923z/sandbox/micropython/v1.11/micropython/ports/unix
The micropython codebase itself is set up a rather modular way. Provided you cloned the micropython repository with
!git clone https://github.com/micropython/micropython.git
onto your computer, and you look at the top-level directories, you will see something like this:
!ls ../../../micropython/
ACKNOWLEDGEMENTS docs lib pic16bit teensy zephyr
bare-arm drivers LICENSE py tests
cc3200 esp8266 logo qemu-arm tools
CODECONVENTIONS.md examples minimal README.md unix
CONTRIBUTING.md extmod mpy-cross stmhal windows
Out of all the directoties, at least two are of particular interest.
Namely, /py/
, where the python interpreter is implemented, and
/ports/
, which contains the hardware-specific files. All questions
pertaining to programming micropython in C can be answered by browsing
these two directories, and perusing the relevant files therein.
User modules in micropython¶
Beginning with the 1.10 version of micropython, it became quite simple
to add a user-defined C module to the firmware. You simply drop two or
three files in an arbitrary directory, and pass two compiler flags to
make
like so:
!make USER_C_MODULES=../../../user_modules CFLAGS_EXTRA=-DMODULE_EXAMPLE_ENABLED=1 all
Here, the USER_C_MODULES
variable is the location (relative to the
location of make
) of your files, while CFLAGS_EXTRA
defines the
flag for your particular module. This is relevant, if you have many
modules, but you want to include only some of them.
Alternatively, you can set the module flags in mpconfigport.h
(to be
found in the port’s root folder, for which you are compiling) as
#define MODULE_SIMPLEFUNCTION_ENABLED (1)
#define MODULE_SIMPLECLASS_ENABLED (1)
#define MODULE_SPECIALCLASS_ENABLED (1)
#define MODULE_KEYWORDFUNCTION_ENABLED (1)
#define MODULE_CONSUMEITERABLE_ENABLED (1)
#define MODULE_VECTOR_ENABLED (1)
#define MODULE_RETURNITERABLE_ENABLED (1)
#define MODULE_PROFILING_ENABLED (1)
#define MODULE_MAKEITERABLE_ENABLED (1)
#define MODULE_SUBSCRIPTITERABLE_ENABLED (1)
#define MODULE_SLICEITERABLE_ENABLED (1)
#define MODULE_VARARG_ENABLED (1)
#define MODULE_STRINGARG_ENABLED (1)
and then call make
without the CFLAGS_EXTRA
flag:
!make USER_C_MODULES=../../../user_modules all
This separation of the user code from the micropython code base is
definitely a convenience, because it is much easier to keep track of
changes, and also because you can’t possibly screw up micropython
itself: you can also go back to a working piece of firmware by dropping
the USER_C_MODULES
argument of make
.
micropython internals¶
Before exploring the exciting problem of micropython function implementation in C, we should first understand how python objects are stored and treated at the firmware level.
Object representation¶
Whenever you write
>>> a = 1
>>> b = 2
>>> a + b
on the python console, first the two new variables, a
, and b
are
created and a reference to them is stored in memory. Then the value of
1, and 2, respectively, will be associated with these variables. In the
last line, when the sum is to be computed, the interpreter somehow has
to figure out, how to decipher the values stored in a
, and b
: in
the RAM, these two variables are just bytes, but depending on the type
of the variable, different meanings will be associated with these bytes.
Since the type cannot be known at compile time, there must be a
mechanism for keeping stock of this extra piece of information. This is,
where mp_obj_t
, defined in obj.h
, takes centre stage.
If you cast a cursory glance at any of the C functions that are exposed to the python interpreter, you will always see something like this
mp_obj_t some_function(mp_obj_t some_variable, ...) {
// some_variable is converted to fundamental C types (bytes, ints, floats, pointers, structures, etc.)
...
}
Variables of type mp_obj_t
are passed to the function, and the
function returns the results as an object of type mp_obj_t
. So, what
is all this fuss this about? Basically, mp_obj_t
is nothing but an
8-byte segment of the memory, where all concrete objects are encoded.
There can be various object encodings. E.g., in the A
encoding,
integers are those objects, whose rightmost bit in this 8-byte
representation is set to 1, and the value of the integer can then be
retrieved by shifting these 8 bytes by one to the right, and then
applying a mask. In the B
encoding, the variable is an integer, if
its value is 1, when ANDed with 3, and the value will be returned, if we
shift the 8 bytes by two to the right.
Type checking¶
Fortunately, we do not have to be concerned with the representations and
the shifts, because there are pre-defined macros for such operations.
So, if we want to find out, whether some_variable
is an integer, we
can inspect the value of the Boolean
MP_OBJ_IS_SMALL_INT(some_variable)
The integer value stored in some_variable
can then be gotten by
calling MP_OBJ_SMALL_INT_VALUE
:
int value_of_some_variable = MP_OBJ_SMALL_INT_VALUE(some_variable);
These decoding steps take place somewhere in the body of
some_function
, before we start working with native C types. Once we
are done with the calculations, we have to return an mp_obj_t
, so
that the interpreter can handle the results (e.g., show it on the
console, or pipe it to the next instruction). In this case, the encoding
is done by calling
mp_obj_new_int(value_of_some_variable)
More generic types can be treated with the macro mp_obj_is_type
,
which takes the object as the first, and a pointer to the type as the
second argument. Now, if you want to find out, whether some_variable
is a tuple, you could apply the mp_obj_is_type
macro,
mp_obj_is_type(some_variable, &mp_type_tuple)
While the available types can be found in obj.h
, they all follow the
mp_type_
+ python type pattern, so in most cases, it is not even
necessary to look them up. We should also note that it is also possible
to define new types. When done properly, mp_obj_is_type
can be
called on objects with this new type, i.e.,
mp_obj_is_type(myobject, &my_type)
will just work. We return to this question later.
python constants¶
At this point, we should mention that python constants,True
(in C
mp_const_true
), False
(in C mp_const_false
), None
(in C
mp_const_none
) and the like are also defined in obj.h
. These are
objects of type mp_obj_t
, as almost anything else, so you can return
them from a function, when the function is meant to return directly to
the interpreter.
Developing your first module¶
Having seen, what the python objects look like to the interpreter, we can start with our explorations in earnest. We begin by adding a simple module to micropython. The module will have a single function that takes two numbers, and adds them. I know that this is the most exciting thing since sliced bread, and you have always wondered, why there isn’t a built-in python function for such a fascinating task. Well, wonder no more! From this moment, your micropython will have one.
First I show the file in its entirety (20 something lines all in all), and then discuss the parts.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simplefunction/simplefunction.c
#include "py/obj.h"
#include "py/runtime.h"
STATIC mp_obj_t simplefunction_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
return mp_obj_new_int(a + b);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(simplefunction_add_ints_obj, simplefunction_add_ints);
STATIC const mp_rom_map_elem_t simplefunction_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_simplefunction) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&simplefunction_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(simplefunction_module_globals, simplefunction_module_globals_table);
const mp_obj_module_t simplefunction_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&simplefunction_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_simplefunction, simplefunction_user_cmodule, MODULE_SIMPLEFUNCTION_ENABLED);
Header files¶
A module will not be too useful without at least two includes:
py/obj.h
, where all the relevant constants and macros are defined,
and py/runtime.h
, which contains the declaration of the interpreter.
Many a time you will also need py/builtin.h
, where the python
built-in functions and modules are declared.
Defining user functions¶
After including the necessary headers, we define the function that is
going to do the heavy lifting. By passing variables of mp_obj_t
type, we make sure that the function will be able to accept values from
the python console. If you happen to have an internal helper function in
your module that is not exposed in python, you can pass whatever type
you need. Similarly, by returning an object of mp_obj_t
type, we
make the results visible to the interpreter, i.e., we can assign the
value returned to variables.
The downside of passing mp_obj_t
s around is that you cannot simply
assign them to usual C variables, i.e., when you want to operate on
them, you have to extract the values first. This is why we have to
invoke the mp_obj_get_int()
function, and conversely, before
returning the results, we have to do a type conversion to mp_obj_t
by calling mp_obj_new_int()
. These are the decoding/encoding steps
that we discussed above.
Referring to user functions¶
We have now a function that should be sort of OK (there is no error checking whatsoever, so you are at the mercy of the firmware, when, e.g., you try to pass a float to the function), but the python interpreter still cannot work with. For that, we have to turn our function into a function object. This is what happens in the line
STATIC MP_DEFINE_CONST_FUN_OBJ_2(simplefunction_add_ints_obj, simplefunction_add_ints);
The first argument of the macro is the name of the function object to
which our actual function, the last argument, will be bound. Now, these
MP_DEFINE_CONST_FUN_OBJ_*
macros, defined in the header file
py/obj.h
(one more reason not to forget about py/obj.h
), come in
seven flavours, depending on what kind of, and how many arguments the
function is supposed to take. In the example above, our function is
meant to take two arguments, hence the 2 at the end of the macro name.
Functions with 0 to 4 arguments can be bound in this way.
But what, if you want a function with more than four arguments, as is the case many a time in python? Under such circumstances, one can make use of the
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(obj_name, n_args_min, fun_name);
macro, where the second argument, an integer, gives the minimum number of arguments. The number of arguments can be bound from above by wrapping the function with
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(obj_name, n_args_min, n_args_max, fun_name);
Later we will see, how we can define functions that can also take keyword arguments.
At this point, we are more or less done with the C implementation of our
function, but we still have to expose it. This we do by adding a table,
an array of key/value pairs to the globals of our module, and bind the
table to the _module_globals
variable by applying the
MP_DEFINE_CONST_DICT
macro. This table should have at least one
entry, the name of the module, which is going to be stored in the string
MP_QSTR___name__
.
These MP_QSTR_
items are the C representation of the python strings
that come at the end of them. So, MP_QSTR_foo_bar
in C will be
turned into a name, foo_bar
, in python. foo_bar
can be a
constant, a function, a class, a type, etc., and depending on what is
associated with it, different things will happen on the console, when
foo_bar
is invoked. But the crucial point is that, if you want
foo_bar
to have any meaning in python, then somewhere in your C
code, you have to define MP_QSTR_foo_bar
.
The second key-value pair of the table is the pointer to the function
that we have just implemented, and the name that we want to call the
functions in python itself. So, in the example below, our
simplefunction_add_ints
function will be invoked, when we call
add_ints
in the console.
STATIC const mp_rom_map_elem_t simplefunction_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_simplefunction) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&simplefunction_add_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(simplefunction_module_globals, simplefunction_module_globals_table);
This three-step pattern is common to all function implementations, so I repeat it here:
- implement the function
- then turn it into a function object (i.e., call the relevant form of MP_DEFINE_CONST_FUN_OBJ_*)
- and finally, register the function in the name space of the module (i.e., add it to the module’s globals table, and turn the table into a dictionary by applying MP_DEFINE_CONST_DICT)
It doesn’t matter, whether our function takes positional arguments, or keyword argument, or both, these are the required steps.
Having defined the function object, we have finally to register the module with
MP_REGISTER_MODULE(MP_QSTR_simplefunction, simplefunction_user_cmodule, MODULE_SIMPLEFUNCTION_ENABLED);
This last line is particularly useful, because by setting the
MODULE_SIMPLEFUNCTION_ENABLED
variable in mpconfigport.h
, you
can selectively exclude modules from the linking, i.e., if in
mpconfigport.h
, which should be in the root directory of the port
you want to compile for,
#define MODULE_SIMPLEFUNCTION_ENABLED (1)
then simplefunction
will be included in the firmware, while with
#define MODULE_SIMPLEFUNCTION_ENABLED (0)
the module will be dropped, even though the source is in your modules folder. (N.B.: the module will still be compiled, but not linked.)
Compiling our module¶
The implementation is done, and we would certainly like to see some
results. First we generate a makefile, which will be inserted in the
module’s own directory, simplefunction/
.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simplefunction/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/simplefunction.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
If mpconfigport.h
is augmented with
#define MODULE_SIMPLEFUNCTION_ENABLED (1)
you should be able to compile the module above by calling
!make clean
!make USER_C_MODULES=../../../usermod/snippets all
As mentioned earlier, if you do not want to touch anything in the micropython code base, you can simply pass the definition to make as
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SIMPLEFUNCTION_ENABLED=1 all
You will also note that we ran make clean
before the compilation.
This is always good practice, when you are developing your own modules.
We can then test the module as
%%micropython
import simplefunction
print(simplefunction.add_ints(123, 456))
579
What a surprise! It works! It works!
Compiling for the microcontroller¶
As pointed out at the very beginning, our first module was compiled for
the unix port, and that it, why we set ../../micropython/ports/unix/
as our working directory. In case, we would like to compile for the
microcontroller, we would have to modify the mpconfigport.h
file of
the port (e.g., in micropython/ports/stm32/
) as shown in Section
User modules.
Next, in the compilation command, one has to specify the target board,
e.g., pyboard, version 1.1, and probably the path to the cross-compiler,
if that could not be installed system-wide. You would issue the make
command in the directory of the port, e.g.,
micropython/ports/stm32/
, and the path in the CROSS_COMPILE
argument must be either absolute, or given relative to
micropython/ports/stm32/
.
make BOARD=PYBV11 CROSS_COMPILE=<Path where you uncompressed the toolchain>/bin/arm-none-eabi-
You will find your firmware under
micropython/ports/stm32/build-PYBV11/firmware.dfu
, and you can
upload it by issuing
!python ../../tools/pydfu.py -u build-PYBV11/firmware.dfu
on the command line. More detailed explanation can be found under https://github.com/micropython/micropython/wiki/Pyboard-Firmware-Update.
Module constants¶
We have just seen, how we add a function to python. But functions are not the only objects that can be attached to a module, and of particular interest are constants. If for nothing else, you can give your module a version number. So, let us see, how that can be achieved.
Contstants, if they are true to their name, won’t change at run time, hence, they can be stored in ROM. We have already seen this, because the globals table of our very first module kicked out with the line
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_simplefunction) }
Here, the MP_QSTR_simplefunction
was a constant, namely, the string
simplefunction
stored in ROM. This is why it is wrapped by the macro
MP_ROM_QSTR
. There are two other MP_ROM
macros defined in
obj.h
, namely, MP_ROM_INT
, and MP_ROM_PTR
.
Integer constants¶
It should not be a big surprise that the MP_ROM_INT
macro generates
ROM objects from integers. Thus, the following code will give you the
magic constant 42:
#define MAGIC_CONSTANT 42
...
{ MP_ROM_QSTR(MP_QSTR_magic), MP_ROM_INT(MAGIC_CONSTANT) },
...
Strings¶
Now, in MP_QSTR_simplefunction
, simplefunction
is a well-behaved
string, containing no special characters. But are we doomed, if we do
want to print out a version string, which would probably look like
1.2.3
, or something similar? And should we give up all hope, if our
string contains an underscore? The answer to these questions is no, and
no! This is, where the MP_ROM_PTR
macro comes to the rescue.
In general, MP_ROM_PTR
will take the address of an object, and
convert it to a 32-bit unsigned integer. At run time, micropython
works with this integer, if it has to access the constant. And this is
exactly what happens in the second line of the globals table of
simplefunction
:
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&simplefunction_add_ints_obj) },
we associated the string add_ints
(incidentally, also stored in ROM)
with the 32-bit unsigned integer generated from the address of
simplefunction_add_ints_obj
. So, the bottom line is, if we can
somehow get hold of the address of an object, we can wrap it with
MP_ROM_PTR
, and we are done.
Thus, if we want to define a string constant, we have to convert it to
something that has an address. The MP_DEFINE_STR_OBJ
of objstr.h
does exactly that:
STATIC const MP_DEFINE_STR_OBJ(version_string_obj, "1.2.3");
takes 1.2.3
as a string, and turns is into a micropython object of
type mp_obj_str_t
. After this, &version_string_obj
can be passed
to the MP_ROM_PTR
macro.
Tuples¶
We don’t have to be satisfied with integers and strings, we can
definitely go further. There is a python type, the tuple
, that is,
by definition, constant (not mutable), and for this reason, we can
easily define a tuple type module constant. objtuple.h
defines
mp_rom_obj_tuple_t
for this purpose. This is a structure with three
members, and looks like this:
const mp_rom_obj_tuple_t version_tuple_obj = {
{&mp_type_tuple},
2,
{
MP_ROM_INT(1),
MP_ROM_PTR(&version_string_obj),
},
};
The first member defines the base type of the object, the second is the
number of elements of the tuple that we want to define, and the third is
itself a structure, listing the tuple elements. The key point here is
that we can apply the address-of operator to version_tuple_obj
, and
pass it to the MP_ROM_PTR
macro.
https://github.com/v923z/micropython-usermod/tree/master/snippets/constants/constants.c
#include "py/obj.h"
#include "py/runtime.h"
#include "py/objstr.h"
#include "py/objtuple.h"
#define MAGIC_CONSTANT 42
STATIC const MP_DEFINE_STR_OBJ(version_string_obj, "1.2.3");
const mp_rom_obj_tuple_t version_tuple_obj = {
{&mp_type_tuple},
2,
{
MP_ROM_INT(1),
MP_ROM_PTR(&version_string_obj),
},
};
STATIC const mp_rom_map_elem_t constants_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_constants) },
{ MP_ROM_QSTR(MP_QSTR___version__), MP_ROM_PTR(&version_string_obj) },
{ MP_ROM_QSTR(MP_QSTR_magic), MP_ROM_INT(MAGIC_CONSTANT) },
{ MP_ROM_QSTR(MP_QSTR_version_tuple), MP_ROM_PTR(&version_tuple_obj) },
};
STATIC MP_DEFINE_CONST_DICT(constants_module_globals, constants_module_globals_table);
const mp_obj_module_t constants_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&constants_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_constants, constants_user_cmodule, MODULE_CONSTANTS_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/constants/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/constants.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_CONSTANTS_ENABLED=1 all
One comment before trying out what we have just implemented: the module is definitely pathological. If all you need is a set of constants organised in some way, then you should write it in python. There is nothing to be gained by working in C, while python is much more flexible.
%%micropython -unix 1
import constants
print(constants.magic)
print(constants.__version__)
print(constants.version_tuple)
42
1.2.3
(1, '1.2.3')
Error handling¶
There will be cases, when something goes wrong, and you want to bail out in an elegant way. If bailing out, and elegance can be used in the same sentence, that is. Depending on what kind of difficulty you are facing, you can indicate this to the user in different ways, and there seems to be a divide between programmers as to whether one should return an error code, or do something else.
But in the python world, the most common method is to raise some sort of exception, and let the user handle the problem. In the following snippet, we will see a couple of ways of going about exceptions. We implement a single function that raises an exception, no matter what. When developing user-friendly code, that is as vicious as you can get, I guess.
First, the code listing:
https://github.com/v923z/micropython-usermod/tree/master/snippets/sillyerrors/sillyerrors.c
#include "py/obj.h"
#include "py/builtin.h"
#include "py/runtime.h"
#include <stdlib.h>
STATIC mp_obj_t mean_function(mp_obj_t error_code) {
int e = mp_obj_get_int(error_code);
if(e == 0) {
mp_raise_msg(&mp_type_ZeroDivisionError, "thou shall not try to divide by 0 on a microcontroller!");
} else if(e == 1) {
mp_raise_msg(&mp_type_IndexError, "dude, that was a silly mistake!");
} else if(e == 2) {
mp_raise_TypeError("look, chap, you can't be serious!");
} else if(e == 3) {
mp_raise_OSError(e);
} else if(e == 4) {
char *buffer;
buffer = malloc(100);
sprintf(buffer, "you are really out of luck today: error code %d", e);
mp_raise_NotImplementedError(buffer);
} else {
mp_raise_ValueError("sorry, you've exhausted all your options");
}
return mp_const_false;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mean_function_obj, mean_function);
STATIC const mp_rom_map_elem_t sillyerrors_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sillyerrors) },
{ MP_ROM_QSTR(MP_QSTR_mean), MP_ROM_PTR(&mean_function_obj) },
};
STATIC MP_DEFINE_CONST_DICT(sillyerrors_module_globals, sillyerrors_module_globals_table);
const mp_obj_module_t sillyerrors_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&sillyerrors_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_sillyerrors, sillyerrors_user_cmodule, MODULE_SILLYERRORS_ENABLED);
Now, not all exceptions are created equal. Some are more exceptional
than the others: ValueError
, TypeError
, OSError
, and
NotImplementedError
can be raised with the syntax
mp_raise_ValueError("wrong value");
which will, in addition to raising the exception at the C level (i.e., interrupting the execution of the code), also return a pretty traceback:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: wrong value
with the error message that we supplied to the mp_raise_ValueError
function. If you want to have a traceback message that is not a
compile-time constant, you could deal with the problem as in case 4 in
the function mean_function
. Such a message might be useful, if the
nature of the exception is somehow related to a quantity that is not
known at compile time, e.g., if you have a function that should not ever
run, if the up-time is shorter than some predefined value. Of course,
one can just say that the “microcontroller hasn’t run long enough yet”,
and this is a pretty good constant string, but perhaps we can give the
user a bit more information, if we can also indicate, how much time is
still missing.
Other exceptions can be raised as in the e == 1
case, with the
mp_raise_msg(&mp_type_IndexError, "dude, that was a silly mistake!")
function. Here one also has to specify the type of the exception, which
is always of the form mp_type_
. A complete list can be found in
obj.h
.
Incidentally, mp_raise_ValueError
, mp_raise_TypeError
, and
mp_raise_NotImplementedError
are nothing but a wrapper for
mp_raise_msg
, which in turn is a wrapper for nlr_raise
of
nlr.c/nlr.h
. The OSError
is somewhat curious in this respect,
because it is raised directly through nlr_raise
, and its argument is
not a string, but an integer error code. All these wrappers are defined
in runtime.c
, by the way.
In our ultimate mean function, we raised a lot of exceptions by now, but
we still have to return some value, because the function signature
stipulates that, and the compiler would be unsatisfied otherwise, even
though code execution will actually never reach the return statement.
Since we are in denial mode anyway, I cast my vote for a return value of
mp_const_false
. mp_const_none
was the other candidate, but ended
up as the runner-up.
I think, it is high time to compile our code.
https://github.com/v923z/micropython-usermod/tree/master/snippets/sillyerrors/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/sillyerrors.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SILLYERRORS_ENABLED=1 all
%%micropython
import sillyerrors
print(sillyerrors.mean(0))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
ZeroDivisionError: thou shall not try to divide by 0 on a microcontroller!
%%micropython
import sillyerrors
print(sillyerrors.mean(1))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
IndexError: dude, that was a silly mistake!
%%micropython
import sillyerrors
print(sillyerrors.mean(2))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
TypeError: look, chap, you can't be serious!
%%micropython
import sillyerrors
print(sillyerrors.mean(3))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
OSError: 3
%%micropython
import sillyerrors
print(sillyerrors.mean(4))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
NotImplementedError: you are really out of luck today: error code 4
One can’t but wonder, why we had to invoke our mean
function in four
separate statements, and why we couldn’t execute everything in a nice
nifty package like
%%micropython
import sillyerrors
print(sillyerrors.mean(0))
print(sillyerrors.mean(1))
print(sillyerrors.mean(2))
print(sillyerrors.mean(3))
print(sillyerrors.mean(4))
Traceback (most recent call last):
File "/dev/shm/micropython.py", line 3, in <module>
ZeroDivisionError: you shall not try to divide by 0 on a microcontroller!
Well, we could have, but since we specifically raised an exception in the first statement, our code would never have gotten beyond
sillyerror.mean(0)
After all, this is what exceptions do: they interrupt the execution of the code.
Argument parsing¶
In practically all cases, you will have to inspect the arguments of your function. Even if you can resort to functions in the micropython implementation, that simply means that the burden of inspection was taken off your shoulders, but not that the inspection does not happen at all. In this section, we are going to see what we can do with both positional, and keyword arguments, and how we can retrieve their values.
Positional arguments¶
Known number of arguments¶
A known number of positional arguments are pretty much a done deal: we
have seen how to get the C values of such arguments: in our very first
module, we called mp_obj_get_int()
, because we wanted to sum two
integers. Should we like to work with float, we could call
mp_obj_get_float()
. (This function will properly work, if the value
is an integer, by the way.)
If we have a more complicated construct, like a tuple or a list, we can turn the argument into a pointer with
mp_obj_t some_function(mp_obj_t object_in) {
mp_obj_tuple_t *object = MP_OBJ_TO_PTR(object_in);
...
}
and continue with *object
. We can then retrieve the tuple’s
structure members with object->items
(the elements in the tuple),
and object->len
(the length of the tuple). This procedure works even
with newly-defined object types. A complete example can be found in
Section Creating new types:
typedef struct _vector_obj_t {
mp_obj_base_t base;
float x, y, z;
} vector_obj_t;
mp_obj_t some_function(mp_obj_t object_in) {
vector_obj_t *vector = MP_OBJ_TO_PTR(object_in);
...
}
Unknown number of arguments¶
Now, we pointed out that the macros generating the function objects can be of the form
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(some_function_obj, n_argmin, n_argmax, some_function);
In such a case, we surely can’t just enumerate the arguments of the function without any checks, especially, that we don’t even know how far we have to go, and the behaviour of the function can depend on the number of arguments. What shall we do in such an instance?
We have to reckon that the signature of a function with a variable number of arguments looks like
mp_obj_t some_function(size_t n_args, const mp_obj_t *args) {
if (n_args == 2) {
...
}
...
}
and the first argument of the C function will store the number of
positional arguments of the python function. Once n_args
is known,
we are set. It is important to note that the work is done by the
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN
macro, we do not have to set up
the C function in any particular way.
Here is a small example that will drive this point home.
https://github.com/v923z/micropython-usermod/tree/master/snippets/vararg/vararg.c
#include "py/obj.h"
#include "py/runtime.h"
STATIC mp_obj_t vararg_function(size_t n_args, const mp_obj_t *args) {
if(n_args == 0) {
printf("no arguments supplied\n");
} else if(n_args == 1) {
printf("this is a %lu\n", mp_obj_get_int(args[0]));
} else if(n_args == 2) {
printf("hm, we will sum them: %lu\n", mp_obj_get_int(args[0]) + mp_obj_get_int(args[1]));
} else if(n_args == 3) {
printf("Look at that! A triplet: %lu, %lu, %lu\n", mp_obj_get_int(args[0]), mp_obj_get_int(args[1]), mp_obj_get_int(args[2]));
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(vararg_function_obj, 0, 3, vararg_function);
STATIC const mp_rom_map_elem_t vararg_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_vararg) },
{ MP_ROM_QSTR(MP_QSTR_vararg), MP_ROM_PTR(&vararg_function_obj) },
};
STATIC MP_DEFINE_CONST_DICT(vararg_module_globals, vararg_module_globals_table);
const mp_obj_module_t vararg_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&vararg_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_vararg, vararg_user_cmodule, MODULE_VARARG_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/vararg/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/vararg.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_VARARG_ENABLED=1 all
%%micropython
import vararg
vararg.vararg()
vararg.vararg(1)
vararg.vararg(10, 20)
vararg.vararg(1, 22, 333)
no arguments supplied
this is a 1
hm, we will sum them: 30
Look at that! A triplet: 1, 22, 333
Working with strings¶
We have discussed numerical values in micropython at length. We know how
we convert an mp_obj_t
object to a native C type, and we also know,
how we can turn an integer or float into an mp_obj_t
, and return it
at the end of the function. The key components were the
mp_obj_get_int()
, mp_obj_new_int()
, and mp_obj_get_float()
,
and mp_obj_new_float()
functions. Later we will see, what we can do
with various iterables, like lists and tuples, but before that, I would
like to explain, how one handles strings. (Strings are also iterables in
python, by the way, however, they also have a native C equivalent.)
At the beginning, we said that in micropython, almost everything is an
mp_obj_t
object. Strings are no exception: however, the mp_obj_t
that denotes the string does not store its value, but a pointer to the
memory location, where the characters are stored. The reason is rather
trivial: the mp_obj_t
has a size of 8 bytes, hence, the object can’t
possibly store a string that is longer than 7 bytes. (The same applies
to more complicated objects, e.g., lists, or tuples.)
Now, the procedure of working with the string would kick out with
retrieving the pointer, and then we could increment its value till we
encounter the \0
character, which indicates that the string has
ended. Fortunately, micropython has a handy macro for retrieving the
string’s value and its length, so we don’t have to concern ourselves
with the really low-level stuff. For the string utilities, we should
include py/objstr.h
(for the micropython things), and string.h
(for strcpy
). py/objstr.c
contains a number of tools for string
manipulation. Before you try to implement your own functions, it might
be worthwhile to check that out. You might find something useful.
Our next module is going to take a single string as an argument, print out its length (you already know, how to return the length, don’t you?), and return the contents backwards. All this in 33 lines.
https://github.com/v923z/micropython-usermod/tree/master/snippets/stringarg/stringarg.c
#include <string.h>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/objstr.h"
#define byteswap(a,b) char tmp = a; a = b; b = tmp;
STATIC mp_obj_t stringarg_function(const mp_obj_t o_in) {
mp_check_self(mp_obj_is_str_or_bytes(o_in));
GET_STR_DATA_LEN(o_in, str, str_len);
printf("string length: %lu\n", str_len);
char out_str[str_len];
strcpy(out_str, (char *)str);
for(size_t i=0; i < (str_len-1)/2; i++) {
byteswap(out_str[i], out_str[str_len-i-1]);
}
return mp_obj_new_str(out_str, str_len);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(stringarg_function_obj, stringarg_function);
STATIC const mp_rom_map_elem_t stringarg_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_stringarg) },
{ MP_ROM_QSTR(MP_QSTR_stringarg), MP_ROM_PTR(&stringarg_function_obj) },
};
STATIC MP_DEFINE_CONST_DICT(stringarg_module_globals, stringarg_module_globals_table);
const mp_obj_module_t stringarg_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&stringarg_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_stringarg, stringarg_user_cmodule, MODULE_STRINGARG_ENABLED);
The macro defined in objstr.h
takes three arguments, out of which
only the first one is actually defined. The other two are defined in the
macro itself. So, in the line
GET_STR_DATA_LEN(o_in, str, str_len);
only o_in
is known at the moment the macro is called, str
, which
will be a pointer to type character, and str_len
, which is of type
size_t
, and holds the length of the string, are created by
GET_STR_DATA_LEN
itself. This is, why we can later stick
str_len
, and str
into print statements, though, we never
declared these variables.
After GET_STR_DATA_LEN
has been called, we are in C land. First, we
print out the length, then reverse the string. But why can’t we do the
string inversion on the original string, and why do we have to declare a
new variable, out_str
? The reason for that is that the
GET_STR_DATA_LEN
macro declares a const
string, which we can’t
change anymore, so we have to copy the content (strcpy
from
string.h
), and swap the bytes in out_str
. When doing so, we
should keep in mind that the very last byte in the string is the
termination character, hence, we exchange the i
th position with
the str_len-i-1
th position. If you fail to notice the -1
,
you’ll end up with an empty string: even though the byte swapping would
run without complaints, the very first byte would be equal to \0
.
At the very end, we return from our function with a call to
mp_obj_new_str
, which creates a new mp_obj_t
object that points
to the content of the string. And we are done! All there is left to do
is compilation. Let’s take care of that!
https://github.com/v923z/micropython-usermod/tree/master/snippets/stringarg/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/stringarg.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_STRINGARG_ENABLED=1 all
%%micropython
import stringarg
print(stringarg.stringarg('...krow ta eludom gragnirts eht'))
string length: 31
the stringarg module at work...
Keyword arguments¶
One of the most useful features of python is that functions can accept positional as well as keyword arguments, thereby providing a very flexible and instructive function interface. (Instructive, insofar as the intent of a variable is very explicit, even at the user level.) In this subsection, we will learn how the processing of keyword arguments is done. Our new module will be the sexed-up version of our very first one, where we added two integers. We will do the same here, except that the second argument will be a keyword, and will assume a default value of 0.
Before jumping into the implementation, we should contemplate the task
for a second. It does not matter, whether we have positional or keyword
arguments, at one point, the interpreter has to turn all arguments into
a deterministic sequence of objects. We stipulate this sequence in the
constant variable called allowed_args[]
. This is an array of type
mp_arg_t
, which is nothing but a structure with two uint16
values, and a union named mp_arg_val_t
. This union holds the default
value and the type of the variable that we want to pass. The
mp_arg_t
structure, defined in runtime.h
, looks like this:
typedef struct _mp_arg_t {
uint16_t qst;
uint16_t flags;
mp_arg_val_t defval;
} mp_arg_t;
The last member, mp_arg_val_t
is
typedef union _mp_arg_val_t {
bool u_bool;
mp_int_t u_int;
mp_obj_t u_obj;
mp_rom_obj_t u_rom_obj;
} mp_arg_val_t;
Keyword arguments come in three flavours: MP_ARG_BOOL
,
MP_ARG_INT
, and MP_ARG_OBJ
.
Keyword arguments with numerical values¶
And now the implementation:
https://github.com/v923z/micropython-usermod/tree/master/snippets/keywordfunction/keywordfunction.c
#include <stdio.h>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/builtin.h"
STATIC mp_obj_t keywordfunction_add_ints(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_a, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_b, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
int16_t a = args[0].u_int;
int16_t b = args[1].u_int;
printf("a = %d, b = %d\n", a, b);
return mp_obj_new_int(a + b);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(keywordfunction_add_ints_obj, 1, keywordfunction_add_ints);
STATIC const mp_rom_map_elem_t keywordfunction_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_keywordfunction) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), (mp_obj_t)&keywordfunction_add_ints_obj },
};
STATIC MP_DEFINE_CONST_DICT(keywordfunction_module_globals, keywordfunction_module_globals_table);
const mp_obj_module_t keywordfunction_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&keywordfunction_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_keywordfunction, keywordfunction_user_cmodule, MODULE_KEYWORDFUNCTION_ENABLED);
One side effect of a function with keyword arguments is that we do not have to care about the arguments in the C implementation: the argument list is always the same, and it is passed in by the interpreter: the number of arguments of the python function, an array with the positional arguments, and a map for the keyword arguments.
After parsing the arguments with mp_arg_parse_all
, whatever was at
the zeroth position of allowed_args[]
will be called args[0]
,
the object at the first position of allowed_args[]
will be turned
into args[1]
, and so on.
This is, where we also define, what the name of the keyword argument is
going to be: whatever comes after MP_QSTR_
. But hey, presto! The
name should be an integer with 16 bits, shouldn’t it? After all, this is
the first member of mp_arg_t
. So what the hell is going on here?
Well, for the efficient use of RAM, all MP_QSTRs are turned into
unint16_t
internally. This applies not only to the names in
functions with keyword arguments, but also for module and function
names, in the _module_globals_table[]
.
The second member of the mp_arg_t
structure is the flags that
determine, e.g., whether the argument is required, if it is of integer
or mp_obj_t
type, and whether it is a positional or a keyword
argument. These flags can be combined by ORing them, as we have done in
the example above.
The last member in mp_arg_t
is the default value. Since this is a
member variable, when we make use of it, we have to extract the value by
adding .u_int
to the argument.
When turning our function into a function object, we have to call a
special macro, MP_DEFINE_CONST_FUN_OBJ_KW
, defined in obj.h
,
which is somewhat similar to MP_DEFINE_CONST_FUN_OBJ_VAR
: in
addition to the function object and the function, one also has to
specify the minimum number of arguments in the python function.
Other examples on passing keyword arguments can be found in some of the
hardware implementation files, e.g., ports/stm32/pyb_i2c.c
, or
ports/stm32/pyb_spi.c
.
Now, let us see, whether we can add two numbers here.
https://github.com/v923z/micropython-usermod/tree/master/snippets/keywordfunction/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/keywordfunction.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_KEYWORDFUNCTION_ENABLED=1 all
%%micropython
import keywordfunction
print(keywordfunction.add_ints(-3, b=4))
print(keywordfunction.add_ints(3))
a = -3, b = 4
1
a = 3, b = 0
3
As advertised, both function calls do what they were supposed to do: in
the first case, b
assumes the value of 4, while in the second case,
it takes on 0, even though we didn’t supply anything to the function.
Arbitrary keyword arguments¶
We have seen how integer values can be extracted from keyword arguments, but unfortunately, that method is going to get you only that far. What if we want to pass something more complicated, in particular a string, or a tuple, or some other non-trivial python type?
A simple solution could be to implement the C function without keywords at all, and do the parsing in python. After all, it is highly unlikely that parsing would be expensive in comparison to the body of the function. But perhaps, you have your reasons for not going down that rabbit hole.
For such cases, we can still resort to objects of type .u_rom_obj
.
In order to experiment with the possibilities, in the next module, we
define a function that simply returns the values passed to it. The input
arguments are going to be a single positional argument, and four keyword
arguments with type int
, string
, tuple
, and float
.
#include <stdio.h>
#include "py/obj.h"
#include "py/objlist.h"
#include "py/runtime.h"
#include "py/builtin.h"
// This is lifted from objfloat.c, because mp_obj_float_t is not exposed there (there is no header file)
typedef struct _mp_obj_float_t {
mp_obj_base_t base;
mp_float_t value;
} mp_obj_float_t;
const mp_obj_float_t my_float = {{&mp_type_float}, 0.987};
const mp_rom_obj_tuple_t my_tuple = {
{&mp_type_tuple},
3,
{
MP_ROM_INT(0),
MP_ROM_QSTR(MP_QSTR_float),
MP_ROM_PTR(&my_float),
},
};
STATIC mp_obj_t arbitrarykeyword_print(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_a, MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_b, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 1} },
{ MP_QSTR_c, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_QSTR(MP_QSTR_float)} },
{ MP_QSTR_d, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_PTR(&my_float)} },
{ MP_QSTR_e, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_PTR(&my_tuple)} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(1, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
mp_obj_t tuple[5];
tuple[0] = mp_obj_new_int(args[0].u_int); // a
tuple[1] = mp_obj_new_int(args[1].u_int); // b
tuple[2] = args[2].u_obj; // c
tuple[3] = args[3].u_obj; // d
tuple[4] = args[4].u_obj; // e
return mp_obj_new_tuple(5, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(arbitrarykeyword_print_obj, 1, arbitrarykeyword_print);
STATIC const mp_rom_map_elem_t arbitrarykeyword_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_arbitrarykeyword) },
{ MP_ROM_QSTR(MP_QSTR_print), (mp_obj_t)&arbitrarykeyword_print_obj },
};
STATIC MP_DEFINE_CONST_DICT(arbitrarykeyword_module_globals, arbitrarykeyword_module_globals_table);
const mp_obj_module_t arbitrarykeyword_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&arbitrarykeyword_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_arbitrarykeyword, arbitrarykeyword_user_cmodule, MODULE_ARBITRARYKEYWORD_ENABLED);
Before compiling the code, let us think a bit about what is going on
here. The first argument, a
, is straightforward: that is a
positional argument, and we deal with that as we did in the last
example. The same applies to the second argument, b
, which is our
first keyword argument with an integer default value.
Matters become more interesting with the third argument, c
: that is
supposed to be a string, whose default value is “float”. We generate the
respective C representation by prepending the MP_QSTR_
. At this
point, we have a string, but we still can’t assign it as a default
value. We do that by first applying the MP_ROM_QSTR
macro, and
assigning the results to the .u_rom_obj
member of the mp_arg_t
structure. You most certainly will want to inspect the value at one
point. We have already discussed the drill in Working with
strings.
The fourth argument, d
, is meant to be a float. Since there is no
equivalent of a float in the mp_arg_t
structure, we have to turn our
number into an MP_ROM_PTR
, so we have to retrieve the address of the
float object. To this end, we define the number in the line
const mp_obj_float_t my_float = {{&mp_type_float}, 0.987};
Note that since mp_obj_float_t
is not exposed in objfloat.c
,
where it is defined, we had to copy the type declaration. This is
certainly not very elegant, but desperate times call for desperate
measures. In addition, we also have to declare my_float
as a
constant. The reason for this is that we have to assure the compiler
that this value is not going to change in the future, so that it can be
saved into the read-only memory.
The last argument, e
, is a tuple, which has a special type for such
cases, namely, the mp_rom_obj_tuple_t
, so we define my_tuple
as
an mp_rom_obj_tuple_t
object, with a base type of mp_type_tuple
,
and three elements, an integer, a string, and a float. The elements go
into the tuple as if they were assigned to the .u_rom_obj
members
directly, hence the macros MP_ROM_INT
, MP_ROM_QSTR
, and
MP_ROM_PTR
.
When we return the default values at the end of our function, we declare
an array of type mb_obj_t
, and of length 5, assign the elements, and
turn the array into a tuple with mp_obj_new_tuple
.
One final comment to this section: I referred to our function as
returning the values of the arguments, yet, I called it print
. Had I
called the function return
, it wouldn’t have worked for the simple
reason, that return
is a keyword of the language itself. As a
friendly advice, do not try to override that!
Having thoroughly discussed the code, we should compile it, and see what happens.
https://github.com/v923z/micropython-usermod/tree/master/snippets/arbitrarykeyword/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/arbitrarykeyword.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_ARBITRARYKEYWORD_ENABLED=1 all
%%micropython
import arbitrarykeyword
print(arbitrarykeyword.print(1, b=123))
print(arbitrarykeyword.print(-35, b=555, c='foo', d='bar', e=[1, 2, 3]))
(1, 123, 'float', 0.9869999999999999, (0, 'float', 0.9869999999999999))
(-35, 555, 'foo', 'bar', [1, 2, 3])
We should note that the particular definition of the float constant will
work in the A
, and B
object representations only. If your
micropython
platform uses either the C
, or the D
representation, your code will still compile, but you’ll be surprised by
the results.
Working with classes¶
Of course, python would not be python without classes. A module can also include the implementation of classes. The procedure is similar to what we have already seen in the context of standard functions, except that we have to define a structure that holds at least a string with the name of the class, a pointer to the initialisation and printout functions, and a local dictionary. A typical class structure would look like
STATIC const mp_rom_map_elem_t simpleclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_method1), MP_ROM_PTR(&simpleclass_method1_obj) },
{ MP_ROM_QSTR(MP_QSTR_method2), MP_ROM_PTR(&simpleclass_method2_obj) },
...
}
const mp_obj_type_t simpleclass_type = {
{ &mp_type_type },
.name = MP_QSTR_simpleclass,
.print = simpleclass_print,
.make_new = simpleclass_make_new,
.locals_dict = (mp_obj_dict_t*)&simpleclass_locals_dict,
};
The locals dictionary, .locals_dict
, contains all user-facing
methods and constants of the class, while the simpleclass_type
structure’s name
member is what our class is going to be called.
.print
is roughly the equivalent of __str__
, and .make_new
is the C name for __init__
.
In order to see how this all works, we are going to implement a very simple class, which holds two integer variables, and has a method that returns the sum of these two variables. In python, a possible realisation could look like this:
class myclass:
def __init__(self, a, b):
self.a = a
self.b = b
def mysum(self):
return self.a + self.b
A = myclass(1, 2)
A.mysum()
3
In addition to the class implementation above and in order to show how class methods and regular functions can live in the same module, we will also have a function, which is not bound to the class itself, and which adds the two components in the class, i.e., that is similar to
def add(class_instance):
return class_instance.a + class_instance.b
add(A)
3
(Note that retrieving values from the class in this way is not exactly elegant, nor is it pythonic. We would usually implement a getter method for that.)
https://github.com/v923z/micropython-usermod/tree/master/snippets/simpleclass/simpleclass.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
typedef struct _simpleclass_myclass_obj_t {
mp_obj_base_t base;
int16_t a;
int16_t b;
} simpleclass_myclass_obj_t;
const mp_obj_type_t simpleclass_myclass_type;
STATIC void myclass_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
simpleclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "myclass(");
mp_obj_print_helper(print, mp_obj_new_int(self->a), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_int(self->b), PRINT_REPR);
mp_print_str(print, ")");
}
STATIC mp_obj_t myclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 2, 2, true);
simpleclass_myclass_obj_t *self = m_new_obj(simpleclass_myclass_obj_t);
self->base.type = &simpleclass_myclass_type;
self->a = mp_obj_get_int(args[0]);
self->b = mp_obj_get_int(args[1]);
return MP_OBJ_FROM_PTR(self);
}
// Class methods
STATIC mp_obj_t myclass_sum(mp_obj_t self_in) {
simpleclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_int(self->a + self->b);
}
MP_DEFINE_CONST_FUN_OBJ_1(myclass_sum_obj, myclass_sum);
STATIC const mp_rom_map_elem_t myclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_mysum), MP_ROM_PTR(&myclass_sum_obj) },
};
STATIC MP_DEFINE_CONST_DICT(myclass_locals_dict, myclass_locals_dict_table);
const mp_obj_type_t simpleclass_myclass_type = {
{ &mp_type_type },
.name = MP_QSTR_simpleclass,
.print = myclass_print,
.make_new = myclass_make_new,
.locals_dict = (mp_obj_dict_t*)&myclass_locals_dict,
};
// Module functions
STATIC mp_obj_t simpleclass_add(const mp_obj_t o_in) {
simpleclass_myclass_obj_t *class_instance = MP_OBJ_TO_PTR(o_in);
return mp_obj_new_int(class_instance->a + class_instance->b);
}
MP_DEFINE_CONST_FUN_OBJ_1(simpleclass_add_obj, simpleclass_add);
STATIC const mp_map_elem_t simpleclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_simpleclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_myclass), (mp_obj_t)&simpleclass_myclass_type },
{ MP_OBJ_NEW_QSTR(MP_QSTR_add), (mp_obj_t)&simpleclass_add_obj },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_simpleclass_globals,
simpleclass_globals_table
);
const mp_obj_module_t simpleclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_simpleclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_simpleclass, simpleclass_user_cmodule, MODULE_SIMPLECLASS_ENABLED);
One more thing to note: the functions that are pointed to in
simpleclass_myclass_type
are not registered with the macro
MP_DEFINE_CONST_FUN_OBJ_VAR
or similar. The reason for this is that
this happens automatically: myclass_print
does not require
user-supplied arguments beyond self
, so it is known what the
signature should look like. In myclass_make_new
, we inspect the
argument list, when calling
mp_arg_check_num(n_args, n_kw, 2, 2, true);
so, again, there is no need to turn our function into a function object.
Printing class properties¶
In my_print
, instead of the standard the C function printf
, we
made use of mp_print_str
, and mp_obj_print_helper
, which are
options in this case. Both take print
as their first argument. The
value of print
is supplied by the .print
method of the class
itself. The second argument is a string (in the case of
mp_print_str
), or a micropython
object (for
mp_obj_print_helper
). In addition, mp_obj_print_helper
takes a
pre-defined constant, PRINT_REPR
as its third argument. By resorting
to these micropython
printing functions, we can make certain that
the output is formatted nicely, independent of the platform. Whenever
print
is available, these function should be used instead of
printf
. For debugging purposes, printf
is also fine. More on the
subject can be found in mpprint.c
.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simpleclass/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/simpleclass.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SIMPLECLASS_ENABLED=1 all
%%micropython
import simpleclass
a = simpleclass.myclass(2, 3)
print(a)
print(a.mysum())
myclass(2, 3)
5
Special methods of classes¶
Python has a number of special methods, which will make a class behave
as a native object. So, e.g., if a class implements the
__add__(self, other)
method, then instances of that class can be
added with the +
operator. Here is an example in python:
class Adder:
def __init__(self, value):
self.value = value
def __add__(self, other):
self.value = self.value + other.value
return self
a = Adder(1)
b = Adder(2)
c = a + b
c.value
3
Note that, while the above example is not particularly useful, it proves
the point: upon calling the +
operator, the values of a
, and
b
are added. If we had left out the implementation of the
__add__
method, the python interpreter would not have a clue as to
what to do with the objects. You can see for yourself, how sloppiness
makes itself manifest:
class Adder:
def __init__(self, value):
self.value = value
a = Adder(1)
b = Adder(2)
c = a + b
c.value
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-77-635006a6f7bc> in <module>
7 b = Adder(2)
8
----> 9 c = a + b
10 c.value
TypeError: unsupported operand type(s) for +: 'Adder' and 'Adder'
Indeed, we do not support the +
operator.
Now, the problem is that in the C implementation, these special methods have to be treated in a special way. The naive approach would be to add the pointer to the function to the locals dictionary as
STATIC const mp_rom_map_elem_t simpleclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___add__), MP_ROM_PTR(&simpleclass_add_obj) },
};
but that would not work. Well, this is not entirely true: the +
operator would not work, but one could still call the method explicitly
as
a = Adder(1)
b = Adder(2)
a.__add__(b)
Before we actually add the +
operator to our class, we should note
that there are two kinds of special methods, namely the unary and the
binary operators.
In the first group are those, whose sole argument is the class instance
itself. Two frequently used cases are the length operator, len
, and
bool
. So, e.g., if your class implements the __len__(self)
method, and the method returns an integer, then you can call the len
function in the console
len(myclass)
In the second category of operators are those, which require a left, as
well as a right hand side: the operand on the left hand side is the
class instance itself, while the right hand side can, in principle, be
another instance of the same class, or some other type. An example for
this was the __add__
method in our Adder
class. To prove that
the right hand side needn’t be of the same type, think of the
multiplication of lists:
[1, 2, 3]*5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
is perfectly valid, and has a well-defined meaning. It is the
responsibility of the C implementation to inspect the right hand side,
and decide how to interpret the operation. The complete list of unary,
as well as binary operators can be found in runtime.h
.
The module below implements five special methods altogether. Two unary,
namely, bool
, and len
, and three binary operators, ==
,
+
, and *
. Since the addition and multiplication will return a
new instance of specialclass_myclass
, we define a new function,
create_new_class
, that, well, creates a new instance of
specialclass_myclass
, and initialises the members with the two input
arguments. This function will also be called in the class initialisation
function, myclass_make_new
, immediately after the argument checking.
When implementing the operators, we have to keep a couple of things in
mind. First, the specialclass_myclass_type
has to be extended with
the two methods, .unary_op
, and .binary_op
, where .unary_op
is equal to the function that handles the unary operation
(specialclass_unary_op
in the example below), and .binary_op
is
equal to the function that deals with binary operations
(specialclass_binary_op
below). These two functions have the
signatures
STATIC mp_obj_t specialclass_unary_op(mp_unary_op_t op, mp_obj_t self_in)
and
STATIC mp_obj_t specialclass_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs)
respectively, and we have to inspect the value of op
in the
implementation. This is done in the two switch
statements.
Second, if .unary_op
, or .binary_op
are defined for the class,
then the handler function must have an implementation of all possible
operators. This doesn’t necessarily mean that you have to have all cases
in the switch
, but if you haven’t, then there must be a default
case with a reasonable return value, e.g., MP_OBJ_NULL
, or
mp_const_none
, so as to indicate that that particular method is not
available.
https://github.com/v923z/micropython-usermod/tree/master/snippets/specialclass/specialclass.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
#include "py/binary.h"
typedef struct _specialclass_myclass_obj_t {
mp_obj_base_t base;
int16_t a;
int16_t b;
} specialclass_myclass_obj_t;
const mp_obj_type_t specialclass_myclass_type;
STATIC void myclass_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
specialclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "myclass(");
mp_obj_print_helper(print, mp_obj_new_int(self->a), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_int(self->b), PRINT_REPR);
mp_print_str(print, ")");
}
mp_obj_t create_new_myclass(uint16_t a, uint16_t b) {
specialclass_myclass_obj_t *out = m_new_obj(specialclass_myclass_obj_t);
out->base.type = &specialclass_myclass_type;
out->a = a;
out->b = b;
return MP_OBJ_FROM_PTR(out);
}
STATIC mp_obj_t myclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 2, 2, true);
return create_new_myclass(mp_obj_get_int(args[0]), mp_obj_get_int(args[1]));
}
STATIC const mp_rom_map_elem_t myclass_locals_dict_table[] = {
};
STATIC MP_DEFINE_CONST_DICT(myclass_locals_dict, myclass_locals_dict_table);
STATIC mp_obj_t specialclass_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
specialclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
switch (op) {
case MP_UNARY_OP_BOOL: return mp_obj_new_bool((self->a > 0) && (self->b > 0));
case MP_UNARY_OP_LEN: return mp_obj_new_int(2);
default: return MP_OBJ_NULL; // operator not supported
}
}
STATIC mp_obj_t specialclass_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
specialclass_myclass_obj_t *left_hand_side = MP_OBJ_TO_PTR(lhs);
specialclass_myclass_obj_t *right_hand_side = MP_OBJ_TO_PTR(rhs);
switch (op) {
case MP_BINARY_OP_EQUAL:
return mp_obj_new_bool((left_hand_side->a == right_hand_side->a) && (left_hand_side->b == right_hand_side->b));
case MP_BINARY_OP_ADD:
return create_new_myclass(left_hand_side->a + right_hand_side->a, left_hand_side->b + right_hand_side->b);
case MP_BINARY_OP_MULTIPLY:
return create_new_myclass(left_hand_side->a * right_hand_side->a, left_hand_side->b * right_hand_side->b);
default:
return MP_OBJ_NULL; // operator not supported
}
}
const mp_obj_type_t specialclass_myclass_type = {
{ &mp_type_type },
.name = MP_QSTR_specialclass,
.print = myclass_print,
.make_new = myclass_make_new,
.unary_op = specialclass_unary_op,
.binary_op = specialclass_binary_op,
.locals_dict = (mp_obj_dict_t*)&myclass_locals_dict,
};
STATIC const mp_map_elem_t specialclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_specialclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_myclass), (mp_obj_t)&specialclass_myclass_type },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_specialclass_globals,
specialclass_globals_table
);
const mp_obj_module_t specialclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_specialclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_specialclass, specialclass_user_cmodule, MODULE_SPECIALCLASS_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/specialclass/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/specialclass.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SPECIALCLASS_ENABLED=1 all
%%micropython
import specialclass
a = specialclass.myclass(1, 2)
b = specialclass.myclass(10, 20)
print(a)
print(b)
print(a + b)
myclass(1, 2)
myclass(10, 20)
myclass(11, 22)
Properties¶
In addition to methods, in python, classes can also have properties, which will basically return some read-only attributes of the class. Take the following example:
class PropClass:
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
We can now create an instance of PropClass
, and access the value of
_x
by “calling” the decorated x
method without the brackets
characteristic of function calls:
c = PropClass(12.3)
c.x
12.3
One use case is, when you want to protect the value of _x
, and want
to prevent accidental changes: if you want to write to the x
property, you’ll get a nicely-formatted exception:
c.x = 55.5
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-50-63b5601caccb> in <module>
----> 1 c.x = 55.5
AttributeError: can't set attribute
It is nifty, isn’t it? Now, let us see, how we can deal with this in micropython. In order to simplify things, we will implement what we have just seen above: a class that holds a single floating point value, and does nothing else.
Most of the code should be familiar from our first example on classes, so I will discuss the single new function that is relevant to properties. At the C level, a property is nothing but a void function with exactly three arguments
STATIC void some_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
...
}
where self_in
is the class instance, attribute
is a string with
the property’s name, and destination
is a pointer to the return
value of the function that is going to be called, when querying for the
property. So, in the python example above, attribute
is x
,
because we queried the x
property, and destination
will be the
equivalent of self._x
, because self._x
is what is returned by
the PropClass.x()
method.
In the C function, we do not return anything, instead, we assign the
desired property (attribute) of the class to destination[0]
as in
the snippet below:
STATIC void propertyclass_attr(mp_obj_t self, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self);
}
}
The qstr
is required, because a class might have multiple
properties, but all these properties are retrieved by a single function,
propertyclass_attr
. Thus, should we want to return another property
with name y
, we would augment the function as
STATIC void propertyclass_attr(mp_obj_t self, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self);
} else if(attribute == MP_QSTR_y) {
destination[0] = propertyclass_y(self);
}
}
Now, we are almost done, but we still have to implement the function that actually retrieves the attribute. This is what happens here:
STATIC mp_obj_t propertyclass_x(mp_obj_t self_in) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_float(self->x);
}
Remember, destination
was a pointer to mp_obj_t
, so whatever
function we have, it must return mp_obj_t
. In this particular case,
the implementation is trivial: we fetch the value of self->x
, and
turn it into an mp_obj_new_float
.
We are now done, right? Not quite: while the required functions are
implemented, they will never be called. We have to attach them to the
class, so that the interpreter knows what is to do, when we try to
access c.x
. This act of attaching the function happens in the type
definition of the class: we equate the .attr
member of the structure
with our propertyclass_attr
functions, so that the interpreter can
fill in the three arguments.
And with that, we are ready to compile the code.
https://github.com/v923z/micropython-usermod/tree/master/snippets/properties/properties.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
typedef struct _propertyclass_obj_t {
mp_obj_base_t base;
mp_float_t x;
} propertyclass_obj_t;
const mp_obj_type_t propertyclass_type;
STATIC mp_obj_t propertyclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
propertyclass_obj_t *self = m_new_obj(propertyclass_obj_t);
self->base.type = &propertyclass_type;
self->x = mp_obj_get_float(args[0]);
return MP_OBJ_FROM_PTR(self);
}
STATIC mp_obj_t propertyclass_x(mp_obj_t self_in) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_float(self->x);
}
MP_DEFINE_CONST_FUN_OBJ_1(propertyclass_x_obj, propertyclass_x);
STATIC const mp_rom_map_elem_t propertyclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_x), MP_ROM_PTR(&propertyclass_x_obj) },
};
STATIC MP_DEFINE_CONST_DICT(propertyclass_locals_dict, propertyclass_locals_dict_table);
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self_in);
}
}
const mp_obj_type_t propertyclass_type = {
{ &mp_type_type },
.name = MP_QSTR_propertyclass,
.make_new = propertyclass_make_new,
.attr = propertyclass_attr,
.locals_dict = (mp_obj_dict_t*)&propertyclass_locals_dict,
};
STATIC const mp_map_elem_t propertyclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_propertyclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_propertyclass), (mp_obj_t)&propertyclass_type },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_propertyclass_globals,
propertyclass_globals_table
);
const mp_obj_module_t propertyclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_propertyclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_propertyclass, propertyclass_user_cmodule, MODULE_PROPERTYCLASS_ENABLED);
Before we compile the module, I would like to add two comments to what was said above.
First, in the function that we assigned to .attr
,
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self_in);
}
}
we called a function on self_in
, propertyclass_x()
, and assigned
the results to destination[0]
. However, this extra trip is not
absolutely necessary: we could have equally done something along these
lines:
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
destination[0] = mp_obj_new_float(self->x);
}
}
The case in point being that destination[0]
is simply an
mp_obj_t
object, it does not matter, where and how it is produced.
Since self
is available to propertyclass_attr
, if the property
is simple, as above, one can save the function call, and do everything
in place.
Second, more examples on implementing properties can be found in
py/profile.c.
Just look for the .attr
string, and the associated functions!
https://github.com/v923z/micropython-usermod/tree/master/snippets/properties/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/properties.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_PROPERTYCLASS_ENABLED=1 all
%%micropython
import propertyclass
a = propertyclass.propertyclass(12.3)
print(a.x)
12.3
Creating new types¶
Sometimes you might need something beyond the standard python data types, and you have to define your own. At first, the task seems daunting, but types are really nothing but a C structure with a couple of special fields. The steps required are very similar to those for classes. Take the following type definition, which could be regarded as the Cartesian components of a vector in three-dimensional space:
typedef struct _vector_obj_t {
mp_obj_base_t base;
float x, y, z;
} vector_obj_t;
Now, in order to see, how we can work with this structure, we are going
to define a new type that simply stores the three values. The module
will also have a method called length
, returning the absolute value
of the vector. Also note that here we check the type of the argument,
and bail out, if it is not a vector. The beauty of all this is that once
the type is defined, the available micropython methods just work. Can
you still recall the
mp_obj_is_type(myobject, &my_type)
macro in Section Type checking? I thought so.
We have our vector structure at the C level. It has four members: an
mp_obj_base_t
, and three floats called x
, y
, and z
. But
this is still not usable in the python interpreter. We have to somehow
tell the interpreter, what it is supposed to do with this new type, and
how a variable of this type is to be presented to the user. This is,
where the structure
const mp_obj_type_t vector_type = {
{ &mp_type_type },
.name = MP_QSTR_vector,
.print = vector_print,
.make_new = vector_make_new,
};
takes centre stage. Does this look familiar? This structure contains the
new type’s name (a string, vector
), how it presents itself to users
(a function, vector_print
), and how a new instance is to be created
(a function, vector_make_new
). These latter two we have to implement
ourselves.
In vector_print
we have three arguments, namely
const mp_print_t *print
, which is a helper that we don’t call,
mp_obj_t self_in
which is a reference to the vector itself, and
mp_print_kind_t kind
, which we can graciously ignore, because we are
not going to use it anyway.
Having seen the bits and pieces, we should build some new firmware.
https://github.com/v923z/micropython-usermod/tree/master/snippets/vector/vector.c
#include <math.h>
#include <stdio.h>
#include "py/obj.h"
#include "py/runtime.h"
const mp_obj_type_t vector_type;
typedef struct _vector_obj_t {
mp_obj_base_t base;
float x, y, z;
} vector_obj_t;
STATIC mp_obj_t vector_length(mp_obj_t o_in) {
if(!mp_obj_is_type(o_in, &vector_type)) {
mp_raise_TypeError("argument is not a vector");
}
vector_obj_t *vector = MP_OBJ_TO_PTR(o_in);
return mp_obj_new_float(sqrtf(vector->x*vector->x + vector->y*vector->y + vector->z*vector->z));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(vector_length_obj, vector_length);
STATIC void vector_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
vector_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "vector(");
mp_obj_print_helper(print, mp_obj_new_float(self->x), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_float(self->y), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_float(self->z), PRINT_REPR);
mp_print_str(print, ")");
}
STATIC mp_obj_t vector_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 3, 3, true);
vector_obj_t *vector = m_new_obj(vector_obj_t);
vector->base.type = &vector_type;
vector->x = mp_obj_get_float(args[0]);
vector->y = mp_obj_get_float(args[1]);
vector->z = mp_obj_get_float(args[2]);
return MP_OBJ_FROM_PTR(vector);
}
const mp_obj_type_t vector_type = {
{ &mp_type_type },
.name = MP_QSTR_vector,
.print = vector_print,
.make_new = vector_make_new,
};
STATIC const mp_rom_map_elem_t vector_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_vector) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_vector), (mp_obj_t)&vector_type },
{ MP_ROM_QSTR(MP_QSTR_length), MP_ROM_PTR(&vector_length_obj) },
};
STATIC MP_DEFINE_CONST_DICT(vector_module_globals, vector_module_globals_table);
const mp_obj_module_t vector_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&vector_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_vector, vector_user_cmodule, MODULE_VECTOR_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/vector/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/vector.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_VECTOR_ENABLED=1 all
%%micropython
import vector
a = vector.vector(1, 20, 30)
print(a)
print(vector.length(a))
vector(1.0, 20.0, 30.0)
36.06937789916993
Just to convince ourselves, when calculated in python proper, the length of the vector is
import math
print(math.sqrt(1**2 + 20**2 + 30**2))
36.069377593742864
Close enough.
Dealing with iterables¶
Without going too deeply into specifics, in python, an iterable is
basically an object that you can have in a for
loop:
for item in my_iterable:
print(item)
Amongst others, lists, tuples, and ranges are iterables, as are strings.
The key is that these objects have a special internal method, an
iterator, attached to them. This iterator is responsible for keeping
track of the index during the iteration, and serving the objects in the
iterable one by one to the for
loop. When writing our own iterable,
we will look under the hood, and see how this all works at the C level.
For now, we are going to discuss only, how we can consume the content
of an iterable in the C code.
Iterating over built-in types¶
In order to demonstrate the use of an iterator, we are going to write a function that sums the square of the values in an iterable. The python version of the function could be something like this:
def sumsq(some_iterable):
return sum([item**2 for item in some_iterable])
sumsq([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
385
In C, the key is in the snippet
mp_obj_iter_buf_t iter_buf;
mp_obj_t item, iterable = mp_getiter(o_in, &iter_buf);
while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
// do something with the item just retrieved
}
This is more or less the equivalent of the for item in some_iterable
instruction. In C, the mp_obj_t
object o_in
is the argument of
our python function, which is turned into an iterable by passing it into
the mp_getiter
function. This function also needs a buffer object
that is of type mp_obj_iter_buf_t
. The buffer type is defined in
obj.h
as
typedef struct _mp_obj_iter_buf_t {
mp_obj_base_t base;
mp_obj_t buf[3];
} mp_obj_iter_buf_t;
where .buf[2]
holds the index value, and this is how mp_iternext
keeps track of the position in the loop.
Once item
is retrieved, the rest of the code is trivial: you do
whatever you want to do with the value, and return at the very end.
Now, what happens, if you pass a non-iterable object to the function?
For a while, nothing. Everything will work till the point
item = mp_iternext(iterable)
, where the interpreter will raise a
TypeError
exception. So, on the python console, you can either
enclose your function in a
try:
sumsq(some_iterable)
except TypeError:
print('something went terribly wrong`)
construct, or you can inspect the type of the variable at the C level.
Unfortunately, there does not seem to be a type identifier for iterables
in general, so you have to check, whether the argument is a list, tuple,
range, etc. This can be done by calling the mp_obj_is_type
macro,
and see which Boolean it returns, if you pass &mp_type_tuple
,
&mp_type_list
, &mp_type_range
etc. to it, as we discussed in the
section Object representation.
The complete code listing of consumeiterable.c
follows below. If you
ask me, this is a lot of code just to replace a python one-liner.
https://github.com/v923z/micropython-usermod/tree/master/snippets/consumeiterable/consumeiterable.c
#include "py/obj.h"
#include "py/runtime.h"
STATIC mp_obj_t consumeiterable_sumsq(mp_obj_t o_in) {
mp_float_t _sum = 0.0, itemf;
mp_obj_iter_buf_t iter_buf;
mp_obj_t item, iterable = mp_getiter(o_in, &iter_buf);
while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
itemf = mp_obj_get_float(item);
_sum += itemf*itemf;
}
return mp_obj_new_float(_sum);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(consumeiterable_sumsq_obj, consumeiterable_sumsq);
STATIC const mp_rom_map_elem_t consumeiterable_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_consumeiterable) },
{ MP_ROM_QSTR(MP_QSTR_sumsq), MP_ROM_PTR(&consumeiterable_sumsq_obj) },
};
STATIC MP_DEFINE_CONST_DICT(consumeiterable_module_globals, consumeiterable_module_globals_table);
const mp_obj_module_t consumeiterable_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&consumeiterable_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_consumeiterable, consumeiterable_user_cmodule, MODULE_CONSUMEITERABLE_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/consumeiterable/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/consumeiterable.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_CONSUMEITERABLE_ENABLED=1 all
%%micropython
import consumeiterable
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(a)
print(consumeiterable.sumsq(a))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
385.0
Returning iterables¶
Let us suppose that the result of some operation is an iterable, e.g., a tuple, or a list. How would we return such an object? How about a function that returns the powers of its argument? In python
def powerit(base, exponent):
return [base**e for e in range(0, exponent+1)]
powerit(2, 10)
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
and in C,
https://github.com/v923z/micropython-usermod/tree/master/snippets/returniterable/returniterable.c
#include "py/obj.h"
#include "py/runtime.h"
STATIC mp_obj_t powers_iterable(mp_obj_t base, mp_obj_t exponent) {
int e = mp_obj_get_int(exponent);
mp_obj_t tuple[e+1];
int b = mp_obj_get_int(base), ba = 1;
for(int i=0; i <= e; i++) {
tuple[i] = mp_obj_new_int(ba);
ba *= b;
}
return mp_obj_new_tuple(e+1, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(powers_iterable_obj, powers_iterable);
STATIC const mp_rom_map_elem_t returniterable_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_returniterable) },
{ MP_ROM_QSTR(MP_QSTR_powers), MP_ROM_PTR(&powers_iterable_obj) },
};
STATIC MP_DEFINE_CONST_DICT(returniterable_module_globals, returniterable_module_globals_table);
const mp_obj_module_t returniterable_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&returniterable_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_returniterable, returniterable_user_cmodule, MODULE_RETURNITERABLE_ENABLED);
As everything else, the elements of tuples and lists are objects of type
mp_obj_t
, so, after finding out how far we have got to go with the
exponents, we declare an array of the required length. Values are
generated and assigned in the for
loop. Since on the left hand side
of the assignment we have an mp_obj_t
, we convert the results with
mp_obj_new_int
. Once we are done with the computations, we return
the array with mp_obj_new_tuple
. This functions takes the array as
the second argument, while the first argument specifies the length.
If you happen to want to return a list instead of a tuple, all you have
to do is use mp_obj_new_list
instead at the very end.
https://github.com/v923z/micropython-usermod/tree/master/snippets/returniterable/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/returniterable.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_RETURNITERABLE_ENABLED=1 all
%%micropython
import returniterable
print(returniterable.powers(3, 10))
(1, 3, 9, 27, 81, 243, 729, 2187, 6561, 19683, 59049)
Creating iterables¶
Having seen how we can consume the elements in an iterable, it is time
to explore what this .getiter
magic is doing. So, let us create a
new type, itarray
, and make it iterable! This new type will have a
constructor method,square
, generating 16-bit integers, where the
values are simply the squares of the indices, i.e., 1, 4, 9, 16… We are
interested only in the iterability of the object, and for this reason,
we will implement only the .getiter
special method, and skip
.binary_op
, and .unary_op
. If needed, these can easily be added
based on the discussion in Special methods of classes.
Before listing the complete code, we discuss the relevant code snippets.
The first chunk is the assignment of .getiter
in the
iterable_array_type
structure. .getiter
will be made equal to a
function called iterarray_getiter
, which simply returns
mp_obj_new_itarray_iterator
. Why can’t we simply assign
mp_obj_new_itarray_iterator
, instead of wrapping it in
iterarray_getiter
? The reason for that is that iterarray_getiter
has a strict signature, and we want to pass an extra argument, 0. This
is nothing but the very first index in the sequence.
STATIC mp_obj_t itarray_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
return mp_obj_new_itarray_iterator(o_in, 0, iter_buf);
}
const mp_obj_type_t iterable_array_type = {
{ &mp_type_type },
.name = MP_QSTR_itarray,
.print = itarray_print,
.make_new = itarray_make_new,
.getiter = itarray_getiter,
};
So, it appears that we have to scrutinise
mp_obj_new_itarray_iterator
. This is a special object type in
micropython, with a base type of mp_type_polymorph_iter
. In
addition, it holds a pointer to the __next__
method, which is
itarray_iternext
in this case, stores a pointer to the variable (the
one that we are iterating over), and the current index (which we
initialised to 0 in mp_obj_new_itarray_iterator
).
mp_obj_t mp_obj_new_itarray_iterator(mp_obj_t itarray, size_t cur, mp_obj_iter_buf_t *iter_buf) {
assert(sizeof(mp_obj_itarray_it_t) <= sizeof(mp_obj_iter_buf_t));
mp_obj_itarray_it_t *o = (mp_obj_itarray_it_t*)iter_buf;
o->base.type = &mp_type_polymorph_iter;
o->iternext = itarray_iternext;
o->itarray = itarray;
o->cur = cur;
return MP_OBJ_FROM_PTR(o);
}
mp_obj_new_itarray_iterator
is not much more than a declaration and
assignments. The object that we return is of type
mp_obj_itarray_it_t
, which has the above-mentioned structure
// itarray iterator
typedef struct _mp_obj_itarray_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
mp_obj_t itarray;
size_t cur;
} mp_obj_itarray_it_t;
mp_obj_t itarray_iternext(mp_obj_t self_in) {
mp_obj_itarray_it_t *self = MP_OBJ_TO_PTR(self_in);
itarray_obj_t *itarray = MP_OBJ_TO_PTR(self->itarray);
if (self->cur < itarray->len) {
// read the current value
uint16_t *arr = itarray->elements;
mp_obj_t o_out = MP_OBJ_NEW_SMALL_INT(arr[self->cur]);
self->cur += 1;
return o_out;
} else {
return MP_OBJ_STOP_ITERATION;
}
}
Now, the complete code in one chunk:
https://github.com/v923z/micropython-usermod/tree/master/snippets/makeiterable/makeiterable.c
#include <stdlib.h>
#include "py/obj.h"
#include "py/runtime.h"
typedef struct _itarray_obj_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
uint16_t *elements;
size_t len;
} itarray_obj_t;
const mp_obj_type_t iterable_array_type;
mp_obj_t mp_obj_new_itarray_iterator(mp_obj_t , size_t , mp_obj_iter_buf_t *);
STATIC void itarray_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
itarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "itarray: ");
uint16_t i;
for(i=0; i < self->len-1; i++) {
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
mp_print_str(print, ", ");
}
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
}
STATIC mp_obj_t itarray_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
itarray_obj_t *self = m_new_obj(itarray_obj_t);
self->base.type = &iterable_array_type;
self->len = mp_obj_get_int(args[0]);
uint16_t *arr = malloc(self->len * sizeof(uint16_t));
for(uint16_t i=0; i < self->len; i++) {
arr[i] = i*i;
}
self->elements = arr;
return MP_OBJ_FROM_PTR(self);
}
STATIC mp_obj_t itarray_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
return mp_obj_new_itarray_iterator(o_in, 0, iter_buf);
}
const mp_obj_type_t iterable_array_type = {
{ &mp_type_type },
.name = MP_QSTR_itarray,
.print = itarray_print,
.make_new = itarray_make_new,
.getiter = itarray_getiter,
};
STATIC const mp_rom_map_elem_t makeiterable_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_makeiterable) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_square), (mp_obj_t)&iterable_array_type },
};
STATIC MP_DEFINE_CONST_DICT(makeiterable_module_globals, makeiterable_module_globals_table);
const mp_obj_module_t makeiterable_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&makeiterable_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_makeiterable, makeiterable_user_cmodule, MODULE_MAKEITERABLE_ENABLED);
// itarray iterator
typedef struct _mp_obj_itarray_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
mp_obj_t itarray;
size_t cur;
} mp_obj_itarray_it_t;
mp_obj_t itarray_iternext(mp_obj_t self_in) {
mp_obj_itarray_it_t *self = MP_OBJ_TO_PTR(self_in);
itarray_obj_t *itarray = MP_OBJ_TO_PTR(self->itarray);
if (self->cur < itarray->len) {
// read the current value
uint16_t *arr = itarray->elements;
mp_obj_t o_out = MP_OBJ_NEW_SMALL_INT(arr[self->cur]);
self->cur += 1;
return o_out;
} else {
return MP_OBJ_STOP_ITERATION;
}
}
mp_obj_t mp_obj_new_itarray_iterator(mp_obj_t itarray, size_t cur, mp_obj_iter_buf_t *iter_buf) {
assert(sizeof(mp_obj_itarray_it_t) <= sizeof(mp_obj_iter_buf_t));
mp_obj_itarray_it_t *o = (mp_obj_itarray_it_t*)iter_buf;
o->base.type = &mp_type_polymorph_iter;
o->iternext = itarray_iternext;
o->itarray = itarray;
o->cur = cur;
return MP_OBJ_FROM_PTR(o);
}
https://github.com/v923z/micropython-usermod/tree/master/snippets/makeiterable/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/makeiterable.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_MAKEITERABLE_ENABLED=1 all
%%micropython
import makeiterable
a = makeiterable.square(15)
print(a)
for j, i in enumerate(a):
if j == 1: print('%dst element: %d'%(j, i))
elif j == 2: print('%dnd element: %d'%(j, i))
elif j == 3: print('%drd element: %d'%(j, i))
else:
print('%dth element: %d'%(j, i))
itarray: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196
0th element: 0
1st element: 1
2nd element: 4
3rd element: 9
4th element: 16
5th element: 25
6th element: 36
7th element: 49
8th element: 64
9th element: 81
10th element: 100
11th element: 121
12th element: 144
13th element: 169
14th element: 196
Subscripts¶
We now know, how we construct something that can be passed to a for
loop. This is a good start. But iterables have other very useful
properties. For instance, have you ever wondered, what actually happens
in the following snippet?
a = 'micropython'
a[5]
'p'
a
is a string, therefore, an iterable. Where does the interpreter
know from, that it has got to return p
, when asked for a[5]
? Or
have you ever been curious to know, how the interpreter replaces p
by q
, if
a = [c for c in 'micropyton']
a[5] = 'q'
a
['m', 'i', 'c', 'r', 'o', 'q', 'y', 't', 'o', 'n']
is passed to it? If so, then it is your lucky day, because we are going to make our iterable class be able to deal with such requests.
The code snippets above rely on a single special method, the
subscription. In the C code of micropython, this method is called
.subscr
, and it should be assigned to in the class declaration,
i.e., if we take makeiterable.c
as our basis for the following
discussion, then we would have to extend the iterable_array_type
as
const mp_obj_type_t iterable_array_type {
...
.subscr = itarray_subscr
}
where the signature of itarray_subscr
has the form
STATIC mp_obj_t itarray_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value)
If .subscr
is not implemented, but you are daring enough to call
>>> a[5]
all the same, then the interpreter is going to throw a TypeError
:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'itarray' object isn't subscriptable
So, what happens in the method that we assigned in
iterable_array_type
? A possible scenario is given below:
STATIC mp_obj_t subitarray_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
subitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
size_t idx = mp_obj_get_int(index);
if(self->len <= idx) {
mp_raise_ValueError("index is out of range");
}
if (value == MP_OBJ_SENTINEL) { // simply return the value at index, no assignment
return MP_OBJ_NEW_SMALL_INT(self->elements[idx]);
} else { // value was passed, replace the element at index
self->elements[idx] = mp_obj_get_int(value);
}
return mp_const_none;
}
subitarray_subscr
takes three arguments: the first is the instance
on which the method is called, i.e., self
. The second is the index,
i.e., what stands in []. And finally, the third argument is the value.
This is what we assign to the element at index idx
, or, when we do
not assign anything (i.e., when we load a value from the iterable),
then value
takes on a special value. If we have
>>> a[5]
on the python console, then the interpreter will automatically assign
value = MP_OBJ_SENTINEL
(this is defined in obj.h
), so that,
though we did not explicitly set anything to it, we can still inspect
value
. This is what happens, when we evaluate
value == MP_OBJ_SENTINEL
: if this statement is true, then we query
for a[5]
. Note that we also implemented some very rudimentary error
checking: we raise an IndexError
, whenever the index is out of
range. We do this by calling
mp_raise_msg(&mp_type_IndexError, "index is out of range");
For a thorough discussion on how to raise exceptions see the Section Error handling.
There is one more thing that we should notice: at the very beginning of the function, in the line
size_t idx = mp_obj_get_int(index);
we call mp_obj_get_int
. This means that any python object with an
integer value is a valid argument, i.e., the following instruction would
still work
%%micropython
a = 'micropython'
b = 5
print(a[b])
p
For compiling, here is the complete code:
#include <stdlib.h>
#include "py/obj.h"
#include "py/runtime.h"
typedef struct _subitarray_obj_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
uint16_t *elements;
size_t len;
} subitarray_obj_t;
const mp_obj_type_t subiterable_array_type;
mp_obj_t mp_obj_new_subitarray_iterator(mp_obj_t , size_t , mp_obj_iter_buf_t *);
STATIC void subitarray_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
subitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "subitarray: ");
uint16_t i;
for(i=0; i < self->len-1; i++) {
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
mp_print_str(print, ", ");
}
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
}
STATIC mp_obj_t subitarray_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
subitarray_obj_t *self = m_new_obj(subitarray_obj_t);
self->base.type = &subiterable_array_type;
self->len = mp_obj_get_int(args[0]);
uint16_t *arr = malloc(self->len * sizeof(uint16_t));
for(uint16_t i=0; i < self->len; i++) {
arr[i] = i*i;
}
self->elements = arr;
return MP_OBJ_FROM_PTR(self);
}
STATIC mp_obj_t subitarray_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
return mp_obj_new_subitarray_iterator(o_in, 0, iter_buf);
}
STATIC mp_obj_t subitarray_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
subitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
size_t idx = mp_obj_get_int(index);
if(self->len <= idx) {
mp_raise_msg(&mp_type_IndexError, "index is out of range");
}
if (value == MP_OBJ_SENTINEL) { // simply return the value at index, no assignment
return MP_OBJ_NEW_SMALL_INT(self->elements[idx]);
} else { // value was passed, replace the element at index
self->elements[idx] = mp_obj_get_int(value);
}
return mp_const_none;
}
const mp_obj_type_t subiterable_array_type = {
{ &mp_type_type },
.name = MP_QSTR_subitarray,
.print = subitarray_print,
.make_new = subitarray_make_new,
.getiter = subitarray_getiter,
.subscr = subitarray_subscr,
};
STATIC const mp_rom_map_elem_t subscriptiterable_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_subscriptiterable) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_square), (mp_obj_t)&subiterable_array_type },
};
STATIC MP_DEFINE_CONST_DICT(subscriptiterable_module_globals, subscriptiterable_module_globals_table);
const mp_obj_module_t subscriptiterable_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&subscriptiterable_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_subscriptiterable, subscriptiterable_user_cmodule, MODULE_SUBSCRIPTITERABLE_ENABLED);
// itarray iterator
typedef struct _mp_obj_subitarray_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
mp_obj_t subitarray;
size_t cur;
} mp_obj_subitarray_it_t;
mp_obj_t subitarray_iternext(mp_obj_t self_in) {
mp_obj_subitarray_it_t *self = MP_OBJ_TO_PTR(self_in);
subitarray_obj_t *subitarray = MP_OBJ_TO_PTR(self->subitarray);
if (self->cur < subitarray->len) {
// read the current value
uint16_t *arr = subitarray->elements;
mp_obj_t o_out = MP_OBJ_NEW_SMALL_INT(arr[self->cur]);
self->cur += 1;
return o_out;
} else {
return MP_OBJ_STOP_ITERATION;
}
}
mp_obj_t mp_obj_new_subitarray_iterator(mp_obj_t subitarray, size_t cur, mp_obj_iter_buf_t *iter_buf) {
assert(sizeof(mp_obj_subitarray_it_t) <= sizeof(mp_obj_iter_buf_t));
mp_obj_subitarray_it_t *o = (mp_obj_subitarray_it_t*)iter_buf;
o->base.type = &mp_type_polymorph_iter;
o->iternext = subitarray_iternext;
o->subitarray = subitarray;
o->cur = cur;
return MP_OBJ_FROM_PTR(o);
}
https://github.com/v923z/micropython-usermod/tree/master/snippets/subscriptiterable/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/subscriptiterable.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SUBSCRIPTITERABLE_ENABLED=1 all
%%micropython
import subscriptiterable
a = subscriptiterable.square(15)
print(a)
print('the fourth element is %d'%a[3])
a[10] = 0
print(a)
subitarray: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196
the fourth element is 9
subitarray: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 0, 121, 144, 169, 196
Index reversing¶
Now, the code above works for non-negative indices, but in python it is quite customary to have something like
a = 'micropython'
a[-2]
'o'
which is equivalent to querying for the last but one element (second
from the right) in the iterable. Knowing how long the iterable is (this
is stored in self->len
), it is a trivial matter to modify our code
in such a way that it can return the values at negative indices.
Slicing¶
In the previous two sections we have worked with single elements of an
iterable. But python wouldn’t be python without slices. Slices are index
ranges specified in a start:end:step
format. Taking our earlier
example, we can print every second character in micropython
by
a = 'micropython'
a[0:8:2]
'mcoy'
This behaviour is also part of the .subscr
special method. Let us
implement it, shall we? In order to simplify the discussion, we will
treat one case only: returning values, and we return a new instance of
the array, if a slice was requested, while a single number, if we passed
a single index.
Since we want to return an array if the indices stem from a slice, we
split our original subscriptitarray_make_new
function, and separate
those parts that reserve space for the array from those that do the
assignments.
It shouldn’t come as a surprise that we have to modify the function that
was hooked up to .subscr
. Let us take a look at the following
snippet:
STATIC mp_obj_t sliceitarray_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
sliceitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
if (value == MP_OBJ_SENTINEL) { // simply return the values at index, no assignment
#if MICROPY_PY_BUILTINS_SLICE
if (mp_obj_is_type(index, &mp_type_slice)) {
mp_bound_slice_t slice;
mp_seq_get_fast_slice_indexes(self->len, index, &slice);
uint16_t len = (slice.stop - slice.start) / slice.step;
sliceitarray_obj_t *res = create_new_sliceitarray(len);
for(size_t i=0; i < len; i++) {
res->elements[i] = self->elements[slice.start+i*slice.step];
}
return MP_OBJ_FROM_PTR(res);
}
#endif
// we have a single index, return a single number
size_t idx = mp_obj_get_int(index);
return MP_OBJ_NEW_SMALL_INT(self->elements[idx]);
} else { // do not deal with assignment, bail out
return mp_const_none;
}
return mp_const_none;
}
As advertised, we treat only the case, when value
is empty, i.e., it
is equal to an MP_OBJ_SENTINEL
. Now, there is no point in trying to
read out the parameters of a slice, if the slice object is not even
defined, is there? This is the case for the minimal ports. So, in order
to prevent nasty things from happening, we insert the #if/#endif
macro with the parameter MICROPY_PY_BUILTINS_SLICE
. Provided that
MICROPY_PY_BUILTINS_SLICE
is defined, we inspect the index, and find
out if it is a slice by calling
mp_obj_is_type(index, &mp_type_slice)
If so, we attempt to load the slice parameters into the slice
object
with
mp_seq_get_fast_slice_indexes(self->len, index, &slice)
The function mp_seq_get_fast_slice_indexes
returns Boolean true
,
if the increment in the slice is 1, and false
otherwise. For the
goal that we are trying to pursue here, it doesn’t matter what the step
size is, so we don’t care about the return value. But the main purpose
of the function is actually something different: the function expands
the start:end:step
slice into a triplet, and it does so, even if one
or two of the slice parameters are missing. So, start::step
,
start::
, :end:step
etc. will also work. In fact, this is why we
have to pass the length of the array: self->len
will be substituted,
if the :end:
parameter is missing.
Equipped with the values of slice.start
, slice.stop
, and
slice.step
, we can determine the length of the new array, and assign
the values in the for
loop.
https://github.com/v923z/micropython-usermod/tree/master/snippets/sliceiterable/sliceiterable.c
#include <stdlib.h>
#include "py/obj.h"
#include "py/runtime.h"
typedef struct _sliceitarray_obj_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
uint16_t *elements;
size_t len;
} sliceitarray_obj_t;
const mp_obj_type_t sliceiterable_array_type;
mp_obj_t mp_obj_new_sliceitarray_iterator(mp_obj_t , size_t , mp_obj_iter_buf_t *);
STATIC void sliceitarray_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
sliceitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "sliceitarray: ");
uint16_t i;
for(i=0; i < self->len-1; i++) {
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
mp_print_str(print, ", ");
}
mp_obj_print_helper(print, mp_obj_new_int(self->elements[i]), PRINT_REPR);
}
sliceitarray_obj_t *create_new_sliceitarray(uint16_t len) {
sliceitarray_obj_t *self = m_new_obj(sliceitarray_obj_t);
self->base.type = &sliceiterable_array_type;
self->len = len;
uint16_t *arr = malloc(self->len * sizeof(uint16_t));
self->elements = arr;
return self;
}
STATIC mp_obj_t sliceitarray_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
sliceitarray_obj_t *self = create_new_sliceitarray(mp_obj_get_int(args[0]));
for(uint16_t i=0; i < self->len; i++) {
self->elements[i] = i*i;
}
return MP_OBJ_FROM_PTR(self);
}
STATIC mp_obj_t sliceitarray_getiter(mp_obj_t o_in, mp_obj_iter_buf_t *iter_buf) {
return mp_obj_new_sliceitarray_iterator(o_in, 0, iter_buf);
}
STATIC mp_obj_t sliceitarray_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
sliceitarray_obj_t *self = MP_OBJ_TO_PTR(self_in);
if (value == MP_OBJ_SENTINEL) { // simply return the values at index, no assignment
#if MICROPY_PY_BUILTINS_SLICE
if (mp_obj_is_type(index, &mp_type_slice)) {
mp_bound_slice_t slice;
mp_seq_get_fast_slice_indexes(self->len, index, &slice);
printf("start: %ld, stop: %ld, step: %ld\n", slice.start, slice.stop, slice.step);
uint16_t len = (slice.stop - slice.start + slice.step - 1) / slice.step;
sliceitarray_obj_t *res = create_new_sliceitarray(len);
for(size_t i=0; i < len; i++) {
res->elements[i] = self->elements[slice.start+i*slice.step];
}
return MP_OBJ_FROM_PTR(res);
}
#endif
// we have a single index, return a single number
size_t idx = mp_obj_get_int(index);
return MP_OBJ_NEW_SMALL_INT(self->elements[idx]);
} else { // do not deal with assignment, bail out
return mp_const_none;
}
return mp_const_none;
}
const mp_obj_type_t sliceiterable_array_type = {
{ &mp_type_type },
.name = MP_QSTR_sliceitarray,
.print = sliceitarray_print,
.make_new = sliceitarray_make_new,
.getiter = sliceitarray_getiter,
.subscr = sliceitarray_subscr,
};
STATIC const mp_rom_map_elem_t sliceiterable_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sliceiterable) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_square), (mp_obj_t)&sliceiterable_array_type },
};
STATIC MP_DEFINE_CONST_DICT(sliceiterable_module_globals, sliceiterable_module_globals_table);
const mp_obj_module_t sliceiterable_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&sliceiterable_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_sliceiterable, sliceiterable_user_cmodule, MODULE_SLICEITERABLE_ENABLED);
// itarray iterator
typedef struct _mp_obj_sliceitarray_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
mp_obj_t sliceitarray;
size_t cur;
} mp_obj_sliceitarray_it_t;
mp_obj_t sliceitarray_iternext(mp_obj_t self_in) {
mp_obj_sliceitarray_it_t *self = MP_OBJ_TO_PTR(self_in);
sliceitarray_obj_t *sliceitarray = MP_OBJ_TO_PTR(self->sliceitarray);
if (self->cur < sliceitarray->len) {
// read the current value
uint16_t *arr = sliceitarray->elements;
mp_obj_t o_out = MP_OBJ_NEW_SMALL_INT(arr[self->cur]);
self->cur += 1;
return o_out;
} else {
return MP_OBJ_STOP_ITERATION;
}
}
mp_obj_t mp_obj_new_sliceitarray_iterator(mp_obj_t sliceitarray, size_t cur, mp_obj_iter_buf_t *iter_buf) {
assert(sizeof(mp_obj_sliceitarray_it_t) <= sizeof(mp_obj_iter_buf_t));
mp_obj_sliceitarray_it_t *o = (mp_obj_sliceitarray_it_t*)iter_buf;
o->base.type = &mp_type_polymorph_iter;
o->iternext = sliceitarray_iternext;
o->sliceitarray = sliceitarray;
o->cur = cur;
return MP_OBJ_FROM_PTR(o);
}
https://github.com/v923z/micropython-usermod/tree/master/snippets/sliceiterable/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/sliceiterable.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
# !make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SLICEITERABLE_ENABLED=1 all
%%micropython
import sliceiterable
a = sliceiterable.square(20)
print(a)
print(a[1:15:3])
sliceitarray: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361
start: 1, stop: 15, step: 3
sliceitarray: 1, 16, 49, 100, 169
A word of caution is in order here: if the step size is negative, the
array is reversed. This means that slice.start
is larger than
slice.stop
, and when we calculate the length of the new array, we
end up with a negative number. Just saying.
Profiling¶
There are times, when you might want to find out what resources (time and RAM) a particular operation requires. Not because you are nosy, but because the resources of a microcontroller are limited, therefore, if you are out of luck, the desired operation might not even fit within the constraints of the chip. In order to locate the bottleneck, you will need to do a bit of profiling. Or perhaps, a lot. This is what we are going to discuss now.
Since you are not going to face serious difficulties when running micropython on a computer, profiling makes really sense only in the context of the microcontroller, so this might be a golden opportunity to brush up on how the firmware has to be compiled and uploaded. It is not by accident that we spent some time on this at the very beginning of this document.
Profiling in python¶
Measuring time¶
If you are interested in the execution time of a complete function, you can measure it simply by making use of the python interpreter
%%micropython
from utime import ticks_us, ticks_diff
def test_function(n):
for i in range(n):
q = i*i*i
return q # return the last
now = ticks_us()
test_function(100)
then = ticks_diff(ticks_us(), now)
print("function test_function() took %d us to run"%then)
function test_function() took 27 us to run
In fact, since our function is flanked by two other statements, this construct easily lends itself to a decorator implementation, as in (taken from http://docs.micropython.org/en/v1.9.3/pyboard/reference/speed_python.html)
%%micropython
import utime
def timed_function(f, *args, **kwargs):
myname = str(f).split(' ')[1]
def new_func(*args, **kwargs):
t = utime.ticks_us()
result = f(*args, **kwargs)
delta = utime.ticks_diff(utime.ticks_us(), t)
print('Function {} time = {:6.3f}ms'.format(myname, delta/1000))
return result
return new_func
@timed_function
def test_function(n):
for i in range(n):
utime.sleep_ms(10)
test_function(10)
Function test_function time = 100.682ms
(If you need an even better estimate, you can get the ticks twice, and
yank run_my_function()
in the second pass: in this way, you would
get the cost of measuring time itself:
from utime import ticks_us, ticks_diff
now = ticks_us
then = ticks_diff(ticks_us(), now)
print("the time measurement took %d us"%then)
Then you subtract the results of the second measurement from those of the first.)
The memory cost of a function¶
While time is money, RAM is gold. We shouldn’t pass up on that! The
micropython
has a very handy function for printing a pretty summary
of the state of the RAM. You would call it like
%%micropython
import micropython
print(micropython.mem_info())
mem: total=2755, current=663, peak=2289
stack: 928 out of 80000
GC: total: 2072832, used: 704, free: 2072128
No. of 1-blocks: 6, 2-blocks: 3, max blk sz: 6, max free sz: 64745
None
If you call mem_info()
after you executed your function, but before
calling the garbage collector (if that is enabled, that is), then from
the two reports, you can figure out how many bytes the function has
eaten.
Profiling in C¶
With the profiling method above, we can measure the cost of a complete function only, but we cannot say anything about individual instructions in the body. Execution time is definitely a significant issue, but even worse is the problem of RAM: it might happen that the function allocates a huge amount of memory, but cleans up properly before returning. Such a function could certainly wreak havoc, even if it is rather inocuous-looking from the outside. So, what do we do? We should probably just measure. It is not going to hurt.
In the example below (profiling.c
), I discuss both time and RAM
measurements in a single module, because splitting them wouldn’t be
worth the trouble. The function, whose behaviour we inspect, does
nothing, but calculate the length of a three-dimensional vector. With
that, we can figure out, how much the assignment, and how much the
actual calculation cost.
https://github.com/v923z/micropython-usermod/tree/master/snippets/profiling/profiling.c
#include <math.h>
#include <stdio.h>
#include "py/obj.h"
#include "py/runtime.h"
#include "mphalport.h" // needed for mp_hal_ticks_cpu()
#include "py/builtin.h" // needed for mp_micropython_mem_info()
STATIC mp_obj_t measure_cpu(mp_obj_t _x, mp_obj_t _y, mp_obj_t _z) {
size_t start, middle, end;
start = m_get_current_bytes_allocated();
float x = mp_obj_get_float(_x);
float y = mp_obj_get_float(_y);
float z = mp_obj_get_float(_z);
middle = m_get_current_bytes_allocated();
float hypo = sqrtf(x*x + y*y + z*z);
end = m_get_current_bytes_allocated();
mp_obj_t tuple[4];
tuple[0] = MP_OBJ_NEW_SMALL_INT(start);
tuple[1] = MP_OBJ_NEW_SMALL_INT(middle);
tuple[2] = MP_OBJ_NEW_SMALL_INT(end);
tuple[3] = mp_obj_new_float(hypo);
return mp_obj_new_tuple(4, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(measure_cpu_obj, measure_cpu);
STATIC const mp_rom_map_elem_t profiling_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_profiling) },
{ MP_ROM_QSTR(MP_QSTR_measure), MP_ROM_PTR(&measure_cpu_obj) },
};
STATIC MP_DEFINE_CONST_DICT(profiling_module_globals, profiling_module_globals_table);
const mp_obj_module_t profiling_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&profiling_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_profiling, profiling_user_cmodule, MODULE_PROFILING_ENABLED);
The above-mentioned mem_info()
function of the micropython module
can directly be called from C: after including the builtin.h
header,
we can issue mp_micropython_mem_info(0, NULL);
, defined in
modmicropython.c
, which will print everything we need. Although its
signature contains two arguments, a size_t
and an mp_obj_t
pointer to the arguments, the function does not seem to care about them,
so we can pass 0
, and NULL
without any meaning.
The function mp_micropython_mem_info()
doesn’t carry out any
measurements in itself, it is only for pretty printing. The stats are
collected by mp_micropython_mem_total()
,
mp_micropython_mem_current()
, and mp_micropython_mem_peak()
.
Unfortunately, these functions are all declared STATIC, so we cannot
call them from outsize modmicropython.c
. If you need a numeric
representation of the state of the RAM, you can make use of the
m_get_total_bytes_allocated(void)
,
m_get_current_bytes_allocated(void)
, and
m_get_peak_bytes_allocated(void)
functions of py/malloc.c
. All
three return a size_t
.
With the help of these three functions, we could, e.g., return the size
of the consumed memory to the micropython interpreter at the end of our
calculations. This is what we do, when collecting the bits an pieces,
and returning the 4-tuple at the end of the measure_cpu
function.
https://github.com/v923z/micropython-usermod/tree/master/snippets/profiling/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/profiling.c
# We can add our module folder to include paths if needed
# This is not actually needed in this example.
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_PROFILING_ENABLED=1 all
%%micropython
import profiling
print(profiling.measure(123, 233, 344))
(672, 672, 672, 433.305908203125)
Working with larger modules¶
Once you add more and more functionality and functions to your module,
it will become unmanageably, and it might make more sense to split the
module into smaller components. We are going to hack our very first
module, simplefunction
, and factor out the function in it.
Since we will want to refer to our functions in the module definition,
we have to declare them in a header file. Let us call this file
helper.h
. The functions declared therein operate on micropython
types, so do not forget to include py/obj.h
, and possibly
py/runtime.h
!
https://github.com/v923z/micropython-usermod/tree/master/snippets/largemodule/helper.h
#include "py/obj.h"
#include "py/runtime.h"
mp_obj_t largemodule_add_ints(mp_obj_t , mp_obj_t );
mp_obj_t largemodule_subtract_ints(mp_obj_t , mp_obj_t );
Next, in helper.c
, we have to implement the functions. helper.c
should also contain the declarations, i.e., header.h
has to be
included.
https://github.com/v923z/micropython-usermod/tree/master/snippets/largemodule/helper.c
#include "helper.h"
mp_obj_t largemodule_add_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
return mp_obj_new_int(a + b);
}
mp_obj_t largemodule_subtract_ints(mp_obj_t a_obj, mp_obj_t b_obj) {
int a = mp_obj_get_int(a_obj);
int b = mp_obj_get_int(b_obj);
return mp_obj_new_int(a - b);
}
Finally, in the module implementation, we include helper.h
, and
create the function objects with MP_DEFINE_CONST_FUN_OBJ_2
, and its
relatives. The rest of the code is equivalent to simplefunction.c
,
with the only exception of the module name.
https://github.com/v923z/micropython-usermod/tree/master/snippets/largemodule/largemodule.c
#include "py/obj.h"
#include "py/runtime.h"
#include "helper.h"
STATIC MP_DEFINE_CONST_FUN_OBJ_2(largemodule_add_ints_obj, largemodule_add_ints);
STATIC MP_DEFINE_CONST_FUN_OBJ_2(largemodule_subtract_ints_obj, largemodule_subtract_ints);
STATIC const mp_rom_map_elem_t largemodule_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_largemodule) },
{ MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&largemodule_add_ints_obj) },
{ MP_ROM_QSTR(MP_QSTR_subtract_ints), MP_ROM_PTR(&largemodule_subtract_ints_obj) },
};
STATIC MP_DEFINE_CONST_DICT(largemodule_module_globals, largemodule_module_globals_table);
const mp_obj_module_t largemodule_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&largemodule_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_largemodule, largemodule_user_cmodule, MODULE_LARGEMODULE_ENABLED);
Now, since we have multiple files in our module, we have to change the
makefile
accordingly, and before linking, we have to compile both
helper.c
, and largemodule.c
, thus, we add
$(USERMODULES_DIR)/helper.c
, and
$(USERMODULES_DIR)/largemodule.c
to SRC_USERMOD
.
https://github.com/v923z/micropython-usermod/tree/master/snippets/largemodule/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/helper.c
SRC_USERMOD += $(USERMODULES_DIR)/largemodule.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_LARGEMODULE_ENABLED=1 all
%%micropython
import largemodule
print(largemodule.add_ints(1, 2))
print(largemodule.subtract_ints(1, 2))
3
-1
A word for the lazy¶
If you still find that coding in C is too cumbersome, you can try your hand at a stub generator, e.g., https://github.com/pazzarpj/micropython-ustubby, or https://gitlab.com/oliver.robson/mpy-c-stub-gen . These tools allow you to convert your python code into C code. Basically, they will produce a boilerplate file, which you can flesh out with the C implementation of the required functionality.
Looking at the usage examples, it is clear to me that one can save a lot of typing with these stub generators, but one will still need a basic understanding of how to work with the micropython C code.
Outline of a math library¶
As I indicated at the very beginning, my main motivation for writing this document was that I wanted to have a reasonable programming manual for the development of a math library. Since I couldn’t find any, I have turned the problem around, and written up, what I have learnt by developing the library. But the question is, what this library should achieve in the first place?
Requirements¶
Recently, I have run into some limitations with the micropython
interpreter. These difficulties were related to both speed, and RAM.
Therefore, I wanted to have something that can perform common
mathematical calculations in a pythonic way, with little burden on the
RAM, and possibly fast. On PCs, such a library is called numpy
, and
it felt only natural to me to implement those aspects of numpy
that
would find an applications in the context of data acquisition of
moderate volume: after all, no matter what, the microcontroller is not
going to produce or collect huge amounts of data, but it might still be
useful to process these data within the constraints of the
microcontroller. Due to the nature of the data that would be dealt with,
one can work with a very limited subset of numpy
.
Keeping these considerations in mind, I set my goals as follows:
- One should be able to vectorise standard mathematical functions, while these functions should still work for scalars, so
a = 1.0
sin(a)
and
a = [1.0, 2.0, 3.0]
sin(a)
should both be valid expressions.
- There should be a binary container, (
ndarray
) for numbers that are results of vectorised operations, and one should be able to initialise a container by passing arbitraryiterables
to a constructor (seesin([1, 2, 3])
above). - The array should be iterable, so that we can turn it into lists, tuples, etc.
- The relevant binary operations should work on arrays as in
numpy
, that is, e.g.,
>>> a = ndarray([1, 2, 3, 4])
>>> (a + 1) + a*10
should evaluate to ndarray([12, 23, 34, 45])
.
- 2D arrays (matrices) could be useful (see below), thus, the
above-mentioned container should be able to store its
shape
. - Having matrices, it is only natural to implement standard matrix operations (inversion, transposition etc.)
- These numerical arrays and matrices should have a reasonable visual representation (pretty printing)
- With the help of matrices, one can also think of polynomial fits to measurement data
- There should be an FFT routine that can work with linear arrays. I do not think that 2D transforms would be very useful for data that come from the ADC of the microcontroller, but being able to extract frequency components of 1D signals would be an asset.
And this is, how ulab
was born. But that is another story, for
another day https://github.com/v923z/micropython-ulab/.