Native machinecode in .mpy-bestanden

Dit gedeelte beschrijft hoe je .mpy-bestanden bouwt en ermee werkt die native machinecode bevatten vanuit een andere taal dan Python. Hiermee kun je code schrijven in een taal als C, deze compileren en linken tot een .mpy-bestand, en dit bestand vervolgens importeren als een normale Python-module. Dit kan worden gebruikt om functionaliteit te implementeren die prestatiekritisch is, of om een bestaande bibliotheek die in een andere taal is geschreven op te nemen.

Een van de belangrijkste voordelen van het gebruik van native .mpy-bestanden is dat native machinecode dynamisch door een script kan worden geïmporteerd, zonder dat de hoofd-firmware van MicroPython opnieuw hoeft te worden gebouwd. Dit in tegenstelling tot Externe C-modules voor MicroPython, waarmee ook aangepaste modules in C kunnen worden gedefinieerd, maar die moeten worden meegecompileerd in de hoofd-firmware-image.

De nadruk ligt hier op het gebruik van C om native modules te bouwen, maar in principe kan elke taal die kan worden gecompileerd tot zelfstandige machinecode in een .mpy-bestand worden geplaatst.

Een native .mpy-module wordt gebouwd met de tool mpy_ld.py, die zich bevindt in de map tools/ van het project. Deze tool neemt een set objectbestanden (.o-bestanden) en linkt ze samen tot een native .mpy-bestand. Het vereist CPython 3 en de bibliotheek pyelftools v0.25 of hoger.

Ondersteunde functies en beperkingen

Een .mpy-bestand kan MicroPython-bytecode en/of native machinecode bevatten. Als het native machinecode bevat, is er een specifieke architectuur aan het .mpy-bestand gekoppeld. De momenteel ondersteunde architecturen zijn (dit zijn de geldige opties voor de variabele ARCH, zie hieronder):

  • x86 (32-bits)

  • x64 (64-bits x86)

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

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

  • armv7emsp (ARM Thumb 2, single-precision float, bv. Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, double-precision float, bv. Cortex-M7)

  • xtensa (non-windowed, bv. ESP8266)

  • xtensawin (windowed met vensterformaat 8, bv. ESP32, ESP32S3)

  • rv32imc (RISC-V 32-bits met compressed instructions, bv. ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64-bits met compressed instructions)

Als het gekozen platform expliciete architectuurvlaggen ondersteunt en je wilt dat het uitvoer-.mpy-bestand de waarde van die vlaggen meeneemt, moet je ze doorgeven aan de variabele ARCH_FLAGS bij het bouwen van het .mpy-bestand.

Bij het compileren en linken van het native .mpy-bestand moet de architectuur worden gekozen, en het bijbehorende bestand kan alleen op die architectuur worden geïmporteerd (en als er architectuurvlaggen aanwezig zijn, alleen als ze overeenkomen met de mogelijkheden van het doelplatform). Voor meer details over .mpy-bestanden, zie MicroPython .mpy-bestanden.

Native code moet worden gecompileerd als position independent code (PIC) en een global offset table (GOT) gebruiken, hoewel de details hiervan per architectuur verschillen. Bij het importeren van .mpy-bestanden met native code kan het importmechanisme enige basisrelocatie van de native code uitvoeren. Dit omvat het verplaatsen van de text-, rodata- en BSS-secties.

Ondersteunde functies van de linker en de dynamische loader zijn:

  • uitvoerbare code (text)

  • alleen-lezen data (rodata), inclusief strings en constante data (arrays, structs, enz.)

  • genulde data (BSS)

  • pointers in text naar text, rodata en BSS

  • pointers in rodata naar text, rodata en BSS

De bekende beperkingen zijn:

  • datasecties worden niet ondersteund; oplossing: gebruik BSS-data en initialiseer de datawaarden expliciet

  • statische BSS-variabelen worden niet ondersteund; oplossing: gebruik globale BSS-variabelen

  • thread-local storage-variabelen worden niet ondersteund op rv32imc; oplossing: gebruik globale BSS-variabelen of reserveer wat ruimte op de heap om ze op te slaan

Dus als je C-code beschrijfbare data bevat, zorg er dan voor dat de data globaal wordt gedefinieerd, zonder initialisator, en alleen binnen functies wordt beschreven.

De native module wordt niet automatisch gelinkt aan de standaard statische bibliotheken zoals libm.a en libgcc.a, wat kan leiden tot undefined symbol-fouten. Je kunt de runtimebibliotheken linken door LINK_RUNTIME = 1 in te stellen in je Makefile. Aangepaste statische bibliotheken kunnen ook worden gelinkt door MPY_LD_FLAGS += -l path/to/library.a toe te voegen. Merk op dat deze in de native module worden gelinkt en niet worden gedeeld met andere modules of het systeem.

Linkerbeperking: de native module wordt niet gelinkt aan de symbooltabel van de volledige MicroPython-firmware. In plaats daarvan wordt deze gelinkt aan een expliciete tabel van geëxporteerde symbolen in mp_fun_table (in py/nativeglue.h), die op het moment van het bouwen van de firmware wordt vastgelegd. Het is dus niet mogelijk om zomaar een willekeurige HAL/OS/RTOS/systeemfunctie aan te roepen, tenzij die zich op een vast adres bevindt. In dat geval kan het pad van een linkerscript dat een reeks symboolnamen en hun vaste adres bevat, worden doorgegeven aan mpy_ld.py via het --externs-commandoregelargument. Op die manier krijgen symbolen die in het linkerscript voorkomen voorrang boven wat door objectbestanden wordt geleverd, maar momenteel zal de implementatie uit de objectbestanden nog steeds in het uiteindelijke MPY-bestand staan. De linkerscript-parser is beperkt in zijn mogelijkheden en wordt momenteel alleen gebruikt voor het parsen van de ROM-symbolenlijst van de ESP8266-port (zie ports/esp8266/boards/eagle.rom.addr.v6.ld).

