- Inicio
- Documentation
- Nativos
- Portando a pi-natives (N-API) — Notas de campo
Portando a pi-natives (N-API) — Notas de campo
Esta es una guía práctica para mover rutas críticas a crates/pi-natives y conectarlas a través de los bindings de JS. Existe para evitar que los mismos errores ocurran dos veces.
Cuándo portar
Sección titulada «Cuándo portar»Porte cuando cualquiera de estas condiciones sea verdadera:
- La ruta crítica se ejecuta en bucles de renderizado, actualizaciones frecuentes de UI o lotes grandes.
- Las asignaciones de JS dominan (rotación de strings, backtracking de regex, arrays grandes).
- Ya tiene una línea base en JS y puede comparar ambas versiones en paralelo.
- El trabajo está limitado por CPU o es I/O bloqueante que puede ejecutarse en el pool de hilos de libuv.
- El trabajo es I/O asíncrono que puede ejecutarse en el runtime de Tokio (por ejemplo, ejecución de shell).
Evite portar lo que dependa de estado exclusivo de JS o importaciones dinámicas. Las exportaciones de N-API deben ser puras, datos de entrada/datos de salida. El trabajo de larga duración debe pasar por task::blocking (limitado por CPU/I/O bloqueante) o task::future (I/O asíncrono) con cancelación.
Anatomía de una exportación nativa
Sección titulada «Anatomía de una exportación nativa»Lado Rust:
- La implementación reside en
crates/pi-natives/src/<module>.rs. Si añade un nuevo módulo, regístrelo encrates/pi-natives/src/lib.rs. - Exporte con
#[napi]; las exportaciones en snake_case se convierten a camelCase automáticamente. Usejs_nameexplícito solo para alias verdaderos/nombres no predeterminados. Use#[napi(object)]para structs. - Use
task::blocking(tag, cancel_token, work)(vercrates/pi-natives/src/task.rs) para trabajo limitado por CPU o bloqueante. Usetask::future(env, tag, work)para trabajo asíncrono que necesite Tokio (por ejemplo, sesiones de shell). Pase unCancelTokencuando expongatimeoutMsoAbortSignal.
Lado JS:
packages/natives/src/bindings.tscontiene la interfaz baseNativeBindings.packages/natives/src/<module>/types.tsdefine los tipos TS y amplíaNativeBindingsmediante declaration merging.packages/natives/src/native.tsimporta cada archivo<module>/types.tspara activar las declaraciones.packages/natives/src/<module>/index.tsenvuelve el bindingnativedepackages/natives/src/native.ts.packages/natives/src/native.tscarga el addon yvalidateNativevalida las exportaciones requeridas.packages/natives/src/index.tsre-exporta el wrapper para los consumidores enpackages/*.
Lista de verificación para portar
Sección titulada «Lista de verificación para portar»- Añadir la implementación en Rust
- Coloque la lógica principal en una función Rust simple.
- Si es un nuevo módulo, añádalo a
crates/pi-natives/src/lib.rs. - Expóngalo con
#[napi]para que el mapeo predeterminado snake_case -> camelCase se mantenga consistente. - Mantenga las firmas propias y simples:
String,Vec<String>,Uint8Array, oEither<JsString, Uint8Array>para entradas grandes de string/bytes. - Para trabajo limitado por CPU o bloqueante, use
task::blocking; para trabajo asíncrono, usetask::future. Pase unCancelTokeny llame aheartbeat()dentro de bucles largos.
- Conectar los bindings JS
- Añada los tipos y la ampliación de
NativeBindingsenpackages/natives/src/<module>/types.ts. - Importe
./<module>/typesenpackages/natives/src/native.tspara activar el declaration merging. - Añada un wrapper en
packages/natives/src/<module>/index.tsque llame anative. - Re-exporte desde
packages/natives/src/index.ts.
- Actualizar la validación nativa
- Añada
checkFn("newExport")envalidateNative(packages/natives/src/native.ts).
- Añadir benchmarks
- Coloque los benchmarks junto al paquete propietario (
packages/tui/bench,packages/natives/bench, opackages/coding-agent/bench). - Incluya una línea base JS y la versión nativa en la misma ejecución.
- Use
Bun.nanoseconds()y un conteo de iteraciones fijo. - Mantenga las entradas del benchmark pequeñas y realistas (datos reales observados en la ruta crítica).
- Compilar el binario nativo
bun --cwd=packages/natives run build- Use
bun --cwd=packages/natives run buildy establezcaPI_DEV=1si desea diagnósticos del loader durante las pruebas.
- Ejecutar el benchmark
bun run packages/<pkg>/bench/<bench>.ts(obun --cwd=packages/natives run bench)
- Decidir sobre el uso
- Si lo nativo es más lento, mantenga JS y deje la exportación nativa sin usar.
- Si lo nativo es más rápido, cambie los sitios de llamada al wrapper nativo.
Puntos problemáticos y cómo evitarlos
Sección titulada «Puntos problemáticos y cómo evitarlos»1) Un pi_natives.node obsoleto impide nuevas exportaciones
Sección titulada «1) Un pi_natives.node obsoleto impide nuevas exportaciones»El loader prefiere el binario etiquetado por plataforma en packages/natives/native (pi_natives.<platform>-<arch>.node). PI_DEV=1 ahora solo habilita diagnósticos del loader; ya no cambia a un nombre de archivo de addon de desarrollo separado. También existe un fallback pi_natives.node. Los binarios compilados se extraen a ~/.xcsh/natives/<version>/pi_natives.<platform>-<arch>.node. Si alguno de estos está obsoleto, las exportaciones no se actualizarán.
Solución: elimine el archivo obsoleto antes de recompilar.
rm packages/natives/native/pi_natives.linux-x64.noderm packages/natives/native/pi_natives.nodebun --cwd=packages/natives run buildSi está ejecutando un binario compilado, elimine el directorio del addon en caché:
rm -rf ~/.xcsh/natives/<version>Luego verifique que la exportación existe en el 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) Errores de “Missing exports” de validateNative
Sección titulada «2) Errores de “Missing exports” de validateNative»Esto es bueno — previene desajustes silenciosos. Cuando vea esto:
Native addon missing exports ... Missing: visibleWidthsignifica que su binario está obsoleto, el nombre de la exportación Rust (o el alias explícito cuando se usa) no coincide con el nombre JS, o la exportación nunca se compiló. Corrija la compilación y la discrepancia de nombres, no debilite la validación.
3) Discrepancia de firma en Rust
Sección titulada «3) Discrepancia de firma en Rust»Manténgalo simple y propio. String, Vec<String> y Uint8Array funcionan. Evite referencias como &str en exportaciones públicas. Si necesita datos estructurados, envuélvalos en structs con #[napi(object)].
4) Errores en benchmarking
Sección titulada «4) Errores en benchmarking»- No compare entradas o asignaciones diferentes.
- Mantenga JS y nativo usando arrays de entrada idénticos.
- Ejecute ambos en el mismo archivo de benchmark para evitar sesgos.
Plantilla de benchmark
Sección titulada «Plantilla 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);});Lista de verificación final
Sección titulada «Lista de verificación final»validateNativepasa (sin exportaciones faltantes).NativeBindingsestá ampliado enpackages/natives/src/<module>/types.tsy el wrapper está re-exportado enpackages/natives/src/index.ts.Object.keys(require(...))incluye su nueva exportación.- Números del benchmark registrados en el PR/notas.
- Sitio de llamada actualizado solo si lo nativo es más rápido o igual.
Regla general
Sección titulada «Regla general»- Si lo nativo es más lento, no cambie. Mantenga la exportación para trabajo futuro, pero la TUI debe permanecer en la ruta más rápida.
- Si lo nativo es más rápido, cambie el sitio de llamada y mantenga el benchmark en su lugar para detectar regresiones.