Natív gépi kód .mpy fájlokban

Ez a fejezet azt írja le, hogyan lehet olyan .mpy fájlokat építeni és kezelni, amelyek a Pythontól eltérő nyelven írt natív gépi kódot tartalmaznak. Ez lehetővé teszi, hogy a kódot egy olyan nyelven írd meg, mint a C, lefordítsd és összelinkeld egy .mpy fájllá, majd ezt a fájlt egy normál Python modulként importáld. Ezzel olyan funkciókat valósíthatsz meg, amelyek teljesítmény szempontjából kritikusak, vagy beépíthetsz egy másik nyelven írt, már létező könyvtárat.

A natív .mpy fájlok használatának egyik fő előnye, hogy a natív gépi kódot egy szkript dinamikusan importálhatja, anélkül, hogy újra kellene építeni a fő MicroPython firmware-t. Ez ellentétben áll a Külső C modulok a MicroPython számára megoldással, amely szintén lehetővé teszi egyéni modulok definiálását C nyelven, de azokat bele kell fordítani a fő firmware-képbe.

A hangsúly itt a C nyelv használatán van natív modulok építéséhez, de elvileg bármely nyelv, amely önálló gépi kóddá fordítható, elhelyezhető egy .mpy fájlban.

A natív .mpy modul a mpy_ld.py eszközzel épül, amely a projekt tools/ könyvtárában található. Ez az eszköz egy sor objektumfájlt (.o fájlt) vesz, és összelinkeli őket egy natív .mpy fájl létrehozásához. CPython 3-at és a pyelftools könyvtár v0.25 vagy újabb verzióját igényli.

Támogatott funkciók és korlátozások

Egy .mpy fájl tartalmazhat MicroPython bájtkódot és/vagy natív gépi kódot. Ha natív gépi kódot tartalmaz, akkor a .mpy fájlhoz egy konkrét architektúra tartozik. A jelenleg támogatott architektúrák a következők (ezek az ARCH változó érvényes opciói, lásd alább):

  • x86 (32 bites)

  • x64 (64 bites x86)

  • armv6m (ARM Thumb, pl. Cortex-M0)

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

  • armv7emsp (ARM Thumb 2, egyszeres pontosságú lebegőpontos, pl. Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, dupla pontosságú lebegőpontos, pl. Cortex-M7)

  • xtensa (nem ablakozott, pl. ESP8266)

  • xtensawin (ablakozott, 8-as ablakmérettel, pl. ESP32, ESP32S3)

  • rv32imc (RISC-V 32 bites, tömörített utasításokkal, pl. ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64 bites, tömörített utasításokkal)

Ha a kiválasztott platform támogat explicit architektúra-jelzőket, és azt szeretnéd, hogy a kimeneti .mpy fájl ezeknek a jelzőknek az értékét hordozza, akkor át kell adnod őket az ARCH_FLAGS jelzőváltozónak a .mpy fájl építésekor.

A natív .mpy fájl fordításakor és linkelésekor ki kell választani az architektúrát, és a megfelelő fájl csak azon az architektúrán importálható (és ha architektúra-jelzők vannak jelen, csak akkor, ha azok megfelelnek a célplatform képességeinek). A .mpy fájlokról bővebben lásd: MicroPython .mpy fájlok.

A natív kódot pozíciófüggetlen kódként (PIC) kell lefordítani, és globális eltolási táblát (GOT) kell használnia, bár ennek részletei architektúránként eltérnek. Natív kódot tartalmazó .mpy fájlok importálásakor az importmechanizmus képes a natív kód néhány alapvető áthelyezésére (relocation). Ez magában foglalja a text, rodata és BSS szakaszok áthelyezését.

A linker és a dinamikus betöltő támogatott funkciói:

  • végrehajtható kód (text)

  • csak olvasható adat (rodata), beleértve a sztringeket és a konstans adatokat (tömbök, struktúrák stb.)

  • nullázott adat (BSS)

  • mutatók a textben textre, rodatára és BSS-re

  • mutatók a rodatában textre, rodatára és BSS-re

