ネイティブマシンコードの .mpy ファイルへの埋込み

この章では、Python 以外の言語で作成したネイティブマシンコードを埋め込んだ .mpy ファイルをビルドして操作する方法について説明します。これにより、C などの言語でコードを記述し、コンパイルして.mpyファイルにリンクし、このファイルを通常の Python モジュールのようにインポートできます。これにより、パフォーマンスが重要な機能を実装したり、別の言語で記述された既存のライブラリを使えるようになります。

ネイティブ .mpy ファイルを使用する主な利点の1つは、メインの MicroPython ファームウェアを再構築する必要なく、ネイティブマシンコードをスクリプトによって動的にインポートできることです。 MicroPython 外部 C モジュール でも C 言語でカスタムモジュールを定義できますが、メインファームウェアのイメージにコンパイルする必要があります。

ここでは C 言語を使用してネイティブモジュールを構築する方法について説明しますが、原則的にはスタンドアロンマシンコードにコンパイルできる言語であればなんでも .mpy ファイルに埋め込めます。

ネイティブ .mpy モジュールは、プロジェクトの tools/ ディレクトリにある mpy_ld.py ツール を使ってビルドできます。このツールは、オブジェクトファイル(.oファイル)の1組を取得・リンクして、ネイティブ .mpy ファイルを作成します。ツールの実行には CPython 3 とライブラリ pyelftools の v0.25 以上が必要です。

サポートされている機能と制限

.mpy ファイルには、MicroPython バイトコードやネイティブマシンコードを含められます。ネイティブマシンコードが含まれている場合、 .mpy ファイルには特定のアーキテクチャが関連付けられています。現在サポートされているアーキテクチャは次のとおりです(これらは 後述する ARCH 変数の有効なオプションです)。

  • x86 (32 ビット)
  • x64 (64 ビット x86)
  • armv6m (ARM Thumb, Cortex-M0 など)
  • armv7m (ARM Thumb 2, Cortex-M3 など)
  • armv7emsp (ARM Thumb 2, 単精度浮動小数点, Cortex-M4F, Cortex-M7 など)
  • armv7emdp (ARM Thumb 2, 倍精度浮動小数点, Cortex-M7 など)
  • xtensa (ウィンドウなし, ESP8266 など)
  • xtensawin (ウィンドウサイズ 8, ESP32 など)

ネイティブ .mpy ファイルをコンパイル・リンクする場合、アーキテクチャの選択が必要です。該当ファイルはそのアーキテクチャにのみインポートできます。 .mpy ファイルの詳細については MicroPython .mpy ファイル ファイルを参照してください。

ネイティブコードは位置非依存コード(PIC)としてコンパイルし、グローバルオフセットテーブル(GOT)を使う必要がありますが、この詳細はアーキテクチャによって異なります。ネイティブコードで .mpy ファイルをインポートする場合、インポート機構はネイティブコードの基本的な再配置を行えます。これには、text, rodata, BSS セクションの再配置も含まれます。

リンカーとダイナミックローダーがサポートされている機能は次のとおりです:

  • 実行可能コード (text)
  • 文字列および定数データ(配列、構造体など)を含む読み取り専用データ (rodata)
  • ゼロに初期化されるデータ (BSS)
  • text 中の text, rodata, BSS へのポインター
  • rodata 中の text, rodata, BSS へのポインター

既知の制限は次のとおりです。

  • data セクションはサポートしていません。回避策: BSSデータを使い、データ値を明示的に初期化してください
  • 静的 BSS 変数はサポートしていません。回避策: グローバル BSS 変数を使ってください

このため、C コードに書き込み可能なデータがある場合、データが初期化子なしでグローバルに定義され、関数内でのみ書き込まれていることを確認してください。

リンカーの制限: ネイティブモジュールは、MicroPython ファームウェア全体のシンボルテーブルに対してリンクされていません。むしろ mp_fun_table (py/nativeglue.h 内)にある明示的にエクスポートされたシンボルテーブルに対してリンクされており、これはファームウェアのビルド時に決まってしまいます。したがって、たとえば任意の HAL/OS/RTOS/システムの関数を単純に呼び出すことはできません。

新しいシンボルをテーブルの最後に追加して、ファームウェアを再ビルドできます。シンボルは、同じ場所にある tools/mpy_ld.pyfun_table 辞書にも追加する必要があります。これにより mpy がインポートされたときに mpy_ld.py が新しいシンボルを見つけ出して、再配置できるようになります。シンボルが関数の場合には、マクロまたはスタブを py/dynruntime.h に追加して、関数を簡単に呼び出せるようにする必要があります。

ネイティブモジュールの定義

