ENbext.dev
DocumentationExtensions

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Plugins Python

bext peut exécuter des plugins Python en chargeant l'interpréteur CPython Pyodide en tant que module wasm dans le runtime bext-plugin-wasm existant. Cela vous donne le langage CPython complet, une tranche significative de la bibliothèque standard et l'accès à n'importe quel package purement Python, sans ajouter un deuxième moteur d'exécution au processus.

Cette page est le plan d'expédition pour Python sur bext. Le sous-mode est aujourd'hui un squelette : la couture architecturale est en place, la caisse du plugin de référence existe, et l'appel du chargeur avec une config Python renvoie une erreur typée "pas encore implémenté". L'exécution complète est bloquée sur le cache d'instantanés P2 AOT du plan de performances – voir État actuel ci-dessous pour savoir pourquoi.

Où Python s'intègre#

bext est livré avec plusieurs environnements d'exécution de plugin. Chacun s’adresse à une forme de travail différente.

Durée d'exécution Idéal pour Démarrage à froid Pourquoi
WASM (natif) Code compilé critique en termes de performances (Rust, C/C++, AssemblyScript) ~1 ms (AOT mis en cache) Compteur de carburant, bac à sable, plus petit frais généraux
V8 Middleware JavaScript sur le chemin actif ~15 ms (piscine chaude : 0) Même moteur que SSR, piscine isolée chaude
QuickJS Minuscules plugins JS, sémantique déterministe < 5 ms Interprète, RAM minimale
Lua Scripts, décisions basées sur la configuration < 5 ms mlua, petite empreinte, grande histoire d'intégration
Pyodide (Python) Travaux par lots, pipelines de données, génération de rapports 1 à 3 s sans AOT / faible ms avec instantanés Langage CPython complet + stdlib + écosystème pur Python

Pyodide n'est pas un runtime de chemin de requête. Même avec un réglage agressif, il est d'un ordre de grandeur plus lourd que le V8 ou QuickJS. Utilisez-le là où l'écosystème de Python gagne (une transformation nocturne programmée, un rapport hebdomadaire, un nettoyage de données en une seule fois) et optez pour V8, QuickJS, Lua ou Wasm natif lorsque la latence est importante.

Architecture#

Un plugin Python est et non, un module wasm brut que bext charge directement. Il s'agit d'un programme Python qui s'exécute dans un interpréteur wasm Pyodide partagé, qui à son tour est chargé par le même runtime bext-plugin-wasm qui contient déjà les plugins wasm natifs. Le manifeste du plugin distingue les deux modes via l'énumération WasmGuestKind :

rust
pub enum WasmGuestKind {
    /// The historical default — wasm compiled from Rust/C++/AssemblyScript.
    Native,
    /// Python source executed inside a bundled Pyodide wasm blob.
    Pyodide {
        python_src: String,
        pyodide_version: String,
    },
}

Le graphique d'appel pour une requête qui atteint un plugin Python ressemble à ceci :

code
bext host (Rust)
   │
   ├── bext-plugin-wasm runtime (wasmtime)
   │      │
   │      ├── Pyodide wasm blob (CPython compiled to wasm)
   │      │      │
   │      │      └── Your transform.py
   │      │
   │      └── Host function bridge (via Pyodide JS proxy layer)
   │
   └── Host capabilities (KV, queue, HTTP fetch, tracer, …)

Deux choses méritent d’être soulignées :

  1. Pyodide est chargé une fois par plugin, pas par requête. L'interpréteur Python démarre, analyse votre script et réside dans un magasin wasmtime. Les appels ultérieurs réutilisent le magasin, de sorte que le coût de démarrage à froid est amorti pour chaque appel qui atteint cette instance.
  2. Les appels de fonction hôte franchissent trois frontières. Python → API C de Pyodide → couche de cale JS de Pyodide → fonction hôte wasmtime → bext. Le saut supplémentaire est mesurable (~ dizaines de microsecondes par appel) mais stable. Ce qu'il faut retenir pour les auteurs : préférez les appels d'hôte grossiers aux appels bavards.
Attention

