Nativní strojový kód v souborech .mpy

Tato část popisuje, jak sestavit a pracovat se soubory .mpy, které obsahují nativní strojový kód z jiného jazyka než Python. To vám umožňuje psát kód v jazyce jako C, zkompilovat jej a slinkovat do souboru .mpy a poté tento soubor importovat jako běžný Python modul. To lze využít pro implementaci funkcionality kritické z hlediska výkonu nebo pro zahrnutí existující knihovny napsané v jiném jazyce.

Jednou z hlavních výhod používání nativních souborů .mpy je, že nativní strojový kód může být importován skriptem dynamicky, bez nutnosti znovu sestavovat hlavní firmware MicroPython. To je v kontrastu s Externí C moduly pro MicroPython, které rovněž umožňují definovat vlastní moduly v jazyce C, ty však musí být zkompilovány do hlavního obrazu firmware.

Důraz je zde kladen na použití jazyka C pro sestavení nativních modulů, ale v principu lze do souboru .mpy vložit jakýkoli jazyk, který lze zkompilovat do samostatného strojového kódu.

Nativní modul .mpy se sestavuje pomocí nástroje mpy_ld.py, který se nachází v adresáři tools/ projektu. Tento nástroj vezme sadu objektových souborů (.o soubory) a slinkuje je dohromady, aby vytvořil nativní soubor .mpy. Vyžaduje CPython 3 a knihovnu pyelftools verze 0.25 nebo vyšší.

Podporované funkce a omezení

Soubor .mpy může obsahovat bytecode MicroPythonu a/nebo nativní strojový kód. Pokud obsahuje nativní strojový kód, pak má soubor .mpy přiřazenu konkrétní architekturu. Aktuálně podporované architektury jsou (toto jsou platné hodnoty pro proměnnou ARCH, viz níže):

  • x86 (32 bitů)

  • x64 (64bitové x86)

  • armv6m (ARM Thumb, např. Cortex-M0)

  • armv7m (ARM Thumb 2, např. Cortex-M3)

  • armv7emsp (ARM Thumb 2, plovoucí čárka s jednoduchou přesností, např. Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, plovoucí čárka s dvojitou přesností, např. Cortex-M7)

  • xtensa (bez oken, např. ESP8266)

  • xtensawin (s okny o velikosti 8, např. ESP32, ESP32S3)

  • rv32imc (RISC-V 32 bitů s komprimovanými instrukcemi, např. ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64 bitů s komprimovanými instrukcemi)

Pokud zvolená platforma podporuje explicitní příznaky architektury a chcete, aby výstupní soubor .mpy nesl hodnotu těchto příznaků, musíte je při sestavování souboru .mpy předat proměnné ARCH_FLAGS.

Při kompilaci a linkování nativního souboru .mpy je nutné zvolit architekturu a odpovídající soubor lze importovat pouze na této architektuře (a pokud jsou přítomny příznaky architektury, pouze pokud odpovídají schopnostem cíle). Více podrobností o souborech .mpy najdete v Soubory .mpy v MicroPythonu.

Nativní kód musí být zkompilován jako pozičně nezávislý kód (PIC) a používat globální offsetovou tabulku (GOT), ačkoli detaily se liší architekturu od architektury. Při importu souborů .mpy s nativním kódem dokáže importovací mechanismus provést základní relokaci nativního kódu. To zahrnuje relokaci sekcí text, rodata a BSS.

Podporované funkce linkeru a dynamického zavaděče jsou:

  • spustitelný kód (text)

  • data určená pouze pro čtení (rodata), včetně řetězců a konstantních dat (pole, struktury atd.)

  • vynulovaná data (BSS)

  • ukazatele v text na text, rodata a BSS

  • ukazatele v rodata na text, rodata a BSS

Známá omezení jsou:

  • datové sekce nejsou podporovány; řešení: použijte data BSS a inicializujte datové hodnoty explicitně

  • statické proměnné BSS nejsou podporovány; řešení: použijte globální proměnné BSS

  • proměnné v thread-local úložišti nejsou na rv32imc podporovány; řešení: použijte globální proměnné BSS nebo alokujte nějaké místo na haldě pro jejich uložení

Pokud tedy váš kód v C obsahuje zapisovatelná data, ujistěte se, že jsou definována globálně, bez inicializátoru, a že se do nich zapisuje pouze uvnitř funkcí.

Nativní modul není automaticky linkován proti standardním statickým knihovnám jako libm.a a libgcc.a, což může vést k chybám undefined symbol. Runtime knihovny můžete slinkovat nastavením LINK_RUNTIME = 1 ve vašem Makefile. Vlastní statické knihovny lze rovněž slinkovat přidáním MPY_LD_FLAGS += -l path/to/library.a. Mějte na paměti, že tyto jsou slinkovány do nativního modulu a nebudou sdíleny s ostatními moduly ani se systémem.

Omezení linkeru: nativní modul není linkován proti tabulce symbolů celého firmware MicroPython. Místo toho je linkován proti explicitní tabulce exportovaných symbolů nacházející se v mp_fun_table (v py/nativeglue.h), která je pevně dána v době sestavování firmware. Není tedy možné jednoduše volat nějakou libovolnou funkci HAL/OS/RTOS/systému, pokud se tato nenachází na pevné adrese. V takovém případě lze nástroji mpy_ld.py předat cestu k linkerskriptu obsahujícímu sérii názvů symbolů a jejich pevných adres pomocí argumentu příkazové řádky --externs. Tímto způsobem budou mít symboly objevující se v linkerskriptu přednost před tím, co poskytují objektové soubory, ale v současnosti bude implementace z objektových souborů stále umístěna ve výsledném souboru MPY. Parser linkerskriptu je ve svých schopnostech omezený a v současnosti se používá pouze pro parsování seznamu ROM symbolů portu ESP8266 (viz ports/esp8266/boards/eagle.rom.addr.v6.ld).

