.mpy ファイル内のネイティブマシンコード

このセクションでは、Python 以外の言語によるネイティブマシンコードを含む .mpy ファイルのビルド方法と扱い方について説明します。これにより、C のような言語でコードを記述し、コンパイルおよびリンクして .mpy ファイルにまとめ、そのファイルを通常の Python モジュールと同じようにインポートできます。これは、パフォーマンスが重要な機能を実装したり、別の言語で書かれた既存のライブラリを取り込んだりするために利用できます。

ネイティブ .mpy ファイルを使用する主な利点の 1 つは、メインの MicroPython ファームウェアを再ビルドすることなく、スクリプトからネイティブマシンコードを動的にインポートできることです。これは、C でカスタムモジュールを定義できるものの、それらをメインのファームウェアイメージにコンパイルして組み込む必要がある MicroPython の外部 C モジュール とは対照的です。

ここでは C を使用してネイティブモジュールをビルドすることに焦点を当てていますが、原理上は単独で動作するマシンコードにコンパイルできる言語であれば、どのような言語でも .mpy ファイルに格納できます。

ネイティブ .mpy モジュールは、プロジェクトの tools/ ディレクトリにある mpy_ld.py ツールを使用してビルドします。このツールは一連のオブジェクトファイル(.o ファイル)を受け取り、それらをリンクしてネイティブ .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.alibgcc.a のような標準静的ライブラリに対して自動的にはリンクされないため、undefined symbol エラーが発生することがあります。Makefile で LINK_RUNTIME = 1 を設定することで、ランタイムライブラリをリンクできます。また、MPY_LD_FLAGS += -l path/to/library.a を追加することで、カスタムの静的ライブラリをリンクすることもできます。これらはネイティブモジュール内にリンクされ、他のモジュールやシステムとは共有されない点に注意してください。

リンカの制限: ネイティブモジュールは MicroPython ファームウェア全体のシンボルテーブルに対してリンクされるわけではありません。代わりに、mp_fun_tablepy/nativeglue.h 内)にあるエクスポート済みシンボルの明示的なテーブルに対してリンクされ、これはファームウェアのビルド時に固定されます。そのため、たとえば任意の HAL/OS/RTOS/システム関数を単純に呼び出すことはできません。ただし、その関数が固定アドレスに存在する場合は別です。その場合、一連のシンボル名とその固定アドレスを含むリンカスクリプトのパスを、--externs コマンドライン引数を介して mpy_ld.py に渡すことができます。そうすると、リンカスクリプトに現れるシンボルがオブジェクトファイルから提供されるものより優先されますが、現時点ではオブジェクトファイルの実装は最終的な MPY ファイルに残ります。リンカスクリプトのパーサーは機能が限られており、現在は ESP8266 ポートの ROM シンボルリストの解析にのみ使用されています(ports/esp8266/boards/eagle.rom.addr.v6.ld を参照)。

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

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

ネイティブ .mpy モジュールは、.mpy のビルドに使用される一連のファイルによって定義されます。ファイルシステムのレイアウトは、ソースファイルと Makefile という 2 つの主要な部分で構成されます。

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

    必要に応じて、モジュールを複数の C ソースファイルに分割できます。モジュールの一部を Python で実装することもできます。すべてのソースファイルは、SRC 変数に追加することで Makefile に列挙する必要があります(後述を参照)。これには、C ソースファイルだけでなく、結果として生成される .mpy ファイルに含まれる Python ファイルも含まれます。

  • Makefile にはモジュールのビルド構成が含まれ、.mpy モジュールのビルドに使用されるソースファイルが列挙されます。MicroPython リポジトリの場所として MPY_DIR(ヘッダーファイル、関連する Makefile フラグメント、および mpy_ld.py ツールを見つけるため)、モジュール名として MOD、ソースファイルのリストとして SRC を定義し、必要に応じて ARCH によりマシンアーキテクチャを指定し、さらに ARCH_FLAGS により任意のマシンアーキテクチャフラグを指定したうえで、py/dynruntime.mk をインクルードする必要があります。

最小限の例

このセクションでは、factorial という名前の単純なモジュールの完全に動作する例を示します。このモジュールは、入力の階乗を計算して結果を返す factorial.factorial(x) という単一の関数を提供します。

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

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

factorial.c ファイルの内容は次のとおりです。

// Include the header file to get access to the MicroPython API
#include "py/dynruntime.h"

// Helper function to compute factorial
static mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// This is the function which will be called from Python, as factorial(x)
static mp_obj_t factorial(mp_obj_t x_obj) {
    // Extract the integer from the MicroPython input object
    mp_int_t x = mp_obj_get_int(x_obj);
    // Calculate the factorial
    mp_int_t result = factorial_helper(x);
    // Convert the result to a MicroPython integer object and return it
    return mp_obj_new_int(result);
}
// Define a Python reference to the function above
static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// This is the entry point and is called when the module is imported
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    // This must be first, it sets up the globals dict and other things
    MP_DYNRUNTIME_INIT_ENTRY

    // Make the function available in the module's namespace
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // This must be last, it restores the globals dict
    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, rv32imc, rv64imc)
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

任意のアーキテクチャフラグについても、次のように同様に指定できます:

$ make ARCH=rv32imc ARCH_FLAGS=zba

MicroPython でのモジュールの使用

モジュールがビルドされると、factorial.mpy というファイルが生成されているはずです。これを、MicroPython システムのファイルシステム上でアクセス可能でインポートパス内から見つけられる場所にコピーします。これでこのモジュールは、他のモジュールと同じように Python から利用できます。例:

import factorial
print(factorial.factorial(10))
# should display 3628800

モジュールのビルド時に Picolibc を使用する

C 標準ライブラリとして Picolibc を使用することはサポートされているだけでなく、実際 rv32imc および rv64imc プラットフォームではデフォルトとなっています。ただし、後でコードをビルドする際に問題が発生しないようにするために、いくつか触れておくべき点があります。

一部のビルド済み Picolibc バージョン(たとえば Ubuntu Linux が picolibc-arm-none-eabipicolibc-riscv64-unknown-elfpicolibc-xtensa-lx106-elf パッケージとして提供しているもの)は、実行時にスレッドローカルストレージ(TLS)が利用可能であることを前提としていますが、残念ながら MicroPython モジュールは一部のアーキテクチャ(具体的には rv32imcrv64imc)でこれをサポートしていません。これは、Picolibc が提供する一部の機能がデフォルトで TLS を使用しようとし、コンパイル時またはリンク時にエラーを返すことを意味します。

これがどのように影響するかの例として、examples/natmod/btree のサンプルモジュールには errno が機能するようにするための回避策が含まれています(Makefile 内の __PICOLIBC_ERRNO_FUNCTION を探し、そこから手がかりをたどってください)。

さらなる例

ネイティブ .mpy モジュールで利用できる多くの機能を示すさらなる例については、examples/natmod/ を参照してください。そのような機能には次のものが含まれます。

  • 複数の C ソースファイルの使用

  • C コードと並行して Python コードを含める

  • rodata および BSS データ

  • メモリ割り当て

  • 浮動小数点の使用

  • 例外処理

  • 外部 C ライブラリのインクルード