A Simple Example

circuitpython_Answer_to_Life.png
Under CC BY-SA 3.0 by Wikipedia user Martinultima

There are three places in the VM codebase that we'll be working: the implementation, the connection into the virtual machine, and integration into the build.

We'll start by exploring this with a very simple example.

shared-module

This is where the implementation goes. We need to add a directory here named for our module: mymodule. In this directory we need to add an __init__.c file that contains module level functions. We also need to add a header and source file for each class in the module. In this case that means MyClass.h and MyClass.c.

__init__.c

As mentioned, the definition of any module level functions go here. This example has none, so it's empty, with a "this space left intentionally blank" comment.

Download: file
// No module functions

MyClass.h

In this file we define the structure that holds the instance variables of our class. In this simple example, all we need to to keep track of whether the instance has been disposed of. Normally there will be other ways of telling this state, and we won't need something specific just for it.

The other variable is required: base provides storage for the basic information for a Python class.

Download: file
#ifndef MICROPY_INCLUDED_MYMODULE_MYCLASS_H
#define MICROPY_INCLUDED_MYMODULE_MYCLASS_H

#include "py/obj.h"

typedef struct {
  mp_obj_base_t base;
  bool deinited;
} mymodule_myclass_obj_t;


#endif // MICROPY_INCLUDED_MYMODULE_MYCLASS_H

MyClass.c

This file contains the methods of MyClass.

It begins by including the corresponding header file as well as some basic runtime support. Then there's the standard constructor that is used to initialize new instances. This example is simple and the constructor takes no arguments. This will typically NOT be the case. The next example shows a constructor with arguments.

Download: file
#include "py/runtime.h"
#include "MyClass.h"

void shared_module_mymodule_myclass_construct(mymodule_myclass_obj_t* self) {
  self->deinited = 0;
}

The deinit related methods handle disposal of instances as well as determining whether an instance has been disposed of. In classes that aren't this trivial, one will usually have a way to identify a valid object inherent to the object itself.

Download: file
bool shared_module_mymodule_myclass_deinited(mymodule_myclass_obj_t* self) {
  return self->deinited;
}

void shared_module_mymodule_myclass_deinit(mymodule_myclass_obj_t* self) {
  self->deinited = 1;
}

The remaining functions implement the class methods and properties.

This example is simple: just two read-only properties that return a fixed value, and nothing has parameters.

The question property is defined in shared_module_mymodule_myclass_get_question. This naming convention is the convention used. Just Do It. Sticking with the established conventions is the safest way to go. You never know when it's depended on. The CircuitPython runtime is complex enough that you don't want to take chances.

Note that these function return native C types. Conversion to CircuitPython runtime types are done in the interface functions below.

Download: file
const char * shared_module_mymodule_myclass_get_question(mymodule_myclass_obj_t* self) {
  return "Tricky...";
}

mp_int_t shared_module_mymodule_myclass_get_answer(mymodule_myclass_obj_t* self) {
  return 42;
}

shared-bindings

In this directory we place the interface for our module. It's the plumbing that connects the implementation to the CircuitPython runtime. Start by adding a directory named after the module as we did for the implementation, e.g. mymodule. We need a couple general files, as well a pair for each class. So at a minimum we need the following.

__init__.h

Even if you don't have anything for this file, it has to be present as shown below.

Download: file
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE___INIT___H
#define MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE___INIT___H

#include "py/obj.h"

// Nothing now.

#endif  // MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE___INIT___H

__init__.c

This file takes care of hooking up the globals provided by the module. In this case it's just the name of the module and the class.

Download: file
#include <stdint.h>

#include "py/obj.h"
#include "py/runtime.h"

#include "shared-bindings/mymodule/__init__.h"
#include "shared-bindings/mymodule/MyClass.h"

STATIC const mp_rom_map_elem_t mymodule_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_mymodule) },
    { MP_ROM_QSTR(MP_QSTR_MyClass), MP_ROM_PTR(&mymodule_myclass_type) },
};

STATIC MP_DEFINE_CONST_DICT(mymodule_module_globals, mymodule_module_globals_table);

const mp_obj_module_t mymodule_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t*)&mymodule_module_globals,
};

MyClass.h

The class header here declares the functions from the implementation. They are declared extern which tells the compiler that they are defined elsewhere and will be available when all the files are linked together. 

Download: file
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE_MYCLASS_H
#define MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE_MYCLASS_H

#include "shared-module/mymodule/MyClass.h"

extern const mp_obj_type_t mymodule_myclass_type;

extern void shared_module_mymodule_myclass_construct(mymodule_myclass_obj_t* self);
extern void shared_module_mymodule_myclass_deinit(mymodule_myclass_obj_t* self);
extern bool shared_module_mymodule_myclass_deinited(mymodule_myclass_obj_t* self);
extern char * shared_module_mymodule_myclass_get_question(mymodule_myclass_obj_t* self);
extern mp_int_t shared_module_mymodule_myclass_get_answer(mymodule_myclass_obj_t* self);

#endif // MICROPY_INCLUDED_SHARED_BINDINGS_MYMODULE_MYCLASS_H

MyClass.c

As usual, we start with some includes: one for your class, and a handful of runtime support headers.

Download: file
#include <stdint.h>
#include <string.h>
#include "lib/utils/context_manager_helpers.h"
#include "py/objproperty.h"
#include "py/runtime.h"
#include "py/runtime0.h"
#include "shared-bindings/mymodule/MyClass.h"
#include "shared-bindings/util.h"

Now we come to the life-cycle functions, including the constructor support.