Les plugins Python ne conviennent pas au chemin de requête jusqu'à ce que le cache d'instantanés AOT arrive. Un démarrage à froid de l'interpréteur Pyodide prend 1 à 3 secondes, soit des ordres de grandeur plus lents que V8 (~ 15 ms) ou QuickJS (< 5 ms). Câblez plutôt les plugins Python via le Capacité planifiée pour le travail par lots et cron.

État actuel#

Le sous-mode Pyodide est un squelette. Concrètement :

  • WasmGuestKind::Pyodide { .. } existe dans bext-plugin-wasm::runtime et constitue une option de configuration visible et saisie.
  • WasmPluginRuntime::load_plugin est distribué sur la variante. Pour Native, le chemin historique reste inchangé. Pour Pyodide, le runtime renvoie une erreur typée qui mentionne cette page et le plan des jalons.
  • Une caisse de plugin Python de référence, bext-transform-py, se trouve dans l'espace de travail avec la source Python et un stub Rust. Son test unitaire affirme que la construction du plugin aujourd'hui renvoie une erreur NotYetImplemented propre plutôt que de se bloquer ou de paniquer.
  • L'exécution complète de bout en bout n'est pas implémentée.

Pourquoi cette attente#

Le démarrage à froid de Pyodide dure 1 à 3 secondes pour un interprète minimal sans roues utilisateur. C'est acceptable pour un travail planifié qui s'exécute toutes les heures et rien de plus, mais cela rend Pyodide activement hostile à toute charge de travail qui pourrait se retrouver sur le chemin de la requête. Le cache d'instantanés P2 AOT du plan de performances ( plan/performance/03-request-path.md ) est la pièce manquante : les instantanés par plug-in conservés sur le disque déplacent le coût de « réinitialiser l'interpréteur » à « mmapper quelques mégaoctets », ce qui entraîne une surcharge par invocation dans la plage de faibles ms.

Nous expédions le squelette maintenant, avant le cache AOT, volontairement :

  • La couture est découplée de la mise en œuvre. Une modification future peut ajouter le chargeur Pyodide sans toucher au runtime externe, à l'appelant du serveur bext ou au schéma manifeste.
  • Le plugin de référence crate donne aux auteurs un type concret à importer. Lorsque le runtime arrive, l'activation des plugins Python existants est une tâche d'une seule ligne.
  • La forme de l'erreur est définitive. Le code en aval de peut aujourd'hui correspondre à Err à partir de load_plugin sans savoir si le runtime de support est tronqué ou réel.

Voir plan/ecosystem/08-milestones.md §E4 pour le plan de déploiement complet.

Quand c'est fait - forme d'utilisation#

Voici à quoi ressemblera un plugin Python une fois le sous-mode connecté. Les formes sont définitives au meilleur de nos connaissances actuelles ; la surface de planification et d'invocation est décrite ici afin que les auteurs puissent planifier avant l'atterrissage du chargeur.

Le côté Python#

Votre plugin est un module Python qui expose un ou plusieurs points d'entrée :

python
def transform(input_json):
    user_id = input_json.get("user_id")
    email = (input_json.get("email") or "").strip().lower()
    amount_cents = int(input_json.get("amount_cents", 0))
    currency = input_json.get("currency", "USD").upper()

    return {
        "user_id": user_id,
        "email": email,
        "amount": {"cents": amount_cents, "currency": currency},
        "is_high_value": amount_cents >= 100_000,
    }

Le travail par enregistrement ressemble à du Python classique. Rien sur l'hôte wasm ne fuit dans le langage - vous lisez un dict, renvoyez un dict et déclenchez des exceptions en cas d'erreur. L'hôte sérialise JSON à la limite.

Le côté rouille#

Le chargement du plugin ressemble à n'importe quel autre WasmPluginConfig, avec le champ guest_kind défini sur Pyodide :

rust
use bext_plugin_wasm::runtime::{WasmGuestKind, WasmPluginConfig, WasmPluginRuntime};

let mut rt = WasmPluginRuntime::new(storage_root)?;
rt.load_plugin(WasmPluginConfig {
    name: "nightly-transform".into(),
    path: pyodide_wasm_blob_path(),   // shared across all Python plugins
    priority: 1000,
    permissions,
    config: serde_json::Value::Null,
    guest_kind: WasmGuestKind::Pyodide {
        python_src: bext_transform_py::PYTHON_SOURCE.into(),
        pyodide_version: bext_transform_py::PYODIDE_VERSION.into(),
    },
})?;

