Code machine natif dans les fichiers .mpy

Cette section décrit comment construire et manipuler des fichiers .mpy contenant du code machine natif issu d’un langage autre que Python. Cela vous permet d’écrire du code dans un langage comme le C, de le compiler et de le lier dans un fichier .mpy, puis d’importer ce fichier comme un module Python ordinaire. Cette technique peut servir à implémenter des fonctionnalités critiques pour les performances, ou à intégrer une bibliothèque existante écrite dans un autre langage.

L’un des principaux avantages des fichiers .mpy natifs est que le code machine natif peut être importé dynamiquement par un script, sans avoir à reconstruire le micrologiciel MicroPython principal. Cela contraste avec les Modules C externes pour MicroPython qui permettent aussi de définir des modules personnalisés en C, mais qui doivent être compilés dans l’image du micrologiciel principal.

L’accent est mis ici sur l’utilisation du C pour construire des modules natifs, mais en principe tout langage pouvant être compilé en code machine autonome peut être placé dans un fichier .mpy.

Un module .mpy natif se construit à l’aide de l’outil mpy_ld.py, qui se trouve dans le répertoire tools/ du projet. Cet outil prend un ensemble de fichiers objets (fichiers .o) et les lie ensemble pour créer un fichier .mpy natif. Il nécessite CPython 3 et la bibliothèque pyelftools v0.25 ou supérieure.

Fonctionnalités prises en charge et limitations

Un fichier .mpy peut contenir du bytecode MicroPython et/ou du code machine natif. S’il contient du code machine natif, alors le fichier .mpy est associé à une architecture spécifique. Les architectures actuellement prises en charge sont les suivantes (ce sont les valeurs valides pour la variable ARCH, voir ci-dessous) :

  • x86 (32 bits)

  • x64 (x86 64 bits)

  • armv6m (ARM Thumb, par exemple Cortex-M0)

  • armv7m (ARM Thumb 2, par exemple Cortex-M3)

  • armv7emsp (ARM Thumb 2, flottant simple précision, par exemple Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, flottant double précision, par exemple Cortex-M7)

  • xtensa (non fenêtré, par exemple ESP8266)

  • xtensawin (fenêtré avec une taille de fenêtre de 8, par exemple ESP32, ESP32S3)

  • rv32imc (RISC-V 32 bits avec instructions compressées, par exemple ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64 bits avec instructions compressées)

Si la plateforme choisie prend en charge des indicateurs d’architecture explicites et que vous souhaitez que le fichier .mpy produit porte la valeur de ces indicateurs, vous devez les passer à la variable d’indicateurs ARCH_FLAGS lors de la construction du fichier .mpy.

Lors de la compilation et de la liaison du fichier .mpy natif, l’architecture doit être choisie et le fichier correspondant ne peut être importé que sur cette architecture (et, si des indicateurs d’architecture sont présents, uniquement s’ils correspondent aux capacités de la cible). Pour plus de détails sur les fichiers .mpy, voir Fichiers .mpy de MicroPython.

Le code natif doit être compilé en code indépendant de la position (PIC) et utiliser une table de décalages globale (GOT), bien que les détails varient d’une architecture à l’autre. Lors de l’importation de fichiers .mpy contenant du code natif, le mécanisme d’importation est capable d’effectuer certaines relocalisations de base du code natif. Cela inclut la relocalisation des sections text, rodata et BSS.

Les fonctionnalités prises en charge par l’éditeur de liens et le chargeur dynamique sont :

  • le code exécutable (text)

  • les données en lecture seule (rodata), y compris les chaînes et les données constantes (tableaux, structures, etc.)

  • les données mises à zéro (BSS)

  • les pointeurs dans text vers text, rodata et BSS

  • les pointeurs dans rodata vers text, rodata et BSS

Les limitations connues sont :

  • les sections de données ne sont pas prises en charge ; solution de contournement : utilisez des données BSS et initialisez explicitement les valeurs des données

  • les variables BSS statiques ne sont pas prises en charge ; solution de contournement : utilisez des variables BSS globales

  • les variables de stockage local au thread ne sont pas prises en charge sur rv32imc ; solution de contournement : utilisez des variables BSS globales ou allouez de l’espace sur le tas pour les stocker

Ainsi, si votre code C comporte des données accessibles en écriture, assurez-vous que ces données sont définies globalement, sans initialiseur, et qu’elles ne sont écrites que dans des fonctions.

Le module natif n’est pas automatiquement lié aux bibliothèques statiques standard comme libm.a et libgcc.a, ce qui peut entraîner des erreurs undefined symbol. Vous pouvez lier les bibliothèques d’exécution en définissant LINK_RUNTIME = 1 dans votre Makefile. Des bibliothèques statiques personnalisées peuvent aussi être liées en ajoutant MPY_LD_FLAGS += -l path/to/library.a. Notez que celles-ci sont liées au module natif et ne seront pas partagées avec d’autres modules ou avec le système.

Limitation de l’éditeur de liens : le module natif n’est pas lié à la table des symboles du micrologiciel MicroPython complet. Il est plutôt lié à une table explicite de symboles exportés présente dans mp_fun_table (dans py/nativeglue.h), qui est figée au moment de la construction du micrologiciel. Il n’est donc pas possible d’appeler simplement une fonction HAL/OS/RTOS/système arbitraire, par exemple, à moins qu’elle ne réside à une adresse fixe. Dans ce cas, le chemin d’un script d’édition de liens contenant une série de noms de symboles et leur adresse fixe peut être passé à mpy_ld.py via l’argument de ligne de commande --externs. Ainsi, les symboles apparaissant dans le script d’édition de liens auront la priorité sur ce qui est fourni par les fichiers objets, mais pour l’instant l’implémentation des fichiers objets résidera toujours dans le fichier MPY final. L’analyseur de scripts d’édition de liens est limité dans ses capacités, et n’est actuellement utilisé que pour analyser la liste des symboles ROM du portage ESP8266 (voir ports/esp8266/boards/eagle.rom.addr.v6.ld).

