Interoperabilidade com Node.js: Neon e N-API

1. Introdução à Interoperabilidade Rust ↔ Node.js

A integração entre Rust e Node.js permite combinar a produtividade do ecossistema JavaScript com o desempenho e a segurança de memória que Rust oferece. Enquanto Node.js é excelente para I/O assíncrono e desenvolvimento rápido, aplicações que exigem processamento intensivo (criptografia, compressão, processamento de imagens, jogos) podem se beneficiar enormemente de módulos nativos escritos em Rust.

O ecossistema oferece três abordagens principais: Neon (abstrações de alto nível), N-API raw (controle direto via bindings C) e napi-rs (geração automática de bindings via macros). Casos de uso típicos incluem módulos nativos para npm, processamento de dados em tempo real e bindings para bibliotecas C existentes.

2. Configuração do Ambiente e Projeto Base

Para começar, instale as dependências necessárias:

# Node.js e npm (já instalados)
node --version
npm --version

# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Neon CLI
npm install -g neon-cli

# napi-rs CLI
npm install -g @napi-rs/cli

Criando um projeto Neon:

neon new meu-modulo-neon
cd meu-modulo-neon
npm install

A estrutura gerada inclui:
- native/src/lib.rs — código Rust
- lib/index.js — wrapper JavaScript
- Cargo.toml — dependências Rust

Criando um projeto napi-rs:

napi new meu-modulo-napi
cd meu-modulo-napi
npm install

O Cargo.toml gerado já inclui napi e napi-derive como dependências.

3. Neon: Abstrações de Alto Nível para Node.js

Neon fornece uma API ergonômica para expor funções Rust ao JavaScript. Veja um exemplo básico:

use neon::prelude::*;

fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
    let name = cx.argument::<JsString>(0)?.value(&mut cx);
    Ok(cx.string(format!("Olá, {}!", name)))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("hello", hello)?;
    Ok(())
}

Manipulação de tipos JavaScript:

fn soma_array(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let arr = cx.argument::<JsArray>(0)?;
    let vec: Vec<Handle<JsValue>> = arr.to_vec(&mut cx)?;

    let mut total = 0.0;
    for valor in vec {
        if let Ok(num) = valor.downcast::<JsNumber>(&mut cx) {
            total += num.value(&mut cx);
        }
    }
    Ok(cx.number(total))
}

Operações assíncronas com Task:

struct ProcessamentoTask {
    dados: Vec<u8>,
}

impl Task for ProcessamentoTask {
    type Output = Vec<u8>;
    type Error = String;
    type JsEvent = JsBuffer;

    fn perform(&self) -> Result<Self::Output, Self::Error> {
        // Processamento pesado em thread separada
        Ok(self.dados.iter().map(|b| b ^ 0xFF).collect())
    }

    fn complete(
        self,
        mut cx: TaskContext,
        result: Result<Self::Output, Self::Error>,
    ) -> JsResult<Self::JsEvent> {
        let buffer = cx.buffer(result.unwrap().len() as u32);
        // Preencher buffer...
        Ok(buffer)
    }
}

4. N-API via napi-rs: Controle e Performance

napi-rs oferece uma abordagem mais direta com macros poderosas:

use napi_derive::napi;

#[napi]
fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[napi(object)]
struct Pessoa {
    pub nome: String,
    pub idade: u32,
}

#[napi]
fn criar_pessoa(nome: String, idade: u32) -> Pessoa {
    Pessoa { nome, idade }
}

Trabalhando com buffers e arrays tipados:

#[napi]
fn processar_buffer(buffer: Buffer) -> Buffer {
    let mut dados = buffer.to_vec();
    for byte in dados.iter_mut() {
        *byte = byte.wrapping_add(1);
    }
    Buffer::from(dados)
}

#[napi]
fn media_array(arr: Float64Array) -> f64 {
    let soma: f64 = arr.iter().sum();
    soma / arr.len() as f64
}

Exportando classes:

#[napi]
struct Contador {
    valor: u32,
}

#[napi]
impl Contador {
    #[napi(constructor)]
    fn new(inicial: u32) -> Self {
        Contador { valor: inicial }
    }

    #[napi]
    fn incrementar(&mut self) -> u32 {
        self.valor += 1;
        self.valor
    }

