Cod mașină nativ în fișiere .mpy¶
Această secțiune descrie modul de construire și de lucru cu fișiere .mpy care conțin cod mașină nativ provenit dintr-un alt limbaj decât Python. Aceasta vă permite să scrieți cod într-un limbaj precum C, să îl compilați și să îl legați într-un fișier .mpy, iar apoi să importați acest fișier ca pe un modul Python obișnuit. Acest lucru poate fi folosit pentru a implementa funcționalități critice pentru performanță sau pentru a include o bibliotecă existentă scrisă într-un alt limbaj.
Unul dintre principalele avantaje ale utilizării fișierelor .mpy native este faptul că un cod mașină nativ poate fi importat dinamic de un script, fără a fi nevoie să reconstruiți firmware-ul principal MicroPython. Aceasta este în contrast cu Module C externe pentru MicroPython, care permite de asemenea definirea unor module personalizate în C, dar care trebuie compilate în imaginea firmware-ului principal.
Accentul se pune aici pe utilizarea limbajului C pentru a construi module native, dar în principiu orice limbaj care poate fi compilat într-un cod mașină autonom poate fi inclus într-un fișier .mpy.
Un modul .mpy nativ se construiește folosind instrumentul mpy_ld.py, care se găsește în directorul tools/ al proiectului. Acest instrument preia un set de fișiere obiect (fișiere .o) și le leagă împreună pentru a crea un fișier .mpy nativ. Necesită CPython 3 și biblioteca pyelftools v0.25 sau mai recentă.
Caracteristici acceptate și limitări¶
Un fișier .mpy poate conține bytecode MicroPython și/sau cod mașină nativ. Dacă acesta conține cod mașină nativ, atunci fișierul .mpy are o arhitectură specifică asociată. Arhitecturile acceptate în prezent sunt (acestea sunt opțiunile valide pentru variabila ARCH, vezi mai jos):
x86(32 de biți)x64(x86 pe 64 de biți)armv6m(ARM Thumb, de ex. Cortex-M0)armv7m(ARM Thumb 2, de ex. Cortex-M3)armv7emsp(ARM Thumb 2, virgulă mobilă în simplă precizie, de ex. Cortex-M4F, Cortex-M7)armv7emdp(ARM Thumb 2, virgulă mobilă în dublă precizie, de ex. Cortex-M7)xtensa(non-windowed, de ex. ESP8266)xtensawin(windowed cu dimensiunea ferestrei 8, de ex. ESP32, ESP32S3)rv32imc(RISC-V pe 32 de biți cu instrucțiuni comprimate, de ex. ESP32C3, ESP32C6)rv64imc(RISC-V pe 64 de biți cu instrucțiuni comprimate)
Dacă platforma aleasă acceptă indicatori de arhitectură expliciți și doriți ca fișierul .mpy rezultat să poarte valoarea acelor indicatori, trebuie să îi transmiteți variabilei de indicatori ARCH_FLAGS la construirea fișierului .mpy.
La compilarea și legarea fișierului .mpy nativ trebuie aleasă arhitectura, iar fișierul corespunzător poate fi importat doar pe acea arhitectură (și, dacă sunt prezenți indicatori de arhitectură, doar dacă aceștia corespund capabilităților țintei). Pentru mai multe detalii despre fișierele .mpy, vezi Fișiere .mpy MicroPython.
Codul nativ trebuie compilat ca un cod independent de poziție (PIC) și trebuie să folosească un tabel global de offset-uri (GOT), deși detaliile acestui aspect variază de la o arhitectură la alta. La importul fișierelor .mpy cu cod nativ, mecanismul de import poate efectua o relocare de bază a codului nativ. Aceasta include relocarea secțiunilor text, rodata și BSS.
Caracteristicile acceptate de linker și de încărcătorul dinamic sunt:
cod executabil (text)
date numai pentru citire (rodata), inclusiv șiruri de caractere și date constante (vectori, structuri etc.)
date inițializate cu zero (BSS)
pointeri în text către text, rodata și BSS
pointeri în rodata către text, rodata și BSS
Limitările cunoscute sunt:
secțiunile de date nu sunt acceptate; soluție alternativă: folosiți date BSS și inițializați valorile datelor în mod explicit
variabilele BSS statice nu sunt acceptate; soluție alternativă: folosiți variabile BSS globale
variabilele de stocare locale firului de execuție (thread-local) nu sunt acceptate pe rv32imc; soluție alternativă: folosiți variabile BSS globale sau alocați spațiu pe heap pentru a le stoca
Așadar, dacă codul vostru C are date care pot fi scrise, asigurați-vă că datele sunt definite global, fără inițializator, și că sunt scrise doar în interiorul funcțiilor.
Modulul nativ nu este legat automat de bibliotecile statice standard precum libm.a și libgcc.a, ceea ce poate duce la erori de tip undefined symbol. Puteți lega bibliotecile de execuție setând LINK_RUNTIME = 1 în Makefile-ul vostru. Pot fi legate și biblioteci statice personalizate prin adăugarea MPY_LD_FLAGS += -l path/to/library.a. Rețineți că acestea sunt legate în modulul nativ și nu vor fi partajate cu alte module sau cu sistemul.
Limitarea linkerului: modulul nativ nu este legat de tabelul de simboluri al firmware-ului MicroPython complet. În schimb, este legat de un tabel explicit de simboluri exportate care se găsește în mp_fun_table (în py/nativeglue.h), tabel fixat la momentul construirii firmware-ului. Prin urmare, nu este posibil să apelați pur și simplu o funcție arbitrară HAL/OS/RTOS/de sistem, de exemplu, decât dacă aceasta se află la o adresă fixă. În acest caz, calea unui linkerscript care conține o serie de nume de simboluri și adresele lor fixe poate fi transmisă lui mpy_ld.py prin argumentul de linie de comandă --externs. Astfel, simbolurile care apar în linkerscript vor avea prioritate față de cele furnizate de fișierele obiect, însă deocamdată implementarea din fișierele obiect va rămâne totuși în fișierul MPY final. Parserul de linkerscript are capabilități limitate și este folosit în prezent doar pentru analizarea listei de simboluri din ROM-ul portului ESP8266 (vezi ports/esp8266/boards/eagle.rom.addr.v6.ld).
Pot fi adăugate simboluri noi la sfârșitul tabelului, iar firmware-ul poate fi reconstruit. Simbolurile trebuie de asemenea adăugate în dicționarul fun_table al tools/mpy_ld.py, în aceeași locație. Aceasta îi permite lui mpy_ld.py să recunoască noile simboluri și să furnizeze relocări pentru ele la importul fișierului mpy. În fine, dacă simbolul este o funcție, ar trebui adăugată o macro sau un stub în py/dynruntime.h pentru a facilita apelarea funcției.
Definirea unui modul nativ¶
Un modul .mpy nativ este definit de un set de fișiere folosite pentru a construi fișierul .mpy. Structura sistemului de fișiere constă din două părți principale, fișierele sursă și Makefile-ul:
În cel mai simplu caz este necesar un singur fișier sursă C, care conține tot codul ce va fi compilat în modulul .mpy. Acest cod sursă C trebuie să includă fișierul
py/dynruntime.hpentru a accesa API-ul dinamic MicroPython și trebuie să definească cel puțin o funcție numitămpy_init. Această funcție va fi punctul de intrare al modulului, apelată la importul modulului.Modulul poate fi împărțit în mai multe fișiere sursă C, dacă se dorește. Părți ale modulului pot fi de asemenea implementate în Python. Toate fișierele sursă ar trebui listate în Makefile, prin adăugarea lor la variabila
SRC(vezi mai jos). Aceasta include atât fișierele sursă C, cât și orice fișiere Python care vor fi incluse în fișierul .mpy rezultat.Fișierul
Makefileconține configurația de construire a modulului și listează fișierele sursă folosite pentru a construi modulul .mpy. Acesta ar trebui să defineascăMPY_DIRca locație a depozitului MicroPython (pentru a găsi fișierele de antet, fragmentul Makefile relevant și instrumentulmpy_ld.py),MODca nume al modulului,SRCca listă a fișierelor sursă, opțional să specifice arhitectura mașinii prinARCH, împreună cu indicatori opționali de arhitectură specificați prinARCH_FLAGS, iar apoi să includăpy/dynruntime.mk.
Exemplu minimal¶
Această secțiune oferă un exemplu pe deplin funcțional al unui modul simplu numit factorial. Acest modul oferă o singură funcție factorial.factorial(x) care calculează factorialul intrării și returnează rezultatul.
Structura directoarelor:
factorial/
├── factorial.c
└── Makefile
Fișierul factorial.c conține:
// 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
}
Fișierul Makefile conține:
# 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
Compilarea modulului¶
Instrumentele necesare pentru a construi un fișier .mpy nativ sunt:
Depozitul MicroPython (cel puțin directoarele
py/șitools/).CPython 3 și biblioteca pyelftools (de ex.
pip install 'pyelftools>=0.25').GNU make.
Un compilator C pentru arhitectura țintă (dacă se folosește sursă C).
Opțional
mpy-cross, construit din depozitul MicroPython (dacă se folosește sursă .py).
Asigurați-vă că selectați ARCH-ul corect pentru ținta pe care veți rula. Apoi construiți cu:
$ make
Fără a modifica Makefile-ul, puteți specifica arhitectura țintă prin:
$ make ARCH=armv7m
Același lucru se aplică pentru indicatorii opționali de arhitectură prin:
$ make ARCH=rv32imc ARCH_FLAGS=zba
Utilizarea modulului în MicroPython¶
Odată ce modulul este construit, ar trebui să existe un fișier numit factorial.mpy. Copiați-l astfel încât să fie accesibil în sistemul de fișiere al sistemului vostru MicroPython și să poată fi găsit în calea de import. Modulul poate fi acum accesat în Python la fel ca orice alt modul, de exemplu:
import factorial
print(factorial.factorial(10))
# should display 3628800
Utilizarea Picolibc la construirea modulelor¶
Utilizarea Picolibc ca bibliotecă standard C nu este doar acceptată, ci este de fapt opțiunea implicită pentru platformele rv32imc și rv64imc. Cu toate acestea, există câteva aspecte care merită menționate pentru a vă asigura că nu întâmpinați probleme mai târziu la construirea codului.
Unele versiuni precompilate de Picolibc (de exemplu, cele furnizate de Ubuntu Linux ca pachetele picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf și picolibc-xtensa-lx106-elf) presupun că stocarea locală firului de execuție (TLS) este disponibilă în timpul execuției, dar din păcate modulele MicroPython nu acceptă acest lucru pe unele arhitecturi (anume rv32imc și rv64imc). Aceasta înseamnă că unele funcționalități furnizate de Picolibc vor folosi în mod implicit TLS, returnând o eroare fie în timpul compilării, fie în timpul legării.
Pentru un exemplu al modului în care acest lucru vă poate afecta, modulul de exemplu examples/natmod/btree conține o soluție alternativă pentru a se asigura că errno funcționează (căutați __PICOLIBC_ERRNO_FUNCTION în Makefile și urmați firul de acolo).
Exemple suplimentare¶
Vezi examples/natmod/ pentru exemple suplimentare care prezintă multe dintre caracteristicile disponibile ale modulelor .mpy native. Astfel de caracteristici includ:
utilizarea mai multor fișiere sursă C
includerea de cod Python alături de cod C
date rodata și BSS
alocarea memoriei
utilizarea virgulei mobile
tratarea excepțiilor
includerea de biblioteci C externe