L’épinglage de pyodide_version est obligatoire. Différentes versions de Pyodide contiennent différents sous-ensembles stdlib et différentes roues précompilées ; une mise à niveau doit être un changement de configuration visible, jamais un changement de comportement silencieux.

La forme de la planification#

Étant donné que les plugins Python sont orientés par lots, le câblage le plus naturel consiste à utiliser la fonctionnalité Scheduled ( @bext/cron ) : enregistrez une entrée cron qui est envoyée dans la transformation Python à chaque tick. Le chemin du travail planifié est synchrone du point de vue de bext : le planificateur transmet un élément de travail au runtime wasm, le runtime appelle Pyodide, Pyodide exécute transform(), le résultat ressort. Pour les travaux de longue durée, divisez-le en morceaux qui correspondent chacun au budget carburant par appel.

L’utilisation du hot-path est possible – rien dans le runtime ne l’interdit – mais n’a de sens qu’une fois que les instantanés AOT sont en place. D’ici là, tout plugin Python qui gère une requête sera la chose la plus lente du cycle de vie de la requête de plusieurs ordres de grandeur.

Contraintes connues#

Celles-ci sont intégrées à l'architecture et resteront vraies une fois le sous-mode entièrement implémenté. Voir crates/bext-plugin-wasm/KNOWN_DIFFERENCES.md pour la liste faisant autorité.

  • Aucune extension native CPython par défaut. Les packages Pure-Python fonctionnent. Tout ce qui dépend d'une extension C compilée nécessite une roue Pyodide ou un port construit par Emscripten. La plupart de la pile scientifique Python possède une roue Pyodide ; les roues arbitraires de PyPI ne le font pas.
  • asyncio s'exécute sur le planificateur interne de Pyodide. bext voit une exécution Python synchrone par appel d'invité ; Le code Python qui s'appuie sur await doit être complété dans la fenêtre de carburant.
  • Les appels de fonction hôte passent par la couche proxy Pyodide JS. Frais généraux mesurables par appel. Les appels à gros grain sont moins chers que de nombreux petits appels.
  • Aucun sous-processus, aucun gestionnaire de signal. L'environnement Pyodide ne peut pas générer de processus ni installer de gestionnaires de signaux. Décochez plutôt via une fonction hôte, ou non.
  • La taille du paquet compte. Un blob Pyodide nu fait environ 10 Mo ; l'ajout de roues scientifiques pousse cela à des centaines de mégaoctets. Les instantanés échangent le disque contre une latence de démarrage à froid. L'interface utilisateur du registre (E10) affichera les deux numéros au moment de l'installation.
  • Une exécution Python par invité. Le GIL est interne au Pyodide ; bext exécute un hook à la fois par magasin. Les plugins qui souhaitent le parallélisme gèrent plusieurs magasins.

Comparaison : quand atteindre quelle exécution#

Une heuristique fonctionnelle pour choisir un runtime sur bext :

  • Rust/C++/AssemblyScript → WASM natif. Vous voulez le code invité le plus rapide possible, vous êtes à l'aise avec une chaîne d'outils compilée et le plugin se trouve sur le chemin de la requête.
  • JavaScript → V8 ou QuickJS. V8 pour les middlewares sensibles au débit (WAF, validation de schéma JSON, décodage de jetons), QuickJS pour les petits utilitaires et la sémantique déterministe.
  • Scripts configurables → Lua. Petites règles, décisions d'autorisation/refus, limiteurs de débit modifiables par l'utilisateur. Petite empreinte, belle histoire d'intégration.
  • Travail sur données par lots → Python (Pyodide). Transformations planifiées, rapports nocturnes, nettoyages de données, tout ce qui indique que « J'ai déjà un script Python qui fait cela » est vrai. Pas le chemin de la requête, du moins pas jusqu'à ce que les instantanés AOT arrivent.

Le runtime que vous choisissez au moment de la création du plugin n'est pas une décision permanente - le manifeste du plugin de bext fait de chaque runtime un homologue, de sorte qu'un prototype Python peut être porté vers wasm natif plus tard s'il dépasse le modèle par lots.

Voir aussi#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.