Nieuwe symbolen kunnen aan het einde van de tabel worden toegevoegd en de firmware kan opnieuw worden gebouwd. De symbolen moeten ook worden toegevoegd aan de fun_table-dict van tools/mpy_ld.py op dezelfde locatie. Hierdoor kan mpy_ld.py de nieuwe symbolen oppikken en relocaties ervoor leveren wanneer de mpy wordt geïmporteerd. Tot slot moet, als het symbool een functie is, een macro of stub worden toegevoegd aan py/dynruntime.h om de functie eenvoudig te kunnen aanroepen.

Een native module definiëren

Een native .mpy-module wordt gedefinieerd door een set bestanden die worden gebruikt om de .mpy te bouwen. De bestandssysteemindeling bestaat uit twee hoofdonderdelen: de bronbestanden en de Makefile:

  • In het eenvoudigste geval is slechts één C-bronbestand vereist, dat alle code bevat die in de .mpy-module zal worden gecompileerd. Deze C-broncode moet het bestand py/dynruntime.h includen om toegang te krijgen tot de dynamische API van MicroPython, en moet minstens een functie genaamd mpy_init definiëren. Deze functie is het toegangspunt van de module en wordt aangeroepen wanneer de module wordt geïmporteerd.

    De module kan desgewenst in meerdere C-bronbestanden worden opgesplitst. Delen van de module kunnen ook in Python worden geïmplementeerd. Alle bronbestanden moeten in de Makefile worden vermeld door ze toe te voegen aan de variabele SRC (zie hieronder). Dit omvat zowel C-bronbestanden als alle Python-bestanden die in het resulterende .mpy-bestand worden opgenomen.

  • De Makefile bevat de bouwconfiguratie voor de module en somt de bronbestanden op die worden gebruikt om de .mpy-module te bouwen. Hij moet MPY_DIR definiëren als de locatie van de MicroPython-repository (om headerbestanden, het relevante Makefile-fragment en de tool mpy_ld.py te vinden), MOD als de naam van de module, SRC als de lijst met bronbestanden, optioneel de machine-architectuur opgeven via ARCH, samen met optionele machine-architectuurvlaggen opgegeven via ARCH_FLAGS, en vervolgens py/dynruntime.mk includen.

Minimaal voorbeeld

Dit gedeelte bevat een volledig werkend voorbeeld van een eenvoudige module genaamd factorial. Deze module biedt een enkele functie factorial.factorial(x) die de faculteit van de invoer berekent en het resultaat retourneert.

Mapindeling:

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

Het bestand factorial.c bevat:

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

Het bestand Makefile bevat:

# 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

De module compileren

De vereiste tools om een native .mpy-bestand te bouwen zijn:

  • De MicroPython-repository (ten minste de mappen py/ en tools/).

  • CPython 3 en de bibliotheek pyelftools (bv. pip install 'pyelftools>=0.25').

  • GNU make.

  • Een C-compiler voor de doelarchitectuur (als C-broncode wordt gebruikt).

  • Optioneel mpy-cross, gebouwd vanuit de MicroPython-repository (als .py-broncode wordt gebruikt).

Zorg ervoor dat je de juiste ARCH selecteert voor het doel waarop je gaat draaien. Bouw vervolgens met:

$ make

Zonder de Makefile te wijzigen kun je de doelarchitectuur opgeven via:

$ make ARCH=armv7m

Hetzelfde geldt voor optionele architectuurvlaggen via:

$ make ARCH=rv32imc ARCH_FLAGS=zba

Modulegebruik in MicroPython

Zodra de module is gebouwd, zou er een bestand genaamd factorial.mpy moeten zijn. Kopieer dit zodat het toegankelijk is op het bestandssysteem van je MicroPython-systeem en kan worden gevonden in het importpad. De module kan nu in Python worden benaderd net als elke andere module, bijvoorbeeld:

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

Picolibc gebruiken bij het bouwen van modules

Het gebruik van Picolibc als je C-standaardbibliotheek wordt niet alleen ondersteund, maar is zelfs de standaard voor de rv32imc- en rv64imc-platforms. Er zijn echter een paar dingen die het vermelden waard zijn om er zeker van te zijn dat je later geen problemen tegenkomt bij het bouwen van code.

Sommige vooraf gebouwde Picolibc-versies (bijvoorbeeld die welke door Ubuntu Linux worden geleverd als de pakketten picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf en picolibc-xtensa-lx106-elf) gaan ervan uit dat thread-local storage (TLS) tijdens runtime beschikbaar is, maar helaas ondersteunen MicroPython-modules dat op sommige architecturen niet (namelijk rv32imc en rv64imc). Dit betekent dat sommige functionaliteiten van Picolibc standaard TLS gebruiken, wat een fout retourneert ofwel tijdens het compileren ofwel tijdens het linken.

Als voorbeeld van hoe dit je kan beïnvloeden: de voorbeeldmodule examples/natmod/btree bevat een oplossing om ervoor te zorgen dat errno werkt (zoek naar __PICOLIBC_ERRNO_FUNCTION in de Makefile en volg het spoor van daaruit).

Verdere voorbeelden

Zie examples/natmod/ voor verdere voorbeelden die veel van de beschikbare functies van native .mpy-modules tonen. Tot deze functies behoren:

  • het gebruik van meerdere C-bronbestanden

  • het opnemen van Python-code naast C-code

  • rodata- en BSS-data

  • geheugentoewijzing

  • gebruik van floating point

  • afhandeling van uitzonderingen

  • het opnemen van externe C-bibliotheken