Nativer Maschinencode in .mpy-Dateien¶
Dieser Abschnitt beschreibt, wie .mpy-Dateien erstellt und verwendet werden, die nativen Maschinencode aus einer anderen Sprache als Python enthalten. Dadurch können Sie Code in einer Sprache wie C schreiben, ihn kompilieren und zu einer .mpy-Datei linken und diese Datei dann wie ein normales Python-Modul importieren. Dies kann verwendet werden, um Funktionalität zu implementieren, die leistungskritisch ist, oder um eine bestehende, in einer anderen Sprache geschriebene Bibliothek einzubinden.
Einer der Hauptvorteile nativer .mpy-Dateien besteht darin, dass nativer Maschinencode von einem Skript dynamisch importiert werden kann, ohne dass die Haupt-MicroPython-Firmware neu erstellt werden muss. Dies steht im Gegensatz zu Externe C-Module für MicroPython, womit ebenfalls benutzerdefinierte Module in C definiert werden können, die jedoch in das Haupt-Firmware-Image kompiliert werden müssen.
Der Schwerpunkt liegt hier auf der Verwendung von C zum Erstellen nativer Module, aber im Prinzip kann jede Sprache, die zu eigenständigem Maschinencode kompiliert werden kann, in eine .mpy-Datei gepackt werden.
Ein natives .mpy-Modul wird mit dem Werkzeug mpy_ld.py erstellt, das sich im Verzeichnis tools/ des Projekts befindet. Dieses Werkzeug nimmt eine Reihe von Objektdateien (.o-Dateien) und linkt sie zusammen, um eine native .mpy-Datei zu erzeugen. Es erfordert CPython 3 und die Bibliothek pyelftools in Version 0.25 oder höher.
Unterstützte Funktionen und Einschränkungen¶
Eine .mpy-Datei kann MicroPython-Bytecode und/oder nativen Maschinencode enthalten. Wenn sie nativen Maschinencode enthält, ist der .mpy-Datei eine bestimmte Architektur zugeordnet. Aktuell unterstützte Architekturen sind (dies sind die gültigen Optionen für die Variable ARCH, siehe unten):
x86(32 Bit)x64(64-Bit-x86)armv6m(ARM Thumb, z. B. Cortex-M0)armv7m(ARM Thumb 2, z. B. Cortex-M3)armv7emsp(ARM Thumb 2, einfache Gleitkommagenauigkeit, z. B. Cortex-M4F, Cortex-M7)armv7emdp(ARM Thumb 2, doppelte Gleitkommagenauigkeit, z. B. Cortex-M7)xtensa(nicht gefenstert, z. B. ESP8266)xtensawin(gefenstert mit Fenstergröße 8, z. B. ESP32, ESP32S3)rv32imc(RISC-V 32 Bit mit komprimierten Befehlen, z. B. ESP32C3, ESP32C6)rv64imc(RISC-V 64 Bit mit komprimierten Befehlen)
Wenn die gewählte Plattform explizite Architektur-Flags unterstützt und Sie möchten, dass die ausgegebene .mpy-Datei den Wert dieser Flags trägt, müssen Sie sie beim Erstellen der .mpy-Datei an die Variable ARCH_FLAGS übergeben.
Beim Kompilieren und Linken der nativen .mpy-Datei muss die Architektur gewählt werden, und die entsprechende Datei kann nur auf dieser Architektur importiert werden (und wenn Architektur-Flags vorhanden sind, nur wenn sie zu den Fähigkeiten des Ziels passen). Weitere Einzelheiten zu .mpy-Dateien finden Sie unter MicroPython .mpy-Dateien.
Nativer Code muss als positionsunabhängiger Code (PIC) kompiliert werden und eine globale Offset-Tabelle (GOT) verwenden, wobei die Details hierbei von Architektur zu Architektur variieren. Beim Importieren von .mpy-Dateien mit nativem Code kann die Import-Maschinerie eine grundlegende Relokation des nativen Codes durchführen. Dies umfasst das Relozieren von text-, rodata- und BSS-Abschnitten.
Unterstützte Funktionen des Linkers und des dynamischen Laders sind:
ausführbarer Code (text)
schreibgeschützte Daten (rodata), einschließlich Strings und konstanter Daten (Arrays, Structs usw.)
auf null gesetzte Daten (BSS)
Zeiger in text auf text, rodata und BSS
Zeiger in rodata auf text, rodata und BSS
Die bekannten Einschränkungen sind:
data-Abschnitte werden nicht unterstützt; Behelfslösung: verwenden Sie BSS-Daten und initialisieren Sie die Datenwerte explizit
statische BSS-Variablen werden nicht unterstützt; Behelfslösung: verwenden Sie globale BSS-Variablen
thread-lokale Speichervariablen werden auf rv32imc nicht unterstützt; Behelfslösung: verwenden Sie globale BSS-Variablen oder reservieren Sie etwas Platz auf dem Heap, um sie zu speichern
Wenn Ihr C-Code also beschreibbare Daten hat, stellen Sie sicher, dass die Daten global, ohne Initialisierer definiert und nur innerhalb von Funktionen beschrieben werden.
Das native Modul wird nicht automatisch gegen die standardmäßigen statischen Bibliotheken wie libm.a und libgcc.a gelinkt, was zu undefined symbol-Fehlern führen kann. Sie können die Laufzeitbibliotheken linken, indem Sie LINK_RUNTIME = 1 in Ihrem Makefile setzen. Benutzerdefinierte statische Bibliotheken können ebenfalls gelinkt werden, indem Sie MPY_LD_FLAGS += -l path/to/library.a hinzufügen. Beachten Sie, dass diese in das native Modul gelinkt werden und nicht mit anderen Modulen oder dem System geteilt werden.
Linker-Einschränkung: Das native Modul wird nicht gegen die Symboltabelle der vollständigen MicroPython-Firmware gelinkt. Stattdessen wird es gegen eine explizite Tabelle exportierter Symbole gelinkt, die sich in mp_fun_table (in py/nativeglue.h) befindet und zur Firmware-Build-Zeit festgelegt ist. Es ist daher nicht möglich, einfach eine beliebige HAL-/OS-/RTOS-/Systemfunktion aufzurufen, es sei denn, diese befindet sich an einer festen Adresse. In diesem Fall kann der Pfad eines Linkerskripts, das eine Reihe von Symbolnamen und ihre festen Adressen enthält, über das Kommandozeilenargument --externs an mpy_ld.py übergeben werden. Auf diese Weise haben Symbole, die im Linkerskript erscheinen, Vorrang vor dem, was von den Objektdateien bereitgestellt wird, aber derzeit verbleibt die Implementierung der Objektdateien dennoch in der endgültigen MPY-Datei. Der Linkerskript-Parser ist in seinen Fähigkeiten begrenzt und wird derzeit nur zum Parsen der ROM-Symbolliste des ESP8266-Ports verwendet (siehe ports/esp8266/boards/eagle.rom.addr.v6.ld).
Neue Symbole können am Ende der Tabelle hinzugefügt und die Firmware neu erstellt werden. Die Symbole müssen außerdem an derselben Stelle zum fun_table-Dict von tools/mpy_ld.py hinzugefügt werden. Dadurch kann mpy_ld.py die neuen Symbole erkennen und Relokationen für sie bereitstellen, wenn die mpy importiert wird. Wenn das Symbol schließlich eine Funktion ist, sollte ein Makro oder ein Stub zu py/dynruntime.h hinzugefügt werden, um den Aufruf der Funktion zu erleichtern.
Definieren eines nativen Moduls¶
Ein natives .mpy-Modul wird durch eine Reihe von Dateien definiert, die zum Erstellen der .mpy verwendet werden. Das Dateisystem-Layout besteht aus zwei Hauptteilen, den Quelldateien und dem Makefile:
Im einfachsten Fall ist nur eine einzige C-Quelldatei erforderlich, die den gesamten Code enthält, der in das .mpy-Modul kompiliert wird. Dieser C-Quellcode muss die Datei
py/dynruntime.heinbinden, um auf die dynamische MicroPython-API zuzugreifen, und muss mindestens eine Funktion namensmpy_initdefinieren. Diese Funktion ist der Einstiegspunkt des Moduls und wird aufgerufen, wenn das Modul importiert wird.Das Modul kann bei Bedarf in mehrere C-Quelldateien aufgeteilt werden. Teile des Moduls können auch in Python implementiert werden. Alle Quelldateien sollten im Makefile aufgeführt werden, indem sie zur Variable
SRChinzugefügt werden (siehe unten). Dies umfasst sowohl C-Quelldateien als auch alle Python-Dateien, die in die resultierende .mpy-Datei aufgenommen werden.Das
Makefileenthält die Build-Konfiguration für das Modul und listet die zum Erstellen des .mpy-Moduls verwendeten Quelldateien auf. Es sollteMPY_DIRals Speicherort des MicroPython-Repositorys definieren (um Header-Dateien, das relevante Makefile-Fragment und das Werkzeugmpy_ld.pyzu finden),MODals Name des Moduls,SRCals Liste der Quelldateien, optional die Maschinenarchitektur überARCHangeben, zusammen mit optionalen Maschinenarchitektur-Flags, die überARCH_FLAGSangegeben werden, und dannpy/dynruntime.mkeinbinden.
Minimalbeispiel¶
Dieser Abschnitt bietet ein voll funktionsfähiges Beispiel eines einfachen Moduls namens factorial. Dieses Modul stellt eine einzelne Funktion factorial.factorial(x) bereit, die die Fakultät der Eingabe berechnet und das Ergebnis zurückgibt.
Verzeichnis-Layout:
factorial/
├── factorial.c
└── Makefile
Die Datei factorial.c enthält:
// 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
}
Die Datei Makefile enthält:
# 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
Kompilieren des Moduls¶
Die zum Erstellen einer nativen .mpy-Datei benötigten Voraussetzungswerkzeuge sind:
Das MicroPython-Repository (mindestens die Verzeichnisse
py/undtools/).CPython 3 und die Bibliothek pyelftools (z. B.
pip install 'pyelftools>=0.25').GNU make.
Ein C-Compiler für die Zielarchitektur (falls C-Quellcode verwendet wird).
Optional
mpy-cross, erstellt aus dem MicroPython-Repository (falls .py-Quellcode verwendet wird).
Stellen Sie sicher, dass Sie die korrekte ARCH für das Ziel auswählen, auf dem Sie das Modul ausführen werden. Erstellen Sie dann mit:
$ make
Ohne das Makefile zu ändern, können Sie die Zielarchitektur angeben über:
$ make ARCH=armv7m
Dasselbe gilt für optionale Architektur-Flags über:
$ make ARCH=rv32imc ARCH_FLAGS=zba
Modulverwendung in MicroPython¶
Sobald das Modul erstellt wurde, sollte es eine Datei namens factorial.mpy geben. Kopieren Sie diese, damit sie auf dem Dateisystem Ihres MicroPython-Systems zugänglich ist und im Importpfad gefunden werden kann. Auf das Modul kann nun in Python wie auf jedes andere Modul zugegriffen werden, zum Beispiel:
import factorial
print(factorial.factorial(10))
# should display 3628800
Verwendung von Picolibc beim Erstellen von Modulen¶
Die Verwendung von Picolibc als Ihre C-Standardbibliothek wird nicht nur unterstützt, sondern ist sogar der Standard für die Plattformen rv32imc und rv64imc. Es gibt jedoch ein paar Dinge, die erwähnenswert sind, um sicherzustellen, dass Sie später beim Erstellen von Code nicht auf Probleme stoßen.
Einige vorgefertigte Picolibc-Versionen (zum Beispiel die von Ubuntu Linux als Pakete picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf und picolibc-xtensa-lx106-elf bereitgestellten) gehen davon aus, dass zur Laufzeit thread-lokaler Speicher (TLS) verfügbar ist, aber leider unterstützen MicroPython-Module dies auf einigen Architekturen nicht (nämlich rv32imc und rv64imc). Das bedeutet, dass einige von Picolibc bereitgestellte Funktionalitäten standardmäßig TLS verwenden und entweder während der Kompilierung oder während des Linkens einen Fehler zurückgeben.
Ein Beispiel dafür, wie sich dies auf Sie auswirken kann: Das Beispielmodul examples/natmod/btree enthält eine Behelfslösung, um sicherzustellen, dass errno funktioniert (suchen Sie nach __PICOLIBC_ERRNO_FUNCTION im Makefile und verfolgen Sie von dort aus die Spur).
Weitere Beispiele¶
Siehe examples/natmod/ für weitere Beispiele, die viele der verfügbaren Funktionen nativer .mpy-Module zeigen. Zu diesen Funktionen gehören:
Verwendung mehrerer C-Quelldateien
Einbinden von Python-Code neben C-Code
rodata- und BSS-Daten
Speicherzuweisung
Verwendung von Gleitkommazahlen
Ausnahmebehandlung
Einbinden externer C-Bibliotheken