MicroPython の外部 C モジュール

MicroPython で使用するモジュールを開発していると、特定のハードウェアリソースにアクセスできなかったり、Python の速度に制限があったりするために、Python 環境の限界に直面することがよくあります。

MicroPython の速度を最大化する で示した方法でも制限を解消できない場合は、モジュールの一部または全部を C 言語(および 移植先で実装されていれば C++ も)で記述するのが有効な選択肢となります。

モジュールが一般的に入手可能なハードウェアやライブラリにアクセスしたり連携したりするように設計されている場合は、類似のモジュールと並べて MicroPython のソースツリー内に実装し、プルリクエストとして提出することを検討してください。一方で、無名または独自仕様のシステムを対象としている場合は、メインの MicroPython リポジトリの外部に保持する方が理にかなっているかもしれません。

この章では、こうした外部モジュールを MicroPython の実行ファイルやファームウェアイメージにコンパイルする方法について説明します。Make と CMake の両方のビルドツールがサポートされており、外部モジュールを作成する際には、すべての移植先でモジュールを使用できるよう、両方のツール向けのビルドファイルを追加しておくとよいでしょう。ただし、特定の移植先をコンパイルする際には、Make か CMake のどちらか一方のビルド方法だけを使用すれば十分です。

別のアプローチとして .mpy ファイル内のネイティブマシンコード を使用する方法があります。これは、メインのファームウェアを再コンパイルすることなく、実行中の MicroPython システムへ動的にインポートできる .mpy ファイルにカスタム C コードを配置できる仕組みです。

外部 C モジュールの構造

MicroPython のユーザー C モジュールは、以下のファイルを含むディレクトリです。

  • モジュール用の *.c / *.cpp / *.h ソースコードファイル。

    これらには通常、実装する低レベルの機能と、関数やモジュールを公開するための MicroPython バインディング関数が含まれます。

    現時点でこれらの関数やモジュールを記述する際の最適な参考資料は、MicroPython ツリー内の類似のモジュールを見つけて、それらを例として利用することです。

  • micropython.mk には、このモジュール用の Makefile フラグメントが含まれます。

    micropython.mk の中では、モジュールディレクトリへのパスとして $(USERMOD_DIR) が利用できます。これは C モジュールごとに再定義されるため、micropython.mk 内でローカルの make 変数に展開しておくべきです。例: EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    micropython.mk では、モジュールのソースファイルを SRC_USERMOD_C または SRC_USERMOD_LIB_C 変数に追加する必要があります。前者は MP_QSTR_ および MP_REGISTER_MODULE の定義について処理されますが、後者は処理されません(例: MicroPython 固有でないヘルパーやライブラリコード)。これらのパスには、展開した $(USERMOD_DIR) のコピーを含める必要があります。例:

    SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c
    SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
    

    同様に、C++ ソースファイルには SRC_USERMOD_CXX および SRC_USERMOD_LIB_CXX を使用します。アセンブリファイルを含めたい場合は SRC_USERMOD_LIB_ASM を使用します。

    カスタムコンパイラオプション(ヘッダーファイルを検索するディレクトリを追加する -I など)がある場合は、C コードについては CFLAGS_USERMOD に、C++ コードについては CXXFLAGS_USERMOD に追加してください。

  • micropython.cmake には、このモジュール用の CMake 設定が含まれます。

    micropython.cmake の中では、現在のモジュールへのパスとして ${CMAKE_CURRENT_LIST_DIR} を使用できます。

    micropython.cmake では、INTERFACE ライブラリを定義し、ソースファイル、コンパイル定義、インクルードディレクトリをそれに関連付ける必要があります。その後、そのライブラリを usermod ターゲットにリンクします。

    add_library(usermod_cexample INTERFACE)
    
    target_sources(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c
    )
    
    target_include_directories(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}
    )
    
    target_link_libraries(usermod INTERFACE usermod_cexample)
    

    完全な使用例については以下を参照してください。

基本的な例