Az ismert korlátozások a következők:

  • az adatszakaszok (data sections) nem támogatottak; megkerülő megoldás: használj BSS adatot, és inicializáld az adatértékeket explicit módon

  • a statikus BSS változók nem támogatottak; megkerülő megoldás: használj globális BSS változókat

  • a szálhoz kötött tárolási (thread-local storage) változók nem támogatottak az rv32imc architektúrán; megkerülő megoldás: használj globális BSS változókat, vagy foglalj némi helyet a kupacon (heap) a tárolásukhoz

Tehát, ha a C kódod írható adatot tartalmaz, győződj meg róla, hogy az adat globálisan van definiálva, inicializáló nélkül, és csak függvényeken belül kerül írásra.

A natív modul nem linkelődik automatikusan a szabványos statikus könyvtárakhoz, mint a libm.a és a libgcc.a, ami undefined symbol hibákhoz vezethet. A futásidejű könyvtárakat a LINK_RUNTIME = 1 beállításával linkelheted a Makefile-odban. Egyéni statikus könyvtárakat is linkelhetsz az MPY_LD_FLAGS += -l path/to/library.a hozzáadásával. Megjegyzendő, hogy ezek a natív modulba linkelődnek, és nem lesznek megosztva más modulokkal vagy a rendszerrel.

Linker korlátozás: a natív modul nem linkelődik a teljes MicroPython firmware szimbólumtáblájához. Ehelyett az exportált szimbólumoknak a mp_fun_table táblában (a py/nativeglue.h fájlban) található explicit táblájához linkelődik, amely a firmware építésének időpontjában rögzítve van. Így nem lehetséges egyszerűen meghívni egy tetszőleges HAL/OS/RTOS/rendszerfüggvényt, hacsak az nem egy rögzített címen található. Ebben az esetben egy linkerszkript elérési útja, amely szimbólumnevek és rögzített címeik sorozatát tartalmazza, átadható a mpy_ld.py eszköznek a --externs parancssori argumentummal. Így a linkerszkriptben megjelenő szimbólumok elsőbbséget élveznek azzal szemben, amit az objektumfájlok biztosítanak, de jelenleg az objektumfájlok implementációja továbbra is a végső MPY fájlban marad. A linkerszkript-elemző képességei korlátozottak, és jelenleg csak az ESP8266 port ROM szimbólumlistájának elemzésére használatos (lásd ports/esp8266/boards/eagle.rom.addr.v6.ld).

Új szimbólumok adhatók a tábla végéhez, és a firmware újraépíthető. A szimbólumokat hozzá kell adni a tools/mpy_ld.py fun_table szótárához is ugyanazon a helyen. Ez lehetővé teszi, hogy a mpy_ld.py felismerje az új szimbólumokat, és áthelyezéseket biztosítson számukra az mpy importálásakor. Végül, ha a szimbólum egy függvény, egy makrót vagy stubot kell hozzáadni a py/dynruntime.h fájlhoz, hogy a függvény könnyen meghívható legyen.

Natív modul definiálása

A natív .mpy modult egy fájlhalmaz definiálja, amelyeket a .mpy építéséhez használnak. A fájlrendszer-elrendezés két fő részből áll: a forrásfájlokból és a Makefile-ból:

  • A legegyszerűbb esetben csak egyetlen C forrásfájlra van szükség, amely tartalmazza az összes kódot, amely a .mpy modulba fordul. Ennek a C forráskódnak tartalmaznia kell a py/dynruntime.h fájlt a MicroPython dinamikus API eléréséhez, és legalább egy mpy_init nevű függvényt kell definiálnia. Ez a függvény lesz a modul belépési pontja, amely a modul importálásakor hívódik meg.

    A modul kívánság szerint több C forrásfájlra is bontható. A modul egyes részei Pythonban is megvalósíthatók. Minden forrásfájlt fel kell sorolni a Makefile-ban, hozzáadva őket az SRC változóhoz (lásd alább). Ez magában foglalja mind a C forrásfájlokat, mind az esetleges Python fájlokat, amelyek bekerülnek a kapott .mpy fájlba.

  • A Makefile tartalmazza a modul építési konfigurációját, és felsorolja a .mpy modul építéséhez használt forrásfájlokat. Definiálnia kell az MPY_DIR változót a MicroPython tároló helyeként (a fejlécfájlok, a vonatkozó Makefile-töredék és a mpy_ld.py eszköz megtalálásához), a MOD változót a modul neveként, az SRC változót a forrásfájlok listájaként, opcionálisan megadhatja a gépi architektúrát az ARCH változón keresztül, valamint opcionális gépi architektúra-jelzőket az ARCH_FLAGS változón keresztül, majd be kell illesztenie a py/dynruntime.mk fájlt.