Download: file
//| .. currentmodule:: mymodule
//|
//| :class:`MyClass` -- The great question (and answer to it) of life, the universe, and everything.
//| ====================================================================================
//|
//| Provides the great question (and answer to it) of life, the universie, and everything.
//|
//| .. class:: MyClass()
//|
//|   Create an object.

STATIC mp_obj_t mymodule_myclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *pos_args) {
  mp_arg_check_num(n_args, n_kw, 0, 0, true);
    mymodule_myclass_obj_t *self = m_new_obj(mymodule_myclass_obj_t);
    self->base.type = &mymodule_myclass_type;
    shared_module_mymodule_myclass_construct(self);
    return MP_OBJ_FROM_PTR(self);
}

//|   .. method:: deinit()
//|
//|      Deinitializes the Meaning and releases any hardware resources for reuse.
//|
STATIC mp_obj_t mymodule_myclass_deinit(mp_obj_t self_in) {
  shared_module_mymodule_myclass_deinit(self_in);
  return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mymodule_myclass_deinit_obj, mymodule_myclass_deinit);

//|   .. method:: __enter__()
//|
//|      No-op used by Context Managers.
//|
//  Provided by context manager helper.

//|   .. method:: __exit__()
//|
//|      Automatically deinitializes the hardware when exiting a context. See
//|      :ref:`lifetime-and-contextmanagers` for more info.
//|
STATIC mp_obj_t mymodule_myclass_obj___exit__(size_t n_args, const mp_obj_t *args) {
  shared_module_mymodule_myclass_deinit(args[0]);
  return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mymodule_myclass___exit___obj, 4, 4, mymodule_myclass_obj___exit__);

Now the actual properties. These simply call the implementation functions. In the case of answer, the implementation 

Download: file
//|   .. attribute:: question
//|
//|     The question of life, the universe and everything
//|
STATIC mp_obj_t mymodule_myclass_obj_get_question(mp_obj_t self_in) {
  char *str = shared_module_mymodule_myclass_get_question(self_in);
  return mp_obj_new_str(str, strlen(str));
}
MP_DEFINE_CONST_FUN_OBJ_1(mymodule_myclass_get_question_obj, mymodule_myclass_obj_get_question);

//|   .. attribute:: answer
//|
//|     The answer to the question of life, the universe and everything
//|
STATIC mp_obj_t mymodule_myclass_obj_get_answer(mp_obj_t self_in) {
  return mp_obj_new_int(shared_module_mymodule_myclass_get_answer(self_in));
}
MP_DEFINE_CONST_FUN_OBJ_1(mymodule_myclass_get_answer_obj, mymodule_myclass_obj_get_answer);

Finally there's code that defines the class locals. This usually serves to bind the method names to the interface functions defines above.

Download: file
const mp_obj_property_t mymodule_myclass_question_obj = {
    .base.type = &mp_type_property,
    .proxy = {(mp_obj_t)&mymodule_myclass_get_question_obj,
              (mp_obj_t)&mp_const_none_obj},
};

const mp_obj_property_t mymodule_myclass_answer_obj = {
    .base.type = &mp_type_property,
    .proxy = {(mp_obj_t)&mymodule_myclass_get_answer_obj,
              (mp_obj_t)&mp_const_none_obj},
};

STATIC const mp_rom_map_elem_t mymodule_myclass_locals_dict_table[] = {
    // Methods
    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mymodule_myclass_deinit_obj) },
    { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&mymodule_myclass___exit___obj) },
    { MP_ROM_QSTR(MP_QSTR_question), MP_ROM_PTR(&mymodule_myclass_question_obj) },
    { MP_ROM_QSTR(MP_QSTR_answer), MP_ROM_PTR(&mymodule_myclass_answer_obj) },
};
STATIC MP_DEFINE_CONST_DICT(mymodule_myclass_locals_dict, mymodule_myclass_locals_dict_table);

const mp_obj_type_t mymodule_myclass_type = {
    { &mp_type_type },
    .name = MP_QSTR_Meaning,
    .make_new = mymodule_myclass_make_new,
    .locals_dict = (mp_obj_dict_t*)&mymodule_myclass_locals_dict,
};

ports/atmel-samd

We'll need to edit two files to hook our new module into the build:

Makefile

We need to add  the c files we added to shared-module to the SRC_SHARED_MODULE list:

mymodule/__init__.c \
mymodule/MyClass.c \

Note the reverse slash at the end of the lines. This is C's line continuation which is needed when defining a multi-line macro.

mpconfigport.h

There are two places in this file that need an addition. 

First we need to add our new module.  Look for a comment very similar to 

// extra built in modules to add to the list of known ones

Add a line to the list immediately following it, similar to the rest. The difference in what you add will be that it mentions your new module:

extern const struct _mp_obj_module_t mymodule_module;

The second thing to do is add your module to the EXTRA_BUILTIN_MODULES macro, with a line like the others there:

{ MP_OBJ_NEW_QSTR(MP_QSTR_mymodule), (mp_obj_t)&mymodule_module }, \

Don't forget that reverse slash at the end of the line. 

In Action

Now build CircuitPython for your board and try it out:

Download: file
>>> import mymodule
>>> dir(mymodule)
['__class__', '__name__', 'MyClass']
>>> dir(mymodule.MyClass)
['__class__', '__enter__', '__exit__', '__name__', 'answer', 'deinit', 'question']
>>> m = mymodule.MyClass()
>>> m.answer
42
>>> m.question
'Tricky...'
This guide was first published on Nov 15, 2018. It was last updated on Nov 15, 2018.
This page (A Simple Example) was last updated on Aug 04, 2020.