cexample モジュールは、関数とクラスの例を提供します。cexample.add_ints(a, b) 関数は 2 つの整数引数を加算して結果を返します。cexample.Timer() 型は、オブジェクトがインスタンス化されてから経過した時間を計測するために使用できるタイマーを作成します。

このモジュールは MicroPython のソースツリーの examples ディレクトリ内 にあり、上記で説明した内容のソースファイルと Makefile フラグメントを備えています:

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

詳しい説明についてはこれらのファイル内のコメントを参照してください。cexample モジュールの隣には cppexample もあり、これは同じように動作しますが、MicroPython で C と C++ のコードを混在させる方法の一例を示しています。

cmodule を MicroPython にコンパイルする

このようなモジュールをビルドするには、MicroPython をコンパイルし(getting started を参照)、次の 2 つの変更を適用します。

  1. ビルド時フラグ USER_C_MODULES を、含めたいモジュールを指すように設定します。Make を使用する移植先では、この変数はモジュールが自動的に検索されるディレクトリにする必要があります。CMake を使用する移植先では、この変数はビルドするモジュールをインクルードするファイルにする必要があります。詳細は以下を参照してください。

  2. 対応する C プリプロセッサマクロを 1 に設定して、モジュールを有効化します。これは、ビルドするモジュールが自動的に有効化されない場合にのみ必要です。

MicroPython に付属する例のモジュールをビルドするには、USER_C_MODULES を、Make の場合は examples/usercmodule ディレクトリに、CMake の場合は examples/usercmodule/micropython.cmake に設定します。

例として、unix の移植先を例のモジュールとともにビルドする方法を示します。

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

ビルドに新しいユーザーモジュールを含める際には、最初に一度 make clean を実行する必要がある場合があります。ビルド出力には、見つかったモジュールが表示されます:

...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...

rp2 のような CMake ベースの移植先では、これは少し異なって見えます(CMake は実際には make によって呼び出される点に注意してください)。

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

ここでも、CMake にユーザーモジュールを認識させるには、まず make clean を実行する必要がある場合があります。CMake のビルド出力には、モジュールが名前で一覧表示されます:

...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...

トップレベルの micropython.cmake の内容を使用して、どのモジュールを有効にするかを制御できます。

自分自身のプロジェクトでは、カスタムコードをメインの MicroPython ソースツリーの外部に保持する方が便利です。そのため、典型的なプロジェクトのディレクトリ構成は次のようになります:

my_project/
├── modules/
│   ├── example1/
│   │   ├── example1.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   ├── example2/
│   │   ├── example2.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   └── micropython.cmake
└── micropython/
    ├──ports/
   ... ├──stm32/
      ...

Make でビルドする場合は、USER_C_MODULESmy_project/modules ディレクトリに設定します。例として、stm32 の移植先をビルドする場合:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

CMake でビルドする場合、my_project/modules ディレクトリの直下にあるトップレベルの micropython.cmake で、利用可能にしたいすべてのモジュールを include する必要があります。

include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)

そして次のコマンドでビルドします。

cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake

USER_C_MODULES には絶対パスを指定することもできます。

USER_C_MODULES 変数で指定されたすべてのモジュール(Make を使用する場合はこのディレクトリ内で見つかったもの、CMake を使用する場合は include で追加されたもの)はコンパイルされますが、有効化されているものだけがインポート可能になります。ユーザーモジュールは通常デフォルトで有効化されており(これはモジュールの開発者が決定します)、その場合は上記のように USER_C_MODULES を設定するだけで、それ以上は何もする必要がありません。

モジュールがデフォルトで有効化されていない場合は、対応する C プリプロセッサマクロを有効にする必要があります。このマクロ名は、モジュールのソースコード内で MP_REGISTER_MODULE の行を検索することで見つけられます(通常、メインソースファイルの末尾に現れます)。このマクロは #if X / #endif のペアで囲まれているはずで、モジュールを利用可能にするには CFLAGS_EXTRA を使って設定オプション X を 1 に設定する必要があります。#if X / #endif のペアがない場合、そのモジュールはデフォルトで有効化されています。

