stinos / micropython-wrap

API for interop between C/C++ and MicroPython
MIT License
124 stars 23 forks source link
micropython

Build status

MicroPython-Wrap

This header-only C++ library provides some interoperability between C/C++ and the MicroPython programming language.

The standard way of extending MicroPython with your own C or C++ modules involves a lot of boilerplate, both for converting function arguments and return values between native types and the MicroPython object model and for regsitering the function and type names so they can be discovered by MicroPython. Using MicroPython-Wrap most of that boilerplate is avoided and instead one can focus on writing the actual C and/or C++ code while the process of integration with MicroPython comes down to adding two lines of code for every function/method or type which needs to be available in your scripts.

WARNING: while fully tested and daily in use without problems, this project should still be considered to be in beta stage and is subject to changes of the code base, including project-wide name changes and API changes. Furthermore the actual integration at the build level is not too straightforward, see below.

Platforms

In principle any MicroPython port which has a working C++ compiler should work since the code is just standard C++. Has been tested for the unix port with gcc, the esp32 port with ESP-IDF sdk v4, and the windows port with gcc (from MSYS2) and msvc.

Example

Complete usage examples covering all aspects can be found in the the tests directory which also serves as documentation: in module.cpp a micropython module is created and a bunch of C++ classes and functions are added to the module. Consequently when running the python test code using the standard MicroPython test runner the module is imported and all registered functions are called.

Just to get an idea here is a short sample of C++ code registration; code achieving the same using just the MicroPython API is not shown here but would likely be around 50 lines:

#include <micropython-wrap/functionwrapper.h>

//function we want to call from within a MicroPython script
std::vector< std::string > SomeFunction( std::vector< std::string > vec )
{
  for( auto& v : vec )
    v += "TRANSFORM";
  return vec;
}

//function names are declared in structs
struct FunctionNames
{
  func_name_def( TransformList )
};

extern "C"
{
  void RegisterMyModule(void)
  {
    //register a module named 'foo'
    auto mod = upywrap::CreateModule( "foo" );

    //register our function with the name 'TransformList'
    //conversion of a MicroPython list of strings is done automatically
    upywrap::FunctionWrapper wrapfunc( mod );
    wrapfunc.Def< FunctionNames::TransformList >( SomeFunction );
  }
}

//now call RegisterMyModule() in MicroPython's main() for example

And the MicroPython code making use of this looks like:

import foo

print(foo.TransformList(['a', 'b']))  # Prints ['aTRANSFORM', 'bTRANSFORM']

Type Conversion

Conversion between standard native types and mp_obj_t, the MicroPython opaque object type is declared in two template classes aptly named ToPyObj and FromPyObj.

Currently these conversions are supported (depending on C++ standard used):

uPy double <-> double/float
uPy int <-> std::int16_t/std::int32_t/std::int64_t/std::uint16_t/std::uint32_t/std::uint64_t with overflow checks
uPy bool <-> bool
uPy str <-> std::string/std::string_view
uPy str <-> const char* (optional)
uPy tuple <-> std::tuple/std::pair
uPy list <-> std::vector (each element must be of the same type)
uPy dict <-> std::map (each key/value must be of the same type)
uPy callable <-> std::function (None maps to empty std::function)
uPy None <-> std::optional (i.e. std::nullopt <-> None, otherwise value gets converted)
uPy None <- empty std::shared_ptr
uPy None <- std::error_code (if empty, otherwise throws runtime_error)

Function and class wrapping

Wrapping code is provided for:

uPy functions <-> free functions via upywrap::FunctionWrapper
uPy class <-> C++ class via upywrap::ClasssWrapper
uPy __init__ <-> C++ class constructor or factory function of choice
uPy __del__ <-> C++ class destructor (called only when instance is grabage collected!)
uPy __exit__ <-> C++ class method with void() signature
uPy __call__ <-> any C++ class method
uPy class methods <-> C++ class methods
uPy class attributes <-> C++ class methods

For builtin types listed under 'type conversion', the native function must take the argument by value, const value or const reference, and only values can be returned. ClassWrapper types can be passed by pointer, value, reference or std::shared_ptr and returned as pointer, reference or std::shared_ptr. See tests for ownership rules.

Furthermore there is optional support for wrapping each native call in a try/catch for std::exception, and re-raise it as a uPy RuntimeError

Optional and keyword argument support

This is supported by naming the arguments and eventually supplying defaults when registering the function in the C++ code, example:

void Foo( int, std::string, const std::vector< int >& );

struct FunctionNames
{
  func_name_def( Foo )
};

extern "C"
{
  void RegisterMyModule(void)
  {
    upywrap::FunctionWrapper wrapfunc( upywrap::CreateModule( "foo" ) );
    //Make the first argument required and the rest optional.
    wrapfunc.Def< FunctionNames::Foo >( Foo, upywrap::Kwargs( "a" )( "b", "default" )( "c", {0, 1} ) );
  }
}

Calling code:

import foo

foo.Foo(0)  # Calls Foo( 0, "default", std::vector< int >{ 0, 1 } ) in C++.
foo.Foo(a=1, c=[2])  # Calls Foo( 1, "default", std::vector< int >{ 2 } ) in C++.

Integrating and Building

First clone this repository alongside the MicroPython repository, then refer to the way the tests module is built and create your own modules in the same way. Also see the Makefile for Unix and Project file for Windows, and the Appveyor config for how builds are done.