- Home
- Documentation
- Nativi
- Portare a pi-natives (N-API) — Note sul campo
Portare a pi-natives (N-API) — Note sul campo
Questa è una guida pratica per spostare i percorsi critici in crates/pi-natives e collegarli attraverso i binding JS. Esiste per evitare che gli stessi errori si ripetano.
Quando effettuare il porting
Sezione intitolata “Quando effettuare il porting”Effettuare il porting quando una qualsiasi di queste condizioni è vera:
- Il percorso critico viene eseguito nei cicli di rendering, negli aggiornamenti rapidi dell’UI o in elaborazioni batch di grandi dimensioni.
- Le allocazioni JS dominano (creazione continua di stringhe, backtracking delle regex, array di grandi dimensioni).
- Si dispone già di un baseline JS e si possono confrontare entrambe le versioni fianco a fianco.
- Il lavoro è CPU-bound o I/O bloccante che può essere eseguito sul thread pool di libuv.
- Il lavoro è I/O asincrono che può essere eseguito sul runtime di Tokio (ad esempio, esecuzione shell).
Evitare i porting che dipendono da stato esclusivamente JS o da import dinamici. Le esportazioni N-API dovrebbero essere pure, con dati in ingresso e dati in uscita. Il lavoro di lunga durata dovrebbe passare attraverso task::blocking (CPU-bound/I/O bloccante) o task::future (I/O asincrono) con cancellazione.
Anatomia di un’esportazione nativa
Sezione intitolata “Anatomia di un’esportazione nativa”Lato Rust:
- L’implementazione risiede in
crates/pi-natives/src/<module>.rs. Se si aggiunge un nuovo modulo, registrarlo incrates/pi-natives/src/lib.rs. - Esportare con
#[napi]; le esportazioni in snake_case vengono convertite automaticamente in camelCase. Usarejs_nameesplicito solo per veri alias/nomi non predefiniti. Usare#[napi(object)]per le struct. - Usare
task::blocking(tag, cancel_token, work)(vederecrates/pi-natives/src/task.rs) per lavoro CPU-bound o bloccante. Usaretask::future(env, tag, work)per lavoro asincrono che necessita di Tokio (ad esempio, sessioni shell). Passare unCancelTokenquando si esponetimeoutMsoAbortSignal.
Lato JS:
packages/natives/src/bindings.tscontiene l’interfaccia baseNativeBindings.packages/natives/src/<module>/types.tsdefinisce i tipi TS e augmentaNativeBindingstramite declaration merging.packages/natives/src/native.tsimporta ciascun file<module>/types.tsper attivare le dichiarazioni.packages/natives/src/<module>/index.tswrappa il bindingnativedapackages/natives/src/native.ts.packages/natives/src/native.tscarica l’addon evalidateNativeverifica le esportazioni richieste.packages/natives/src/index.tsri-esporta il wrapper per i chiamanti inpackages/*.
Checklist per il porting
Sezione intitolata “Checklist per il porting”- Aggiungere l’implementazione Rust
- Inserire la logica principale in una funzione Rust pura.
- Se è un nuovo modulo, aggiungerlo a
crates/pi-natives/src/lib.rs. - Esporlo con
#[napi]in modo che la mappatura predefinita snake_case -> camelCase rimanga consistente. - Mantenere le firme owned e semplici:
String,Vec<String>,Uint8ArrayoEither<JsString, Uint8Array>per input di stringhe/byte di grandi dimensioni. - Per lavoro CPU-bound o bloccante, usare
task::blocking; per lavoro asincrono, usaretask::future. Passare unCancelTokene chiamareheartbeat()all’interno dei cicli lunghi.
- Collegare i binding JS
- Aggiungere i tipi e l’augmentation di
NativeBindingsinpackages/natives/src/<module>/types.ts. - Importare
./<module>/typesinpackages/natives/src/native.tsper attivare il declaration merging. - Aggiungere un wrapper in
packages/natives/src/<module>/index.tsche chiamanative. - Ri-esportare da
packages/natives/src/index.ts.
- Aggiornare la validazione nativa
- Aggiungere
checkFn("newExport")invalidateNative(packages/natives/src/native.ts).
- Aggiungere benchmark
- Posizionare i benchmark accanto al pacchetto proprietario (
packages/tui/bench,packages/natives/benchopackages/coding-agent/bench). - Includere una versione baseline JS e una versione nativa nella stessa esecuzione.
- Usare
Bun.nanoseconds()e un conteggio di iterazioni fisso. - Mantenere gli input del benchmark piccoli e realistici (dati effettivi osservati nel percorso critico).
- Compilare il binario nativo
bun --cwd=packages/natives run build- Usare
bun --cwd=packages/natives run builde impostarePI_DEV=1se si desiderano diagnostiche del loader durante i test.
- Eseguire il benchmark
bun run packages/<pkg>/bench/<bench>.ts(oppurebun --cwd=packages/natives run bench)
- Decidere sull’utilizzo
- Se il nativo è più lento, mantenere JS e lasciare l’esportazione nativa inutilizzata.
- Se il nativo è più veloce, passare i punti di chiamata al wrapper nativo.
Punti critici e come evitarli
Sezione intitolata “Punti critici e come evitarli”1) pi_natives.node obsoleto impedisce le nuove esportazioni
Sezione intitolata “1) pi_natives.node obsoleto impedisce le nuove esportazioni”Il loader preferisce il binario con tag della piattaforma in packages/natives/native (pi_natives.<platform>-<arch>.node). PI_DEV=1 ora abilita solo le diagnostiche del loader; non passa più a un nome file addon di sviluppo separato. Esiste anche un fallback pi_natives.node. I binari compilati vengono estratti in ~/.xcsh/natives/<version>/pi_natives.<platform>-<arch>.node. Se uno qualsiasi di questi è obsoleto, le esportazioni non si aggiorneranno.
Soluzione: rimuovere il file obsoleto prima di ricompilare.
rm packages/natives/native/pi_natives.linux-x64.noderm packages/natives/native/pi_natives.nodebun --cwd=packages/natives run buildSe si sta eseguendo un binario compilato, eliminare la directory dell’addon in cache:
rm -rf ~/.xcsh/natives/<version>Quindi verificare che l’esportazione esista nel binario:
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) Errori “Missing exports” da validateNative
Sezione intitolata “2) Errori “Missing exports” da validateNative”Questo è positivo — previene disallineamenti silenti. Quando si vede questo:
Native addon missing exports ... Missing: visibleWidthsignifica che il binario è obsoleto, il nome dell’esportazione Rust (o l’alias esplicito quando usato) non corrisponde al nome JS, oppure l’esportazione non è mai stata compilata. Correggere la build e il disallineamento dei nomi, non indebolire la validazione.
3) Mismatch della firma Rust
Sezione intitolata “3) Mismatch della firma Rust”Mantenerla semplice e owned. String, Vec<String> e Uint8Array funzionano. Evitare riferimenti come &str nelle esportazioni pubbliche. Se si necessita di dati strutturati, avvolgerli in struct #[napi(object)].
4) Errori nei benchmark
Sezione intitolata “4) Errori nei benchmark”- Non confrontare input o allocazioni diverse.
- Mantenere JS e nativo con array di input identici.
- Eseguire entrambi nello stesso file di benchmark per evitare scostamenti.
Template per benchmark
Sezione intitolata “Template per 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);});Checklist di verifica
Sezione intitolata “Checklist di verifica”validateNativepassa (nessuna esportazione mancante).NativeBindingsè augmentato inpackages/natives/src/<module>/types.tse il wrapper è ri-esportato inpackages/natives/src/index.ts.Object.keys(require(...))include la nuova esportazione.- Numeri dei benchmark registrati nella PR/note.
- Punto di chiamata aggiornato solo se il nativo è più veloce o equivalente.
Regola generale
Sezione intitolata “Regola generale”- Se il nativo è più lento, non effettuare il passaggio. Mantenere l’esportazione per lavoro futuro, ma la TUI dovrebbe rimanere sul percorso più veloce.
- Se il nativo è più veloce, passare al punto di chiamata nativo e mantenere il benchmark attivo per intercettare regressioni.