Os Addons C++ no Node.js permitem carregar código compilado como um módulo .node e expor funções, classes e objetos ao JavaScript. Eles são usados para integrar bibliotecas nativas, acessar APIs do sistema, trabalhar com hardware, executar algoritmos intensivos e reutilizar código existente.
Existem diferentes níveis de integração. O caminho recomendado para compatibilidade é Node-API, uma ABI estável fornecida pelo runtime. Também existem APIs baseadas em V8, libuv e headers internos, mas elas podem exigir mudanças e recompilação quando o Node.js é atualizado.
Neste guia, você aprenderá a organizar o projeto, configurar node-gyp, criar um addon com node-addon-api, compilar, carregar, trabalhar com argumentos, erros, classes, tarefas assíncronas, bibliotecas externas, prebuilds, segurança e testes.
O que é um addon nativo?
Um addon nativo é uma biblioteca dinâmica carregada pelo Node.js. A documentação oficial de addons C++ apresenta conceitos e exemplos. A documentação do node-gyp explica o sistema de build.
Para uma ABI estável, consulte Node-API no Node.js. Para threads JavaScript, veja Worker Threads no Node.js. O artigo de Buffer no Node.js ajuda na troca de bytes.
Quando criar um addon?
- integrar uma biblioteca C ou C++ existente;
- acessar dispositivo ou API específica do sistema;
- usar codec, algoritmo ou banco nativo;
- reduzir custo de uma rotina comprovadamente crítica;
- oferecer bindings de um SDK.
Não use C++ apenas por expectativa de desempenho. Medição, complexidade e custo de distribuição precisam justificar a decisão.
Alternativas
Antes do addon, avalie:
- JavaScript otimizado;
- Worker Threads;
- WebAssembly;
- processo filho;
- serviço separado;
- biblioteca nativa já mantida.
O módulo Child Process no Node.js oferece isolamento maior quando uma falha nativa não deve derrubar a aplicação principal.
Estrutura do projeto
native-addon/
├── binding.gyp
├── package.json
├── src/
│ └── addon.cc
└── index.jspackage.json
{
"name": "native-addon-example",
"version": "1.0.0",
"gypfile": true,
"main": "index.js"
}Fixe versões de dependências e mantenha lockfile.
binding.gyp
{
"targets": [
{
"target_name": "addon",
"sources": ["src/addon.cc"]
}
]
}O target_name determina o nome do binário resultante.
Instalando node-gyp
npm install --save-dev node-gypO ambiente precisa de Python e toolchain C++ compatível com o sistema operacional.
Compilando
npx node-gyp configure
npx node-gyp buildO binário costuma aparecer em build/Release/addon.node.
Carregando o módulo
const addon = require(
'./build/Release/addon.node'
);
console.log(addon.answer());Usando node-addon-api
O pacote node-addon-api oferece wrappers C++ sobre Node-API:
npm install node-addon-apiAjuste o binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": ["src/addon.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
}
]
}Função simples
#include <napi.h>
Napi::Value Answer(
const Napi::CallbackInfo& info
) {
Napi::Env env = info.Env();
return Napi::Number::New(env, 42);
}
Napi::Object Init(
Napi::Env env,
Napi::Object exports
) {
exports.Set(
"answer",
Napi::Function::New(env, Answer)
);
return exports;
}
NODE_API_MODULE(addon, Init)Recebendo argumentos
Napi::Value Add(
const Napi::CallbackInfo& info
) {
Napi::Env env = info.Env();
if (info.Length() < 2 ||
!info[0].IsNumber() ||
!info[1].IsNumber()) {
Napi::TypeError::New(
env,
"Dois números são obrigatórios"
).ThrowAsJavaScriptException();
return env.Null();
}
double a = info[0].As<Napi::Number>();
double b = info[1].As<Napi::Number>();
return Napi::Number::New(env, a + b);
}Validação
Valide tipo, tamanho, faixa e encoding antes de converter. Uma string ou Buffer enorme pode provocar alocações perigosas.
Strings
std::string value = info[0]
.As<Napi::String>()
.Utf8Value();A conversão pode alocar memória. Defina tamanho máximo no JavaScript e no C++.
Buffers
Napi::Buffer<uint8_t> buffer =
info[0].As<Napi::Buffer<uint8_t>>();
uint8_t* data = buffer.Data();
size_t length = buffer.Length();Não mantenha o ponteiro além da vida segura do Buffer sem criar referência ou copiar os bytes.
Retornando Buffer
auto result = Napi::Buffer<uint8_t>::New(
env,
length
);
std::memcpy(
result.Data(),
source,
length
);Memória externa
É possível criar Buffer sobre memória externa com finalizador. Defina ownership: apenas uma parte deve liberar o ponteiro.
Erros
Napi::Error::New(
env,
"Falha nativa"
).ThrowAsJavaScriptException();Depois de lançar, interrompa o caminho normal. Não continue usando valores inválidos.
Exceções C++
O projeto pode habilitar ou desabilitar exceções. Use uma estratégia consistente e converta falhas para erros JavaScript na fronteira.
Classes nativas
Napi::ObjectWrap permite expor uma classe:
class Counter : public Napi::ObjectWrap<Counter> {
public:
static Napi::Function GetClass(Napi::Env env);
Counter(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<Counter>(info), value_(0) {}
private:
int64_t value_;
};O objeto JavaScript fica associado à instância C++.
Destrutor
O destrutor pode liberar memória, mas recursos importantes devem ter método close() explícito. Garbage collection não possui momento previsível.
Operação assíncrona
Não execute trabalho pesado na thread JavaScript. Use Napi::AsyncWorker ou APIs de async work:
class HashWorker : public Napi::AsyncWorker {
public:
HashWorker(
Napi::Function& callback,
std::vector<uint8_t> input
) : Napi::AsyncWorker(callback),
input_(std::move(input)) {}
void Execute() override {
result_ = calculateHash(input_);
}
void OnOK() override {
Callback().Call({
Env().Null(),
Napi::String::New(Env(), result_)
});
}
};Execute()
Execute roda fora da thread do JavaScript. Não crie objetos Napi nessa fase.
OnOK e OnError
Esses métodos voltam ao contexto apropriado. Crie valores JavaScript e chame callbacks apenas ali.
Promises
Também é possível expor uma Promise por meio de Deferred. Resolva ou rejeite na thread correta.
Threads externas
Uma biblioteca nativa pode chamar um callback em sua própria thread. Use thread-safe functions, nunca chame JavaScript diretamente.
Fila limitada
Defina limite da thread-safe function. Uma câmera, socket ou sensor pode produzir eventos mais rápido que o JavaScript consome.
AsyncResource
Para contexto e observabilidade, consulte AsyncResource no Node.js.
Bibliotecas externas
Adicione include_dirs, libraries e condições no binding.gyp. Evite caminhos absolutos específicos da máquina.
Condições por sistema
"conditions": [
["OS=='win'", {
"defines": ["PLATFORM_WINDOWS"]
}],
["OS=='linux'", {
"defines": ["PLATFORM_LINUX"]
}]
]Arquiteturas
x64, arm64 e outras arquiteturas exigem binários próprios. Não copie um artefato entre plataformas.
glibc e musl
Linux baseado em Alpine usa musl, enquanto muitas distribuições usam glibc. Disponibilize artefatos compatíveis ou compile no destino.
prebuilds
Ferramentas de prebuild podem gerar binários para várias plataformas e publicar junto ao pacote.
Instalação sem compilador
O instalador tenta baixar o binário correspondente. Se não houver, pode compilar localmente como fallback.
Compatibilidade ABI
Addons baseados diretamente em V8 possuem compatibilidade mais restrita. Node-API oferece estabilidade maior entre versões.
Electron
Electron pode usar headers e ABI próprios. Node-API reduz diferenças, mas ainda teste com as versões suportadas.
Single Executable
Addons .node normalmente continuam como arquivos externos. Planeje extração e paths em uma Single Executable no Node.js.
Segurança de memória
Falhas nativas podem causar crash ou vulnerabilidades:
- buffer overflow;
- use-after-free;
- double free;
- integer overflow;
- race condition;
- leitura fora de limite.
Sanitizers
Use AddressSanitizer e UndefinedBehaviorSanitizer em builds de teste. ThreadSanitizer ajuda em condições de corrida, embora exija configuração compatível.
Fuzzing
Parsers de arquivos, protocolos e imagens se beneficiam de fuzzing com entradas aleatórias e corpus real.
Dependências nativas
Mantenha bibliotecas atualizadas e acompanhe CVEs. Um pacote npm pode incluir binário vulnerável mesmo quando o JavaScript é simples.
Cadeia de suprimentos
Proteja:
- runners de build;
- tokens de publicação;
- artefatos;
- checksums;
- assinaturas;
- dependências de compilação.
Performance
Atravessar a fronteira JavaScript-C++ possui custo. Agrupe operações quando possível e evite chamadas nativas minúsculas em loops intensos.
Zero-copy
Compartilhar memória pode reduzir cópias, mas aumenta complexidade de ownership e duração. Prefira segurança até medir necessidade real.
Event loop
Um addon síncrono de 500 ms bloqueia todas as requisições da thread. Use async worker ou Worker Thread.
Shutdown
Cancele trabalho, pare threads externas, libere callbacks e feche handles. Veja Graceful Shutdown no Node.js.
Testes
Cubra:
- tipos inválidos;
- valores limites;
- buffers vazios e grandes;
- erros da biblioteca;
- tarefas assíncronas;
- cancelamento;
- garbage collection;
- shutdown;
- plataformas e arquiteturas;
- versões do Node.js.
CI
Execute matriz de sistemas, arquiteturas e versões LTS. Teste o pacote instalado a partir do artefato, não apenas o diretório de build.
Erros comuns
- Usar V8 diretamente sem necessidade: compatibilidade fica frágil.
- Bloquear o event loop: a aplicação perde capacidade.
- Guardar ponteiro de Buffer: memória pode ser coletada.
- Chamar JS de thread externa: ocorre comportamento indefinido.
- Sem prebuilds: usuários precisam de toolchain.
- Um binário para todos: plataformas falham.
- Confiar no destrutor: recursos ficam abertos.
Boas práticas
- Prefira Node-API.
- Valide entradas.
- Defina ownership.
- Use trabalho assíncrono.
- Limite filas.
- Teste com sanitizers.
- Crie prebuilds.
- Proteja o pipeline.
- Exponha close explícito.
- Meça a fronteira nativa.
Conclusão
Os Addons C++ no Node.js conectam JavaScript a bibliotecas e recursos nativos, permitindo desempenho e integração que não seriam simples no runtime puro.
A escolha recomendada é Node-API, combinada com node-addon-api para ergonomia. O sucesso depende de memória segura, trabalho fora do event loop, builds por plataforma, prebuilds, testes com sanitizers e shutdown explícito. Sem esses cuidados, um pequeno erro nativo pode comprometer todo o processo.