ネイティブ .mpy モジュールは、.mpy のビルドに使う1組のファイルで定義します。ファイルシステムのレイアウトは、ソースファイルと Makefile の2つの主要部分で構成します。

  • 最も単純なケースでは、単一の C ソースファイルのみが必要です。これには、.mpy モジュールにコンパイルされるすべてのコードを含みます。この C ソースコードには MicroPython 動的 API にアクセスするための py/dynruntime.h ファイルをインクルードし、少なくとも mpy_init という関数を定義する必要があります。この関数は、モジュールのエントリポイントであり、モジュールがインポートされるときに呼び出されます。

    必要に応じて、モジュールを複数の C ソースファイルに分割できます。モジュールの一部は Python でも実装できます。すべてのソースファイルは Makefile 中の SRC 変数に指定する必要があります(後述)。これには C ソースファイルと、結果の .mpy ファイルに含まれる Python ファイルの両方を含めます。

  • Makefile はモジュールのビルド設定および .mpy モジュールをビルドするのに使うソースファイルの指定を含めます。変数 MPY_DIR には MicroPython リポジト(ヘッダファイル、関連する Makefile 断片、 mpy_ld.py ツールが存在)の場所を指定します。変数 MOD にはモジュールの名前を指定します。変数 SRC にはソースファイルの一覧を指定します。オプションでマシンアーキテクチャを指定するには変数 ARCH を使います。さらに py/dynruntime.mk のインクルードも必要です

最小限の例

この章では factorial という簡単なモジュールの完全に機能する例を示します。このモジュールは入力の階乗を計算して結果を返す単一の関数 factorial.factorial(x) を提供します。

ディレクトリのレイアウト

factorial/
├── factorial.c
└── Makefile

ファイル factorial.c の内容は以下のとおりです:

// MicroPython API にアクセスするためのヘッダファイルをインクルード
#include "py/dynruntime.h"

// 階乗を計算するヘルパー関数
STATIC mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// Python から factorial(x) として呼び出される関数
STATIC mp_obj_t factorial(mp_obj_t x_obj) {
    // MicroPython 入力オブジェクトから整数を抽出
    mp_int_t x = mp_obj_get_int(x_obj);
    // 階乗を計算
    mp_int_t result = factorial_helper(x);
    // 計算結果を MicroPython 整数オブジェクトに変換して戻す
    return mp_obj_new_int(result);
}
// 上の関数の Python 参照を定義
STATIC MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// これはエントリポイントであり、モジュールをインポートしたときに呼び出される
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    // これが最初になければならず、グローバル辞書などをセットアップする
    MP_DYNRUNTIME_INIT_ENTRY

    // モジュールの名前空間で関数を利用できるようにする
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // これが最後になければならず、グローバル辞書をリストアする
    MP_DYNRUNTIME_INIT_EXIT
}

ファイル Makefile の内容は以下のとおりです:

# Location of top-level MicroPython directory
MPY_DIR = ../../..

# Name of module
MOD = factorial

# Source files (.c or .py)
SRC = factorial.c

# Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin)
ARCH = x64

# Include to get the rules for compiling and linking the module
include $(MPY_DIR)/py/dynruntime.mk

モジュールのコンパイル

ネイティブ .mpy ファイルをビルドするために必要となるツールは以下の通りです:

  • MicroPython リポジトリ(少なくとも py/tools/ ディレクトリ)。
  • CPython 3 とライブラリ pyelftools (pip install 'pyelftools>=0.25' などでインストール)。
  • GNU make.
  • ターゲットアーキテクチャ用の C コンパイラ(C ソースが使われている場合)。
  • オプションで MicroPython リポジトリでビルドした mpy-cross (.py ソースを使用している場合)。

実行するターゲットに対して適切な ARCH を選択してください。ビルドは次のように行います:

$ make

Makefile を変更しなくても、次のようにしてターゲットのアーキテクチャを指定できます:

$ make ARCH=armv7m

MicroPython でのモジュールの使い方

モジュールをビルドすると factorial.mpy というファイルが作成されます。これをコピーして MicroPython システムのファイルシステムでアクセスできるようにし、インポートパスで見つけられるようにします。これで作成したモジュールを、他のモジュールと同じように Python でアクセスできるようになります。例えば次のように使います:

import factorial
print(factorial.factorial(10))
# 3628800 が表示されるはず

さらなる例

ネイティブ .mpy モジュールで利用可能な機能の多くを示すさらなる例については examples/natmod/ を参照してください。以下の例があります:

  • 複数の C ソースファイルの利用
  • C コード と Python コードを一緒に含める
  • rodata と BSS データ
  • メモリー割り当て
  • 浮動小数点の利用
  • 例外処理
  • 外部 C ライブラリの取り込み