Nové symboly lze přidat na konec tabulky a firmware znovu sestavit. Symboly je rovněž třeba přidat do slovníku fun_table v tools/mpy_ld.py na stejném místě. To umožňuje nástroji mpy_ld.py zachytit nové symboly a poskytnout pro ně relokace při importu mpy. Nakonec, pokud je symbolem funkce, mělo by se do py/dynruntime.h přidat makro nebo stub, aby bylo volání funkce snadné.

Definování nativního modulu

Nativní modul .mpy je definován sadou souborů, které se používají k sestavení .mpy. Rozložení souborového systému se skládá ze dvou hlavních částí, zdrojových souborů a Makefile:

  • V nejjednodušším případě je vyžadován pouze jeden zdrojový soubor v C, který obsahuje veškerý kód, jenž bude zkompilován do modulu .mpy. Tento zdrojový kód v C musí zahrnovat soubor py/dynruntime.h pro přístup k dynamickému API MicroPythonu a musí definovat alespoň funkci s názvem mpy_init. Tato funkce bude vstupním bodem modulu, volaným při importu modulu.

    Modul lze v případě potřeby rozdělit do více zdrojových souborů v C. Části modulu lze rovněž implementovat v Pythonu. Všechny zdrojové soubory by měly být uvedeny v Makefile jejich přidáním do proměnné SRC (viz níže). To zahrnuje jak zdrojové soubory v C, tak veškeré soubory Python, které budou zahrnuty do výsledného souboru .mpy.

  • Soubor Makefile obsahuje konfiguraci sestavení modulu a uvádí zdrojové soubory použité k sestavení modulu .mpy. Měl by definovat MPY_DIR jako umístění repozitáře MicroPython (pro nalezení hlavičkových souborů, příslušného fragmentu Makefile a nástroje mpy_ld.py), MOD jako název modulu, SRC jako seznam zdrojových souborů, volitelně specifikovat strojovou architekturu pomocí ARCH spolu s volitelnými příznaky strojové architektury specifikovanými pomocí ARCH_FLAGS, a poté zahrnout py/dynruntime.mk.

Minimální příklad

Tato část poskytuje plně funkční příklad jednoduchého modulu s názvem factorial. Tento modul poskytuje jedinou funkci factorial.factorial(x), která vypočítá faktoriál vstupu a vrátí výsledek.

Rozložení adresáře:

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

Soubor factorial.c obsahuje:

// 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
}

Soubor Makefile obsahuje:

# 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

Kompilace modulu

Nezbytné nástroje potřebné k sestavení nativního souboru .mpy jsou:

  • Repozitář MicroPython (alespoň adresáře py/ a tools/).

  • CPython 3 a knihovna pyelftools (např. pip install 'pyelftools>=0.25').

  • GNU make.

  • Kompilátor jazyka C pro cílovou architekturu (pokud je použit zdrojový kód v C).

  • Volitelně mpy-cross sestavený z repozitáře MicroPython (pokud je použit zdrojový kód .py).

Ujistěte se, že jste zvolili správnou hodnotu ARCH pro cíl, na kterém budete program spouštět. Poté sestavte pomocí:

$ make

Bez úpravy Makefile můžete cílovou architekturu specifikovat pomocí:

$ make ARCH=armv7m

Totéž platí pro volitelné příznaky architektury pomocí:

$ make ARCH=rv32imc ARCH_FLAGS=zba

Použití modulu v MicroPythonu

Jakmile je modul sestaven, měl by existovat soubor s názvem factorial.mpy. Zkopírujte jej tak, aby byl přístupný v souborovém systému vašeho systému MicroPython a aby jej bylo možné nalézt v cestě pro import. Modul je nyní přístupný v Pythonu stejně jako jakýkoli jiný modul, například:

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

Použití Picolibc při sestavování modulů

Použití Picolibc jako vaší standardní knihovny C je nejen podporováno, ale ve skutečnosti je výchozí volbou pro platformy rv32imc a rv64imc. Existuje však několik věcí, které stojí za zmínku, abyste se ujistili, že později při sestavování kódu nenarazíte na problémy.

Některé předsestavené verze Picolibc (například ty, které poskytuje Ubuntu Linux jako balíčky picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf a picolibc-xtensa-lx106-elf) předpokládají, že je za běhu k dispozici thread-local úložiště (TLS), to však moduly MicroPythonu na některých architekturách bohužel nepodporují (jmenovitě rv32imc a rv64imc). To znamená, že některé funkcionality poskytované Picolibc budou ve výchozím nastavení používat TLS, což vrátí chybu buď během kompilace, nebo během linkování.

Příklad toho, jak se vás to může týkat, najdete v příkladovém modulu examples/natmod/btree, který obsahuje řešení zajišťující, že errno funguje (vyhledejte __PICOLIBC_ERRNO_FUNCTION v Makefile a sledujte stopu odtud).

Další příklady

Další příklady, které ukazují mnoho z dostupných funkcí nativních modulů .mpy, najdete v examples/natmod/. Mezi takové funkce patří:

  • použití více zdrojových souborů v C

  • zahrnutí kódu Python vedle kódu C

  • data rodata a BSS

  • alokace paměti

  • použití plovoucí čárky

  • zpracování výjimek

  • zahrnutí externích knihoven v C