例えば、examples/usercmodule/cexample モジュールはデフォルトで有効化されているため、ソースコードに次の行が含まれています。

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

あるいは、このモジュールをデフォルトでは無効にしつつプリプロセッサ設定オプションで選択できるようにするには、次のようになります。

#if MODULE_CEXAMPLE_ENABLED
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
#endif

この場合、make コマンドに CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 を追加するか、mpconfigport.h または mpconfigboard.h を編集して次を追加することで、モジュールが有効化されます。

#define MODULE_CEXAMPLE_ENABLED (1)

正確な方法は移植先によって異なる点に注意してください。移植先ごとに構造が違うためです。正しく行わないとコンパイルは通りますが、インポート時にモジュールが見つかりません。

MicroPython でのモジュールの使用

自分の MicroPython にビルドして組み込んだら、このモジュールは他の任意の組み込みモジュールと同じように Python から利用できます。例:

import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms

watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000

C の動的メモリ割り当て

MicroPython は メモリ管理 のために独自の「Python ヒープ」を使用します。これは、C ライブラリ関数 malloc()free() などが使用する「C ヒープ」とは異なります。MicroPython のすべての移植先が「C ヒープ」を備えているわけではありません。

Tier 1 および 2 の移植先では、「C ヒープ」を介した C の動的メモリ割り当てのサポートはまちまちです。

  • unix、windows、esp32、webassembly の移植先は C の動的メモリ割り当てをサポートしています。

  • rp2 の移植先では、C ヒープ用に n バイトのメモリを予約するために MICROPY_C_HEAP_SIZE=n を指定してファームウェアをビルドしない限り、実行時のメモリ割り当てがすべて失敗します。このメモリは Python コードでは利用できません。

  • alif、mimxrt、nrf、renesas-ra、samd、stm32 の移植先で動的な C 割り当てを含めてビルドすると、undefined reference to `malloc' などのエラーでリンク時に失敗します。MicroPython にはこれらの移植先での動的な C 割り当てに対する組み込みサポートがありません。何らかの解決策には、カスタムビルドに C ヒープの実装を手動で追加する必要があります。

  • zephyr の移植先は、現在ユーザーモジュールを含めたビルドをサポートしていません。

C ヒープとしての Python ヒープ

代わりに、C コードから m_malloc()m_malloc0()m_free() といった「Python ヒープ」の動的割り当て関数を呼び出す方が実用的な場合があります。

このアプローチの詳細については Cコードから見たMicroPythonのメモリ を参照してください。

C++ モジュール

ほとんどの Tier 1 および 2 の MicroPython 移植先(および一部の Tier 3)は、上記で説明した C++ 固有の環境変数を使用して、C++ ユーザーモジュールのビルドをサポートしています。

C++ と MicroPython をうまく統合するには、いくつかの追加の考慮事項があります。

C++ の動的メモリ割り当て

C++ プログラム(および C++ 標準ライブラリの機能)は、通常、動的メモリ割り当てを使用します。C++ のデフォルトのメモリアロケータ(すなわち new および delete 演算子)は、通常 C の動的メモリ割り当て の上に積み重ねられたレイヤーとして実装されています。

C の動的メモリ割り当てのサポートを含まない MicroPython の移植先では、C++ の動的メモリ割り当ては次の 2 つの方法のいずれかでサポートできます。

  • カスタムビルドに C の動的メモリ割り当てを実装する。

  • カスタムビルドにカスタムの C++ アロケータを実装する。

リンケージに関する考慮事項

MicroPython は C ベースのプロジェクトであるため、MicroPython にリンクする、あるいは MicroPython からリンクされるシンボルはすべて、C++ コード内で extern "C" として修飾する必要があります。

examples/usercmodule/cppexample で示されているパターンに従うことを強くお勧めします。そこでは、Python モジュールが C++ コードを包む最小限の C ファイルラッパー内に実装されています。