- Início
- Documentation
- Nativos
- Portando para pi-natives (N-API) — Notas de Campo
Portando para pi-natives (N-API) — Notas de Campo
Este é um guia prático para mover caminhos críticos (hot paths) para crates/pi-natives e conectá-los através das bindings JS. Ele existe para evitar que os mesmos erros aconteçam duas vezes.
Quando portar
Seção intitulada “Quando portar”Porte quando qualquer uma destas condições for verdadeira:
- O caminho crítico executa em loops de renderização, atualizações rápidas de UI ou lotes grandes.
- Alocações JS dominam (rotatividade de strings, backtracking de regex, arrays grandes).
- Você já tem uma baseline JS e pode comparar ambas as versões lado a lado.
- O trabalho é limitado por CPU ou I/O bloqueante que pode rodar no thread pool do libuv.
- O trabalho é I/O assíncrono que pode rodar no runtime do Tokio (ex.: execução de shell).
Evite portar código que dependa de estado exclusivo do JS ou imports dinâmicos. Exports N-API devem ser puros, dados-entram/dados-saem. Trabalhos de longa duração devem passar por task::blocking (limitado por CPU/I/O bloqueante) ou task::future (I/O assíncrono) com cancelamento.
Anatomia de um export nativo
Seção intitulada “Anatomia de um export nativo”Lado Rust:
- A implementação fica em
crates/pi-natives/src/<module>.rs. Se você adicionar um novo módulo, registre-o emcrates/pi-natives/src/lib.rs. - Exporte com
#[napi]; exports em snake_case são convertidos para camelCase automaticamente. Usejs_nameexplícito apenas para aliases verdadeiros/nomes não-padrão. Use#[napi(object)]para structs. - Use
task::blocking(tag, cancel_token, work)(vejacrates/pi-natives/src/task.rs) para trabalho limitado por CPU ou bloqueante. Usetask::future(env, tag, work)para trabalho assíncrono que precisa do Tokio (ex.: sessões de shell). Passe umCancelTokenquando exportimeoutMsouAbortSignal.
Lado JS:
packages/natives/src/bindings.tscontém a interface baseNativeBindings.packages/natives/src/<module>/types.tsdefine tipos TS e estendeNativeBindingsvia declaration merging.packages/natives/src/native.tsimporta cada arquivo<module>/types.tspara ativar as declarações.packages/natives/src/<module>/index.tsencapsula a bindingnativedepackages/natives/src/native.ts.packages/natives/src/native.tscarrega o addon evalidateNativegarante os exports obrigatórios.packages/natives/src/index.tsre-exporta o wrapper para consumidores empackages/*.
Checklist de portabilidade
Seção intitulada “Checklist de portabilidade”- Adicione a implementação Rust
- Coloque a lógica principal em uma função Rust simples.
- Se for um novo módulo, adicione-o em
crates/pi-natives/src/lib.rs. - Exponha com
#[napi]para que o mapeamento padrão snake_case -> camelCase permaneça consistente. - Mantenha as assinaturas owned e simples:
String,Vec<String>,Uint8Array, ouEither<JsString, Uint8Array>para inputs grandes de string/bytes. - Para trabalho limitado por CPU ou bloqueante, use
task::blocking; para trabalho assíncrono, usetask::future. Passe umCancelTokene chameheartbeat()dentro de loops longos.
- Conecte as bindings JS
- Adicione os tipos e a extensão de
NativeBindingsempackages/natives/src/<module>/types.ts. - Importe
./<module>/typesempackages/natives/src/native.tspara acionar o declaration merging. - Adicione um wrapper em
packages/natives/src/<module>/index.tsque chamenative. - Re-exporte de
packages/natives/src/index.ts.
- Atualize a validação nativa
- Adicione
checkFn("newExport")emvalidateNative(packages/natives/src/native.ts).
- Adicione benchmarks
- Coloque benchmarks junto ao pacote proprietário (
packages/tui/bench,packages/natives/bench, oupackages/coding-agent/bench). - Inclua uma baseline JS e a versão nativa na mesma execução.
- Use
Bun.nanoseconds()e uma contagem fixa de iterações. - Mantenha os inputs do benchmark pequenos e realistas (dados reais vistos no caminho crítico).
- Compile o binário nativo
bun --cwd=packages/natives run build- Use
bun --cwd=packages/natives run builde definaPI_DEV=1se quiser diagnósticos do loader durante os testes.
- Execute o benchmark
bun run packages/<pkg>/bench/<bench>.ts(oubun --cwd=packages/natives run bench)
- Decida sobre o uso
- Se o nativo for mais lento, mantenha o JS e deixe o export nativo sem uso.
- Se o nativo for mais rápido, mude os pontos de chamada para o wrapper nativo.
Pontos problemáticos e como evitá-los
Seção intitulada “Pontos problemáticos e como evitá-los”1) pi_natives.node desatualizado impede novos exports
Seção intitulada “1) pi_natives.node desatualizado impede novos exports”O loader prefere o binário com tag de plataforma em packages/natives/native (pi_natives.<platform>-<arch>.node). PI_DEV=1 agora apenas habilita diagnósticos do loader; não altera mais para um nome de arquivo de addon de desenvolvimento separado. Há também um fallback pi_natives.node. Binários compilados são extraídos para ~/.xcsh/natives/<version>/pi_natives.<platform>-<arch>.node. Se qualquer um destes estiver desatualizado, os exports não serão atualizados.
Correção: remova o arquivo desatualizado antes de recompilar.
rm packages/natives/native/pi_natives.linux-x64.noderm packages/natives/native/pi_natives.nodebun --cwd=packages/natives run buildSe você está executando um binário compilado, delete o diretório de addon em cache:
rm -rf ~/.xcsh/natives/<version>Em seguida, verifique se o export existe no binário:
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) Erros de “Missing exports” do validateNative
Seção intitulada “2) Erros de “Missing exports” do validateNative”Isso é bom — previne incompatibilidades silenciosas. Quando você vê isto:
Native addon missing exports ... Missing: visibleWidthsignifica que seu binário está desatualizado, o nome do export Rust (ou alias explícito quando usado) não corresponde ao nome JS, ou o export nunca foi compilado. Corrija o build e a incompatibilidade de nomes, não enfraqueça a validação.
3) Incompatibilidade de assinatura Rust
Seção intitulada “3) Incompatibilidade de assinatura Rust”Mantenha simples e owned. String, Vec<String> e Uint8Array funcionam. Evite referências como &str em exports públicos. Se precisar de dados estruturados, encapsule em structs com #[napi(object)].
4) Erros de benchmarking
Seção intitulada “4) Erros de benchmarking”- Não compare inputs ou alocações diferentes.
- Mantenha JS e nativo usando arrays de input idênticos.
- Execute ambos no mesmo arquivo de benchmark para evitar distorções.
Template de benchmark
Seção intitulada “Template 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);});Checklist de verificação
Seção intitulada “Checklist de verificação”validateNativepassa (sem exports faltando).NativeBindingsestá estendido empackages/natives/src/<module>/types.tse o wrapper está re-exportado empackages/natives/src/index.ts.Object.keys(require(...))inclui seu novo export.- Números de benchmark registrados no PR/notas.
- Ponto de chamada atualizado apenas se o nativo for mais rápido ou equivalente.
Regra geral
Seção intitulada “Regra geral”- Se o nativo for mais lento, não mude. Mantenha o export para trabalho futuro, mas o TUI deve permanecer no caminho mais rápido.
- Se o nativo for mais rápido, mude o ponto de chamada e mantenha o benchmark em vigor para detectar regressões.