Minimális példa

Ez a szakasz egy teljesen működő példát mutat be egy factorial nevű egyszerű modulra. Ez a modul egyetlen factorial.factorial(x) függvényt biztosít, amely kiszámítja a bemenet faktoriálisát, és visszaadja az eredményt.

Könyvtárelrendezés:

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

A factorial.c fájl tartalma:

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

A Makefile fájl tartalma:

# 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

A modul fordítása

A natív .mpy fájl építéséhez szükséges előfeltétel-eszközök:

  • A MicroPython tároló (legalább a py/ és tools/ könyvtárak).

  • CPython 3, és a pyelftools könyvtár (pl. pip install 'pyelftools>=0.25').

  • GNU make.

  • C fordító a célarchitektúrához (ha C forrást használsz).

  • Opcionálisan mpy-cross, a MicroPython tárolóból építve (ha .py forrást használsz).

Ügyelj rá, hogy a megfelelő ARCH értéket válaszd ki ahhoz a célplatformhoz, amelyen futtatni fogod. Majd építsd a következővel:

$ make

A Makefile módosítása nélkül a célarchitektúrát a következővel adhatod meg:

$ make ARCH=armv7m

Ugyanez vonatkozik az opcionális architektúra-jelzőkre a következővel:

$ make ARCH=rv32imc ARCH_FLAGS=zba

A modul használata MicroPythonban

Miután a modul felépült, létre kell jönnie egy factorial.mpy nevű fájlnak. Másold ezt át úgy, hogy elérhető legyen a MicroPython rendszered fájlrendszerén, és megtalálható legyen az importálási útvonalon. A modul mostantól ugyanúgy elérhető Pythonban, mint bármely más modul, például:

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

Picolibc használata modulok építésekor

A Picolibc használata C szabványkönyvtárként nemcsak támogatott, hanem valójában ez az alapértelmezett az rv32imc és rv64imc platformokon. Van azonban néhány említésre méltó dolog, hogy biztosan ne ütközz problémákba később a kód építésekor.

Néhány előre épített Picolibc verzió (például az Ubuntu Linux által biztosítottak a picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf és picolibc-xtensa-lx106-elf csomagokként) azt feltételezi, hogy a szálhoz kötött tárolás (TLS) elérhető futásidőben, de sajnos a MicroPython modulok ezt egyes architektúrákon (nevezetesen az rv32imc és rv64imc architektúrákon) nem támogatják. Ez azt jelenti, hogy a Picolibc által biztosított egyes funkciók alapértelmezetten a TLS-t fogják használni, hibát adva vissza akár a fordítás, akár a linkelés során.

Egy példára, hogy ez hogyan érinthet téged, az examples/natmod/btree példamodul tartalmaz egy megkerülő megoldást annak biztosítására, hogy az errno működjön (keresd a __PICOLIBC_ERRNO_FUNCTION kifejezést a Makefile-ban, és kövesd onnan a nyomot).

További példák

Lásd az examples/natmod/ könyvtárat további példákért, amelyek a natív .mpy modulok számos elérhető funkcióját mutatják be. Ilyen funkciók többek között:

  • több C forrásfájl használata

  • Python kód beépítése a C kód mellé

  • rodata és BSS adat

  • memóriafoglalás

  • lebegőpontos számok használata

  • kivételkezelés

  • külső C könyvtárak beépítése