ネイティブマシンコードの .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, ESP32S3 など)rv32imc(RISC-V 32 ビット, 圧縮命令あり: ESP32C3, ESP32C6 など)rv64imc(RISC-V 64 ビット, 圧縮命令あり)
選択したプラットフォームが明示的なアーキテクチャフラグをサポートしていて、出力する .mpy ファイルにそれらのフラグの値を適用させたい場合、.mpy ファイルをビルドする際に ARCH_FLAGS フラグ変数へそれらを指定する必要があります。
ネイティブ .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 変数を使ってください
rv32imc ではスレッドローカルストレージ変数をサポートしてません。回避策: グローバル BSS セクションの変数を使うか、ヒープに領域を確保して格納用に使います。
このため、C コードに書き込み可能なデータがある場合、データが初期化子なしでグローバルに定義され、関数内でのみ書き込まれていることを確認してください。
ネイティブモジュールは libm.a や libgcc.a などの標準の静的ライブラリに自動的にはリンクされません。そのため undefined symbol (未定義シンボル)エラーが発生する可能性があります。ランタイムライブラリをリンクするには Makefile 内で LINK_RUNTIME = 1 を設定してください。また、カスタムの静的ライブラリをリンクするには MPY_LD_FLAGS += -l path/to/library.a を追加します。ただし、これらはネイティブモジュールにリンクされるのみで、他のモジュールやシステムと共有されることはありません。
リンカの制限事項:ネイティブモジュールは、MicroPython ファームウェア全体のシンボルテーブルにリンクされているわけではありません。代わりに、ファームウェアのビルド時に固定される mp_fun_table (py/nativeglue.h 内)にある明示的にエクスポートされたシンボルテーブルにリンクされます。そのため、任意のHAL/OS/RTOS/システム関数を単に呼び出すことはできません(例外として、固定アドレスにある場合は可能です)。その場合、シンボル名と固定アドレスの一覧を含むリンカスクリプトのパスを --externs コマンドライン引数を使って mpy_ld.py に渡せます。こうすることで、リンカスクリプトに記載されたシンボルがオブジェクトファイルに提供されたものより優先されますが、現時点ではオブジェクトファイルの実装も最終的なMPYファイルに含まれることになります。なお、リンカスクリプトパーサーは機能に制限があり、現在は主にESP8266ポートのROMシンボルリスト(ports/esp8266/boards/eagle.rom.addr.v6.ld)の解析に使われています
新しいシンボルをテーブルの最後に追加して、ファームウェアを再ビルドできます。シンボルは、同じ場所にある tools/mpy_ld.py の fun_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を使い、追加のアーキテクチャフラグがある場合には ARCH_FLAGS で指定し、さらに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 の内容は以下のとおりです:
# 最上位の MicroPython ディレクトリの場所
MPY_DIR = ../../..
# モジュールの名前
MOD = factorial
# ソースファイル(.c や .py)
SRC = factorial.c
# ビルド対象のアーキテクチャ(x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc)
ARCH = x64
# このインクルードにより、モジュールをコンパイル・リンクするルールを取得
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
オプションのアーキテクチャフラグについても同様に指定できます:
$ make ARCH=rv32imc ARCH_FLAGS=zba
MicroPython でのモジュールの使い方
モジュールをビルドすると factorial.mpy というファイルが作成されます。これをコピーして MicroPython システムのファイルシステムでアクセスできるようにし、インポートパスで見つけられるようにします。これで作成したモジュールを、他のモジュールと同じように Python でアクセスできるようになります。例えば次のように使います:
import factorial
print(factorial.factorial(10))
# 3628800 が表示されるはず
モジュールのビルド時における Picolibc の使用について
C 標準ライブラリとして Picolibc を使うことはサポートされているだけでなく、rv32imc と rv64imc のプラットフォームでは実際にデフォルト設定となっています。しかし、コードをビルドする際に後々問題が発生しないよう、いくつか留意すべき点があります。
Ubuntu Linux が提供する picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf, picolibc-xtensa-lx106-elf といった一部の事前ビルド済み Picolibc バージョンは、実行時にスレッドローカルストレージ(TLS)が利用可能であることを前提としています。しかし残念ながら、MicroPython のモジュールは一部のアーキテクチャ(特に rv32imc と rv64imc)では TLS をサポートしていません。このため、Picolibc が提供する一部の機能はデフォルトで TLS を使おうとすると、コンパイル時やリンク時にエラーが発生することがあります。
これがどのような影響を及ぼすかの例として examples/natmod/btree のサンプルモジュールがあります。このモジュールでは errno を正しく動作させるための回避策が含まれています(Makefile 内の __PICOLIBC_ERRNO_FUNCTION を探し、そこからたどると詳細が分かります)。
さらなる例
ネイティブ .mpy モジュールで利用可能な機能の多くを示すさらなる例については examples/natmod/ を参照してください。以下の例があります:
複数の C ソースファイルの利用
C コード と Python コードを一緒に含める
rodata と BSS データ
メモリー割り当て
浮動小数点の利用
例外処理
外部 C ライブラリの取り込み