    #[napi(getter)]
    fn valor_atual(&self) -> u32 {
        self.valor
    }
}

5. Comunicação Assíncrona e Threads

Usando tokio com napi-rs:

use napi_derive::napi;
use tokio::time::{sleep, Duration};

#[napi(ts_return_type = "Promise<number>")]
async fn calcular_hash_async(dados: Buffer) -> napi::Result<u64> {
    // Simula processamento pesado
    sleep(Duration::from_secs(2)).await;

    let hash: u64 = dados.iter()
        .fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64));

    Ok(hash)
}

Sincronização com Arc e Mutex:

use std::sync::{Arc, Mutex};

#[napi]
struct Cache {
    dados: Arc<Mutex<Vec<String>>>,
}

#[napi]
impl Cache {
    #[napi(constructor)]
    fn new() -> Self {
        Cache {
            dados: Arc::new(Mutex::new(Vec::new())),
        }
    }

    #[napi]
    fn adicionar(&self, item: String) {
        let mut dados = self.dados.lock().unwrap();
        dados.push(item);
    }

    #[napi]
    fn listar(&self) -> Vec<String> {
        self.dados.lock().unwrap().clone()
    }
}

6. Tratamento de Erros e Validação de Limites

Convertendo Result em exceções JavaScript:

#[napi]
fn dividir(a: f64, b: f64) -> napi::Result<f64> {
    if b == 0.0 {
        return Err(napi::Error::from_reason("Divisão por zero não permitida"));
    }
    Ok(a / b)
}

Prevenindo panics:

use std::panic::catch_unwind;

#[napi]
fn operacao_segura() -> napi::Result<i32> {
    let resultado = catch_unwind(|| {
        // Código que pode panicar
        let vetor = vec![1, 2, 3];
        vetor[5] // Isso panicaria
    });

    match resultado {
        Ok(valor) => Ok(valor),
        Err(_) => Err(napi::Error::from_reason("Operação falhou internamente")),
    }
}

Testes de integração:

// tests/integracao.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fibonacci() {
        assert_eq!(fibonacci(0), 0);
        assert_eq!(fibonacci(10), 55);
    }
}
// test/index.test.js
const addon = require('../index.node');

test('fibonacci de 10 deve ser 55', () => {
    expect(addon.fibonacci(10)).toBe(55);
});

7. Publicação e Distribuição do Módulo Nativo

Compilação para produção:

# Neon
neon build --release

# napi-rs
napi build --release

Scripts de build no package.json:

{
  "scripts": {
    "build": "napi build --release",
    "build:debug": "napi build",
    "prepublishOnly": "npm run build"
  },
  "napi": {
    "name": "meu-modulo",
    "triples": {
      "defaults": true,
      "additional": ["aarch64-apple-darwin", "x86_64-unknown-linux-musl"]
    }
  }
}

CI/CD com GitHub Actions:

name: Build e Teste
on: [push, pull_request]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - uses: dtolnay/rust-toolchain@stable
      - run: npm install
      - run: npm run build
      - run: npm test

8. Comparação Neon vs napi-rs e Boas Práticas

Característica Neon napi-rs
Curva de aprendizado Baixa Média
Performance Boa Excelente
Maturidade Maduro Muito ativo
Documentação Extensa Boa
Suporte a async Task tokio nativo
Tamanho do binário Maior Menor
Controle fino Médio Alto

Quando escolher Neon:
- Prototipagem rápida e projetos pequenos
- Equipes com pouca experiência em Rust
- Necessidade de abstrações simples e seguras

Quando escolher napi-rs:
- Projetos de produção com requisitos de performance
- Integração com ecossistema Rust existente (tokio, serde)
- Necessidade de controle fino sobre memória e tipos

Padrões de design recomendados:
1. Separe a lógica de negócio Rust pura da camada FFI
2. Use tipos Rust nativos internamente e converta apenas na fronteira
3. Documente claramente as exceções e limites de cada função
4. Implemente testes unitários em Rust e testes de integração em Node.js
5. Considere usar cargo bench para benchmarks de performance

A interoperabilidade entre Rust e Node.js abre possibilidades enormes para aplicações que exigem o melhor dos dois mundos: a produtividade do ecossistema JavaScript com a performance e segurança do Rust.

Referências