- Accueil
- Documentation
- Natifs
- Portage vers pi-natives (N-API) — Notes de terrain
Portage vers pi-natives (N-API) — Notes de terrain
Ceci est un guide pratique pour déplacer les chemins critiques dans crates/pi-natives et les connecter via les bindings JS. Il existe pour éviter que les mêmes erreurs se reproduisent.
Quand effectuer un portage
Section intitulée « Quand effectuer un portage »Portez lorsque l’une de ces conditions est vraie :
- Le chemin critique s’exécute dans des boucles de rendu, des mises à jour UI fréquentes ou des traitements par lots volumineux.
- Les allocations JS dominent (rotation de chaînes, retour en arrière de regex, grands tableaux).
- Vous disposez déjà d’une référence JS et pouvez comparer les deux versions côte à côte.
- Le travail est limité par le CPU ou du I/O bloquant qui peut s’exécuter sur le pool de threads libuv.
- Le travail est du I/O asynchrone qui peut s’exécuter sur le runtime Tokio (par ex., exécution shell).
Évitez les portages qui dépendent d’un état uniquement JS ou d’imports dynamiques. Les exports N-API doivent être purs, données en entrée/données en sortie. Le travail de longue durée doit passer par task::blocking (limité par le CPU/I/O bloquant) ou task::future (I/O asynchrone) avec annulation.
Anatomie d’un export natif
Section intitulée « Anatomie d’un export natif »Côté Rust :
- L’implémentation se trouve dans
crates/pi-natives/src/<module>.rs. Si vous ajoutez un nouveau module, enregistrez-le danscrates/pi-natives/src/lib.rs. - Exportez avec
#[napi]; les exports en snake_case sont convertis automatiquement en camelCase. Utilisezjs_nameexplicite uniquement pour les vrais alias/noms non par défaut. Utilisez#[napi(object)]pour les structs. - Utilisez
task::blocking(tag, cancel_token, work)(voircrates/pi-natives/src/task.rs) pour le travail limité par le CPU ou bloquant. Utiliseztask::future(env, tag, work)pour le travail asynchrone nécessitant Tokio (par ex., sessions shell). Passez unCancelTokenlorsque vous exposeztimeoutMsouAbortSignal.
Côté JS :
packages/natives/src/bindings.tscontient l’interface de baseNativeBindings.packages/natives/src/<module>/types.tsdéfinit les types TS et augmenteNativeBindingsvia la fusion de déclarations.packages/natives/src/native.tsimporte chaque fichier<module>/types.tspour activer les déclarations.packages/natives/src/<module>/index.tsencapsule le bindingnativedepuispackages/natives/src/native.ts.packages/natives/src/native.tscharge l’addon etvalidateNativevérifie les exports requis.packages/natives/src/index.tsréexporte le wrapper pour les appelants danspackages/*.
Liste de contrôle du portage
Section intitulée « Liste de contrôle du portage »- Ajouter l’implémentation Rust
- Placez la logique principale dans une fonction Rust simple.
- S’il s’agit d’un nouveau module, ajoutez-le à
crates/pi-natives/src/lib.rs. - Exposez-le avec
#[napi]pour que le mapping par défaut snake_case -> camelCase reste cohérent. - Gardez les signatures possédées et simples :
String,Vec<String>,Uint8Array, ouEither<JsString, Uint8Array>pour les entrées volumineuses de chaînes/octets. - Pour le travail limité par le CPU ou bloquant, utilisez
task::blocking; pour le travail asynchrone, utiliseztask::future. Passez unCancelTokenet appelezheartbeat()à l’intérieur des boucles longues.
- Connecter les bindings JS
- Ajoutez les types et l’augmentation
NativeBindingsdanspackages/natives/src/<module>/types.ts. - Importez
./<module>/typesdanspackages/natives/src/native.tspour déclencher la fusion de déclarations. - Ajoutez un wrapper dans
packages/natives/src/<module>/index.tsqui appellenative. - Réexportez depuis
packages/natives/src/index.ts.
- Mettre à jour la validation native
- Ajoutez
checkFn("newExport")dansvalidateNative(packages/natives/src/native.ts).
- Ajouter des benchmarks
- Placez les benchmarks à côté du package propriétaire (
packages/tui/bench,packages/natives/bench, oupackages/coding-agent/bench). - Incluez une référence JS et une version native dans la même exécution.
- Utilisez
Bun.nanoseconds()et un nombre d’itérations fixe. - Gardez les entrées du benchmark petites et réalistes (données réelles observées dans le chemin critique).
- Compiler le binaire natif
bun --cwd=packages/natives run build- Utilisez
bun --cwd=packages/natives run buildet définissezPI_DEV=1si vous souhaitez des diagnostics du loader pendant les tests.
- Exécuter le benchmark
bun run packages/<pkg>/bench/<bench>.ts(oubun --cwd=packages/natives run bench)
- Décider de l’utilisation
- Si le natif est plus lent, gardez JS et laissez l’export natif inutilisé.
- Si le natif est plus rapide, basculez les sites d’appel vers le wrapper natif.
Points de friction et comment les éviter
Section intitulée « Points de friction et comment les éviter »1) Un pi_natives.node obsolète empêche les nouveaux exports
Section intitulée « 1) Un pi_natives.node obsolète empêche les nouveaux exports »Le loader préfère le binaire taggé par plateforme dans packages/natives/native (pi_natives.<platform>-<arch>.node). PI_DEV=1 n’active désormais que les diagnostics du loader ; il ne bascule plus vers un nom de fichier d’addon dev séparé. Il existe également un fallback pi_natives.node. Les binaires compilés s’extraient vers ~/.xcsh/natives/<version>/pi_natives.<platform>-<arch>.node. Si l’un d’entre eux est obsolète, les exports ne se mettront pas à jour.
Correctif : supprimez le fichier obsolète avant de reconstruire.
rm packages/natives/native/pi_natives.linux-x64.noderm packages/natives/native/pi_natives.nodebun --cwd=packages/natives run buildSi vous exécutez un binaire compilé, supprimez le répertoire d’addon en cache :
rm -rf ~/.xcsh/natives/<version>Puis vérifiez que l’export existe dans le binaire :
bun -e 'const tag = `${process.platform}-${process.arch}`; const mod = require(`./packages/natives/native/pi_natives.${tag}.node`); console.log(Object.keys(mod).includes("newExport"));'2) Erreurs “Missing exports” de validateNative
Section intitulée « 2) Erreurs “Missing exports” de validateNative »C’est positif — cela empêche les incohérences silencieuses. Lorsque vous voyez ceci :
Native addon missing exports ... Missing: visibleWidthcela signifie que votre binaire est obsolète, que le nom de l’export Rust (ou l’alias explicite lorsqu’il est utilisé) ne correspond pas au nom JS, ou que l’export n’a jamais été compilé. Corrigez le build et l’incohérence de nommage, n’affaiblissez pas la validation.
3) Incompatibilité de signature Rust
Section intitulée « 3) Incompatibilité de signature Rust »Gardez-la simple et possédée. String, Vec<String>, et Uint8Array fonctionnent. Évitez les références comme &str dans les exports publics. Si vous avez besoin de données structurées, encapsulez-les dans des structs #[napi(object)].
4) Erreurs de benchmarking
Section intitulée « 4) Erreurs de benchmarking »- Ne comparez pas des entrées ou allocations différentes.
- Gardez JS et natif utilisant des tableaux d’entrée identiques.
- Exécutez les deux dans le même fichier de benchmark pour éviter le biais.
Modèle de benchmark
Section intitulée « Modèle de benchmark »const ITERATIONS = 2000;
function bench(name: string, fn: () => void): number { const start = Bun.nanoseconds(); for (let i = 0; i < ITERATIONS; i++) fn(); const elapsed = (Bun.nanoseconds() - start) / 1e6; console.log(`${name}: ${elapsed.toFixed(2)}ms total (${(elapsed / ITERATIONS).toFixed(6)}ms/op)`); return elapsed;}
bench("feature/js", () => { jsImpl(sample);});
bench("feature/native", () => { nativeImpl(sample);});Liste de contrôle de vérification
Section intitulée « Liste de contrôle de vérification »validateNativepasse (aucun export manquant).NativeBindingsest augmenté danspackages/natives/src/<module>/types.tset le wrapper est réexporté danspackages/natives/src/index.ts.Object.keys(require(...))inclut votre nouvel export.- Les chiffres de benchmark sont enregistrés dans la PR/notes.
- Le site d’appel est mis à jour uniquement si le natif est plus rapide ou équivalent.
Règle générale
Section intitulée « Règle générale »- Si le natif est plus lent, ne basculez pas. Gardez l’export pour un travail futur, mais le TUI doit rester sur le chemin le plus rapide.
- Si le natif est plus rapide, basculez le site d’appel et gardez le benchmark en place pour détecter les régressions.