De nouveaux symboles peuvent être ajoutés à la fin de la table, puis le micrologiciel reconstruit. Les symboles doivent aussi être ajoutés au dictionnaire fun_table de tools/mpy_ld.py au même endroit. Cela permet à mpy_ld.py de détecter les nouveaux symboles et de fournir leurs relocalisations lors de l’importation du mpy. Enfin, si le symbole est une fonction, une macro ou un stub doit être ajouté à py/dynruntime.h pour faciliter l’appel de la fonction.

Définir un module natif

Un module .mpy natif est défini par un ensemble de fichiers utilisés pour construire le .mpy. La disposition du système de fichiers comporte deux parties principales, les fichiers source et le Makefile :

  • Dans le cas le plus simple, un seul fichier source C est requis, contenant tout le code qui sera compilé dans le module .mpy. Ce code source C doit inclure le fichier py/dynruntime.h pour accéder à l’API dynamique de MicroPython, et doit au minimum définir une fonction nommée mpy_init. Cette fonction sera le point d’entrée du module, appelée lors de l’importation du module.

    Le module peut être divisé en plusieurs fichiers source C si nécessaire. Des parties du module peuvent aussi être implémentées en Python. Tous les fichiers source doivent être listés dans le Makefile, en les ajoutant à la variable SRC (voir ci-dessous). Cela inclut aussi bien les fichiers source C que les éventuels fichiers Python qui seront inclus dans le fichier .mpy résultant.

  • Le Makefile contient la configuration de construction du module et liste les fichiers source utilisés pour construire le module .mpy. Il doit définir MPY_DIR comme l’emplacement du dépôt MicroPython (pour trouver les fichiers d’en-tête, le fragment de Makefile correspondant et l’outil mpy_ld.py), MOD comme le nom du module, SRC comme la liste des fichiers source, spécifier éventuellement l’architecture machine via ARCH, avec d’éventuels indicateurs d’architecture machine spécifiés via ARCH_FLAGS, puis inclure py/dynruntime.mk.

Exemple minimal

Cette section fournit un exemple entièrement fonctionnel d’un module simple nommé factorial. Ce module fournit une seule fonction factorial.factorial(x) qui calcule la factorielle de l’entrée et renvoie le résultat.

Disposition du répertoire

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

Le fichier factorial.c contient :

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

Le fichier Makefile contient :

# 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

Compiler le module

Les outils prérequis pour construire un fichier .mpy natif sont :

  • Le dépôt MicroPython (au moins les répertoires py/ et tools/).

  • CPython 3, et la bibliothèque pyelftools (par exemple pip install 'pyelftools>=0.25').

  • GNU make.

  • Un compilateur C pour l’architecture cible (si du code source C est utilisé).

  • Éventuellement mpy-cross, construit à partir du dépôt MicroPython (si du code source .py est utilisé).

Veillez à sélectionner la bonne valeur ARCH pour la cible sur laquelle vous allez exécuter le module. Construisez ensuite avec

$ make

Sans modifier le Makefile, vous pouvez spécifier l’architecture cible via

$ make ARCH=armv7m

Il en va de même pour les indicateurs d’architecture optionnels via

$ make ARCH=rv32imc ARCH_FLAGS=zba

Utilisation du module dans MicroPython

Une fois le module construit, vous devriez obtenir un fichier nommé factorial.mpy. Copiez-le de manière à ce qu’il soit accessible sur le système de fichiers de votre système MicroPython et qu’il puisse être trouvé dans le chemin d’importation. Le module peut alors être utilisé en Python comme n’importe quel autre module, par exemple

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

Utiliser Picolibc pour construire des modules

L’utilisation de Picolibc comme bibliothèque standard C est non seulement prise en charge, mais constitue en fait la valeur par défaut pour les plateformes rv32imc et rv64imc. Cependant, il y a quelques points à mentionner pour vous assurer de ne pas rencontrer de problèmes par la suite lors de la construction du code.

Certaines versions précompilées de Picolibc (par exemple, celles fournies par Ubuntu Linux sous forme des paquets picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf et picolibc-xtensa-lx106-elf) supposent que le stockage local au thread (TLS) est disponible à l’exécution, mais malheureusement les modules MicroPython ne le prennent pas en charge sur certaines architectures (à savoir rv32imc et rv64imc). Cela signifie que certaines fonctionnalités fournies par Picolibc utiliseront par défaut le TLS, renvoyant une erreur soit lors de la compilation, soit lors de l’édition de liens.

Pour un exemple de la manière dont cela peut vous affecter, le module d’exemple examples/natmod/btree contient une solution de contournement pour s’assurer que errno fonctionne (cherchez __PICOLIBC_ERRNO_FUNCTION dans le Makefile et suivez la piste à partir de là).

Autres exemples

Consultez examples/natmod/ pour d’autres exemples qui montrent un grand nombre des fonctionnalités disponibles des modules .mpy natifs. Parmi ces fonctionnalités :

  • l’utilisation de plusieurs fichiers source C

  • l’inclusion de code Python aux côtés du code C

  • les données rodata et BSS

  • l’allocation de mémoire

  • l’utilisation de virgule flottante

  • la gestion des exceptions

  • l’inclusion de bibliothèques C externes