Skip to content

Addons em C++

Addons são objetos compartilhados com ligação dinâmica escritos em C++. A função require() pode carregar addons como módulos normais do Node.js. Addons fornecem uma interface entre o JavaScript e as bibliotecas C/C++.

Existem três opções para implementar addons:

A menos que haja necessidade de acesso direto a funcionalidades que não são expostas pela Node-API, use Node-API. Consulte Addons C/C++ com Node-API para obter mais informações sobre Node-API.

Quando não se usa Node-API, a implementação de addons torna-se mais complexa, exigindo conhecimento de vários componentes e APIs:

  • V8: a biblioteca C++ que o Node.js usa para fornecer a implementação JavaScript. Ela fornece os mecanismos para criar objetos, chamar funções, etc. A API do V8 é documentada principalmente no arquivo de cabeçalho v8.h (deps/v8/include/v8.h na árvore de origem do Node.js) e também está disponível online.
  • libuv: A biblioteca C que implementa o loop de eventos do Node.js, suas threads de trabalho e todos os comportamentos assíncronos da plataforma. Ela também serve como uma biblioteca de abstração de plataforma cruzada, fornecendo acesso fácil e semelhante a POSIX em todos os principais sistemas operacionais a muitas tarefas comuns do sistema, como interagir com o sistema de arquivos, sockets, timers e eventos do sistema. A libuv também fornece uma abstração de threading semelhante às threads POSIX para addons assíncronos mais sofisticados que precisam ir além do loop de eventos padrão. Os autores de addons devem evitar bloquear o loop de eventos com E/S ou outras tarefas demoradas, descarregando o trabalho via libuv para operações de sistema não bloqueadoras, threads de trabalho ou um uso personalizado de threads libuv.
  • Bibliotecas internas do Node.js: O próprio Node.js exporta APIs C++ que os addons podem usar, a mais importante das quais é a classe node::ObjectWrap.
  • Outras bibliotecas vinculadas estaticamente (incluindo OpenSSL): Essas outras bibliotecas estão localizadas no diretório deps/ na árvore de origem do Node.js. Apenas os símbolos libuv, OpenSSL, V8 e zlib são propositalmente reexportados pelo Node.js e podem ser usados ​​em vários níveis por addons. Consulte Vinculando a bibliotecas incluídas com Node.js para obter informações adicionais.

Todos os exemplos a seguir estão disponíveis para download e podem ser usados como ponto de partida para um addon.

Olá mundo

Este exemplo de "Olá mundo" é um addon simples, escrito em C++, que é o equivalente ao seguinte código JavaScript:

js
module.exports.hello = () => 'world'

Primeiro, crie o arquivo hello.cc:

C++
// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world", NewStringType::kNormal).ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo

Todos os addons do Node.js devem exportar uma função de inicialização seguindo o padrão:

C++
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

Não há ponto e vírgula após NODE_MODULE, pois não é uma função (consulte node.h).

O module_name deve corresponder ao nome do arquivo binário final (excluindo o sufixo .node).

No exemplo hello.cc, então, a função de inicialização é Initialize e o nome do módulo addon é addon.

Ao construir addons com node-gyp, usar a macro NODE_GYP_MODULE_NAME como o primeiro parâmetro de NODE_MODULE() garantirá que o nome do binário final seja passado para NODE_MODULE().

Addons definidos com NODE_MODULE() não podem ser carregados em vários contextos ou várias threads ao mesmo tempo.

Addons com reconhecimento de contexto

Existem ambientes em que os addons do Node.js podem precisar ser carregados várias vezes em vários contextos. Por exemplo, o runtime Electron executa várias instâncias do Node.js em um único processo. Cada instância terá seu próprio cache require() e, portanto, cada instância precisará de um addon nativo para se comportar corretamente quando carregado via require(). Isso significa que o addon deve suportar várias inicializações.

Um addon com reconhecimento de contexto pode ser construído usando a macro NODE_MODULE_INITIALIZER, que se expande para o nome de uma função que o Node.js espera encontrar ao carregar um addon. Um addon pode, portanto, ser inicializado como no exemplo a seguir:

C++
using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Execute as etapas de inicialização do addon aqui. */
}

Outra opção é usar a macro NODE_MODULE_INIT(), que também construirá um addon com reconhecimento de contexto. Ao contrário de NODE_MODULE(), que é usado para construir um addon em torno de uma determinada função de inicialização do addon, NODE_MODULE_INIT() serve como a declaração de tal inicializador a ser seguido por um corpo de função.

As três variáveis a seguir podem ser usadas dentro do corpo da função após uma invocação de NODE_MODULE_INIT():

  • Local\<Object\> exports,
  • Local\<Value\> module e
  • Local\<Context\> context

A construção de um addon com reconhecimento de contexto requer um gerenciamento cuidadoso de dados estáticos globais para garantir a estabilidade e a correção. Como o addon pode ser carregado várias vezes, potencialmente até mesmo de threads diferentes, todos os dados estáticos globais armazenados no addon devem ser protegidos adequadamente e não devem conter nenhuma referência persistente a objetos JavaScript. A razão para isso é que os objetos JavaScript são válidos apenas em um contexto e provavelmente causarão uma falha quando acessados ​​do contexto errado ou de uma thread diferente daquela em que foram criados.

O addon com reconhecimento de contexto pode ser estruturado para evitar dados estáticos globais, executando as seguintes etapas:

  • Defina uma classe que manterá os dados por instância de addon e que possui um membro estático da forma
  • Alocar na heap uma instância dessa classe no inicializador do addon. Isso pode ser feito usando a palavra-chave new.
  • Chame node::AddEnvironmentCleanupHook(), passando-lhe a instância criada acima e um ponteiro para DeleteInstance(). Isso garantirá que a instância seja excluída quando o ambiente for destruído.
  • Armazene a instância da classe em um v8::External e
  • Passe o v8::External para todos os métodos expostos ao JavaScript, passando-o para v8::FunctionTemplate::New() ou v8::Function::New(), que cria as funções JavaScript com suporte nativo. O terceiro parâmetro de v8::FunctionTemplate::New() ou v8::Function::New() aceita o v8::External e o torna disponível no callback nativo usando o método v8::FunctionCallbackInfo::Data().

Isso garantirá que os dados por instância de addon alcancem cada ligação que pode ser chamada do JavaScript. Os dados por instância de addon também devem ser passados ​​para quaisquer callbacks assíncronos que o addon possa criar.

O exemplo a seguir ilustra a implementação de um addon com reconhecimento de contexto:

C++
#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // Garanta que esses dados por instância de addon sejam excluídos na limpeza do ambiente.
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // Dados por addon.
  int call_count;

  static void DeleteInstance(void* data) {
    delete static_cast<AddonData*>(data);
  }
};

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // Recupere os dados por instância de addon.
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// Inicialize este addon para que ele reconheça o contexto.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // Crie uma nova instância de `AddonData` para esta instância do addon e
  // vincule seu ciclo de vida ao do ambiente Node.js.
  AddonData* data = new AddonData(isolate);

  // Envolva os dados em um `v8::External` para que possamos passá-lo para o método que
  // expomos.
  Local<External> external = External::New(isolate, data);

  // Exponha o método `Method` para JavaScript e certifique-se de que ele receba o
  // dados por instância de addon que criamos acima, passando `external` como o
  // terceiro parâmetro para o construtor `FunctionTemplate`.
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Suporte a Workers

[Histórico]

VersãoMudanças
v14.8.0, v12.19.0Hooks de limpeza agora podem ser assíncronos.

Para ser carregado a partir de múltiplos ambientes Node.js, como uma thread principal e uma thread Worker, um add-on precisa:

  • Ser um addon Node-API, ou
  • Ser declarado como context-aware usando NODE_MODULE_INIT() conforme descrito acima

Para dar suporte a threads Worker, os addons precisam limpar quaisquer recursos que possam ter alocado quando tal thread é encerrada. Isso pode ser alcançado através do uso da função AddEnvironmentCleanupHook():

C++
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
                               void (*fun)(void* arg),
                               void* arg);

Essa função adiciona um hook que será executado antes que uma dada instância do Node.js seja desligada. Se necessário, tais hooks podem ser removidos antes de serem executados usando RemoveEnvironmentCleanupHook(), que tem a mesma assinatura. Callbacks são executadas na ordem LIFO (último a entrar, primeiro a sair).

Se necessário, existe um par adicional de sobrecargas AddEnvironmentCleanupHook() e RemoveEnvironmentCleanupHook(), onde o hook de limpeza recebe uma função de callback. Isso pode ser usado para desligar recursos assíncronos, como quaisquer handles libuv registrados pelo addon.

O seguinte addon.cc usa AddEnvironmentCleanupHook:

C++
// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>

using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;

// Observação: Em uma aplicação real, não confie em dados estáticos/globais.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;

static void cleanup_cb1(void* arg) {
  Isolate* isolate = static_cast<Isolate*>(arg);
  HandleScope scope(isolate);
  Local<Object> obj = Object::New(isolate);
  assert(!obj.IsEmpty());  // assegurar que a VM ainda está ativa
  assert(obj->IsObject());
  cleanup_cb1_called++;
}

static void cleanup_cb2(void* arg) {
  assert(arg == static_cast<void*>(cookie));
  cleanup_cb2_called++;
}

static void sanity_check(void*) {
  assert(cleanup_cb1_called == 1);
  assert(cleanup_cb2_called == 1);
}

// Inicialize este addon para ser context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
  AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
  AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}

Teste em JavaScript executando:

js
// test.js
require('./build/Release/addon')

Construindo

Depois que o código fonte é escrito, ele deve ser compilado no arquivo binário addon.node. Para fazer isso, crie um arquivo chamado binding.gyp no nível superior do projeto, descrevendo a configuração de construção do módulo usando um formato semelhante a JSON. Este arquivo é usado por node-gyp, uma ferramenta escrita especificamente para compilar complementos Node.js.

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["hello.cc"]
    }
  ]
}

Uma versão do utilitário node-gyp é agrupada e distribuída com o Node.js como parte do npm. Esta versão não está diretamente disponível para os desenvolvedores usarem e destina-se apenas a dar suporte à capacidade de usar o comando npm install para compilar e instalar complementos. Os desenvolvedores que desejam usar o node-gyp diretamente podem instalá-lo usando o comando npm install -g node-gyp. Consulte as instruções de instalação do node-gyp para obter mais informações, incluindo os requisitos específicos da plataforma.

Depois que o arquivo binding.gyp for criado, use node-gyp configure para gerar os arquivos de construção de projeto apropriados para a plataforma atual. Isso irá gerar um Makefile (em plataformas Unix) ou um arquivo vcxproj (no Windows) no diretório build/.

Em seguida, invoque o comando node-gyp build para gerar o arquivo compilado addon.node. Isso será colocado no diretório build/Release/.

Ao usar npm install para instalar um complemento Node.js, o npm usa sua própria versão agrupada de node-gyp para executar o mesmo conjunto de ações, gerando uma versão compilada do complemento para a plataforma do usuário sob demanda.

Uma vez construído, o complemento binário pode ser usado de dentro do Node.js apontando require() para o módulo addon.node construído:

js
// hello.js
const addon = require('./build/Release/addon')

console.log(addon.hello())
// Imprime: 'mundo'

Como o caminho exato para o binário do complemento compilado pode variar dependendo de como ele é compilado (ou seja, às vezes pode estar em ./build/Debug/), os complementos podem usar o pacote bindings para carregar o módulo compilado.

Embora a implementação do pacote bindings seja mais sofisticada em como localiza os módulos de complemento, ela está essencialmente usando um padrão try…catch semelhante a:

js
try {
  return require('./build/Release/addon.node')
} catch (err) {
  return require('./build/Debug/addon.node')
}

Vinculando a bibliotecas incluídas no Node.js

O Node.js usa bibliotecas vinculadas estaticamente, como V8, libuv e OpenSSL. Todos os addons são obrigados a se vincular ao V8 e também podem se vincular a qualquer uma das outras dependências. Normalmente, isso é tão simples quanto incluir as instruções #include \<...\> apropriadas (por exemplo, #include \<v8.h\>) e o node-gyp localizará os cabeçalhos apropriados automaticamente. No entanto, existem algumas ressalvas a serem observadas:

  • Quando o node-gyp é executado, ele detecta a versão específica do Node.js e baixa o tarball da fonte completa ou apenas os cabeçalhos. Se a fonte completa for baixada, os addons terão acesso completo ao conjunto completo de dependências do Node.js. No entanto, se apenas os cabeçalhos do Node.js forem baixados, apenas os símbolos exportados pelo Node.js estarão disponíveis.
  • O node-gyp pode ser executado usando o sinalizador --nodedir apontando para uma imagem de origem local do Node.js. Usando esta opção, o addon terá acesso ao conjunto completo de dependências.

Carregando addons usando require()

A extensão do nome do arquivo do binário do addon compilado é .node (em vez de .dll ou .so). A função require() é escrita para procurar arquivos com a extensão de arquivo .node e inicializá-los como bibliotecas vinculadas dinamicamente.

Ao chamar require(), a extensão .node geralmente pode ser omitida e o Node.js ainda encontrará e inicializará o addon. Uma ressalva, no entanto, é que o Node.js primeiro tentará localizar e carregar módulos ou arquivos JavaScript que compartilham o mesmo nome base. Por exemplo, se houver um arquivo addon.js no mesmo diretório do binário addon.node, então require('addon') dará precedência ao arquivo addon.js e o carregará em vez disso.

Abstrações nativas para Node.js

Cada um dos exemplos ilustrados neste documento usa diretamente as APIs Node.js e V8 para implementar addons. A API V8 pode ter sido alterada drasticamente de uma versão V8 para a próxima (e de uma versão principal do Node.js para a próxima). A cada alteração, os addons podem precisar ser atualizados e recompilados para continuar funcionando. O cronograma de lançamento do Node.js foi projetado para minimizar a frequência e o impacto dessas alterações, mas há pouco que o Node.js possa fazer para garantir a estabilidade das APIs V8.

As Abstrações Nativas para Node.js (ou nan) fornecem um conjunto de ferramentas que os desenvolvedores de addons são recomendados a usar para manter a compatibilidade entre versões passadas e futuras do V8 e Node.js. Veja os exemplos do nan para uma ilustração de como ele pode ser usado.

Node-API

[Stable: 2 - Stable]

Stable: 2 Estabilidade: 2 - Estável

Node-API é uma API para construir addons nativos. Ela é independente do ambiente de execução JavaScript subjacente (por exemplo, V8) e é mantida como parte do Node.js em si. Esta API será de Interface Binária de Aplicativo (ABI) estável entre as versões do Node.js. Ela tem como objetivo isolar os addons das mudanças no mecanismo JavaScript subjacente e permitir que os módulos compilados para uma versão sejam executados em versões posteriores do Node.js sem recompilação. Os addons são construídos/empacotados com a mesma abordagem/ferramentas descritas neste documento (node-gyp, etc.). A única diferença é o conjunto de APIs que são usadas pelo código nativo. Em vez de usar as APIs V8 ou Abstrações Nativas para Node.js, as funções disponíveis no Node-API são usadas.

Criar e manter um addon que se beneficia da estabilidade ABI fornecida pelo Node-API traz consigo certas considerações de implementação.

Para usar o Node-API no exemplo "Olá mundo" acima, substitua o conteúdo de hello.cc pelo seguinte. Todas as outras instruções permanecem as mesmas.

C++
// hello.cc usando Node-API
#include <node_api.h>

namespace demo {

napi_value Method(napi_env env, napi_callback_info args) {
  napi_value greeting;
  napi_status status;

  status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
  if (status != napi_ok) return nullptr;
  return greeting;
}

napi_value init(napi_env env, napi_value exports) {
  napi_status status;
  napi_value fn;

  status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
  if (status != napi_ok) return nullptr;

  status = napi_set_named_property(env, exports, "hello", fn);
  if (status != napi_ok) return nullptr;
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo

As funções disponíveis e como usá-las são documentadas em Addons C/C++ com Node-API.

Exemplos de Addons

A seguir, estão alguns exemplos de addons com o objetivo de ajudar desenvolvedores a começar. Os exemplos usam as APIs V8. Consulte a referência V8 online para obter ajuda com as diversas chamadas V8, e o Guia do Incorporador do V8 para uma explicação de vários conceitos utilizados, como handles, scopes, templates de funções, etc.

Cada um desses exemplos usa o seguinte arquivo binding.gyp:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc"]
    }
  ]
}

Nos casos em que há mais de um arquivo .cc, basta adicionar o nome do arquivo adicional ao array sources:

json
"sources": ["addon.cc", "myexample.cc"]

Uma vez que o arquivo binding.gyp está pronto, os addons de exemplo podem ser configurados e construídos usando node-gyp:

bash
node-gyp configure build

Argumentos de função

Os addons normalmente expõem objetos e funções que podem ser acessadas a partir de JavaScript em execução dentro do Node.js. Quando as funções são invocadas a partir de JavaScript, os argumentos de entrada e o valor de retorno devem ser mapeados de e para o código C/C++.

O exemplo a seguir ilustra como ler os argumentos de função passados de JavaScript e como retornar um resultado:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// Esta é a implementação do método "add"
// Os argumentos de entrada são passados usando o
// struct const FunctionCallbackInfo<Value>& args
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Verifica o número de argumentos passados.
  if (args.Length() < 2) {
    // Lança um erro que é passado de volta para JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Número incorreto de argumentos").ToLocalChecked()));
    return;
  }

  // Verifica os tipos de argumentos
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Argumentos incorretos").ToLocalChecked()));
    return;
  }

  // Realiza a operação
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Define o valor de retorno (usando o passado em
  // FunctionCallbackInfo<Value>&)
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

Uma vez compilado, o addon de exemplo pode ser requerido e usado de dentro do Node.js:

js
// test.js
const addon = require('./build/Release/addon')

console.log('Isto deve ser oito:', addon.add(3, 5))

Callbacks

É prática comum em addons passar funções JavaScript para uma função C++ e executá-las a partir daí. O exemplo a seguir ilustra como invocar tais callbacks:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;

void RunCallback(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<Function> cb = Local<Function>::Cast(args[0]);
  const unsigned argc = 1;
  Local<Value> argv[argc] = {
      String::NewFromUtf8(isolate,
                          "hello world").ToLocalChecked() };
  cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", RunCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

Este exemplo usa uma forma de dois argumentos de Init() que recebe o objeto module completo como segundo argumento. Isso permite que o addon sobrescreva completamente exports com uma única função, em vez de adicionar a função como uma propriedade de exports.

Para testá-lo, execute o seguinte JavaScript:

js
// test.js
const addon = require('./build/Release/addon')

addon(msg => {
  console.log(msg)
  // Imprime: 'hello world'
})

Neste exemplo, a função de callback é invocada de forma síncrona.

Fábrica de objetos

Os addons podem criar e retornar novos objetos de dentro de uma função C++, como ilustrado no exemplo a seguir. Um objeto é criado e retornado com uma propriedade msg que ecoa a string passada para createObject():

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<Object> obj = Object::New(isolate);
  obj->Set(context,
           String::NewFromUtf8(isolate,
                               "msg").ToLocalChecked(),
                               args[0]->ToString(context).ToLocalChecked())
           .FromJust();

  args.GetReturnValue().Set(obj);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

Para testá-lo em JavaScript:

js
// test.js
const addon = require('./build/Release/addon')

const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// Imprime: 'hello world'

Fábrica de Funções

Outro cenário comum é a criação de funções JavaScript que envolvem funções C++ e o retorno delas para o JavaScript:

C++
// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void MyFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "hello world").ToLocalChecked());
}

void CreateFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  Local<Context> context = isolate->GetCurrentContext();
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
  Local<Function> fn = tpl->GetFunction(context).ToLocalChecked();

  // omitir isto para tornar anônimo
  fn->SetName(String::NewFromUtf8(
      isolate, "theFunction").ToLocalChecked());

  args.GetReturnValue().Set(fn);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateFunction);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

Para testar:

js
// test.js
const addon = require('./build/Release/addon')

const fn = addon()
console.log(fn())
// Imprime: 'hello world'

Envolvendo objetos C++

Também é possível envolver objetos/classes C++ de forma que permita que novas instâncias sejam criadas usando o operador new do JavaScript:

C++
// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::Local;
using v8::Object;

void InitAll(Local<Object> exports) {
  MyObject::Init(exports);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

Então, em myobject.h, a classe wrapper herda de node::ObjectWrap:

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Local<v8::Object> exports);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);

  double value_;
};

}  // namespace demo

#endif

Em myobject.cc, implemente os vários métodos que devem ser expostos. No código a seguir, o método plusOne() é exposto adicionando-o ao protótipo do construtor:

C++
// myobject.cc
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Local<Object> exports) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
  addon_data_tpl->SetInternalFieldCount(1);  // 1 campo para MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // Preparar modelo de construtor
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Protótipo
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
  addon_data->SetInternalField(0, constructor);
  exports->Set(context, String::NewFromUtf8(
      isolate, "MyObject").ToLocalChecked(),
      constructor).FromJust();
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invocado como construtor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invocado como função simples `MyObject(...)`, transforma em chamada de construtor.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons =
        args.Data().As<Object>()->GetInternalField(0)
            .As<Value>().As<Function>();
    Local<Object> result =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(result);
  }
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo

Para construir este exemplo, o arquivo myobject.cc deve ser adicionado ao binding.gyp:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

Teste com:

js
// test.js
const addon = require('./build/Release/addon')

const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Imprime: 11
console.log(obj.plusOne())
// Imprime: 12
console.log(obj.plusOne())
// Imprime: 13

O destrutor para um objeto wrapper será executado quando o objeto for coletado pelo coletor de lixo. Para testes de destrutor, existem sinalizadores de linha de comando que podem ser usados para possibilitar a forçagem da coleta de lixo. Esses sinalizadores são fornecidos pelo mecanismo V8 JavaScript subjacente. Eles estão sujeitos a alterações ou remoção a qualquer momento. Eles não são documentados pelo Node.js ou V8 e nunca devem ser usados fora dos testes.

Durante o desligamento do processo ou threads de trabalho, os destrutores não são chamados pelo mecanismo JS. Portanto, é responsabilidade do usuário rastrear esses objetos e garantir a destruição adequada para evitar vazamentos de recursos.

Fábrica de objetos encapsulados

Alternativamente, é possível usar um padrão de fábrica para evitar criar explicitamente instâncias de objetos usando o operador new do JavaScript:

js
const obj = addon.createObject()
// em vez de:
// const obj = new addon.Object();

Primeiro, o método createObject() é implementado em addon.cc:

C++
// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void InitAll(Local<Object> exports, Local<Object> module) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

Em myobject.h, o método estático NewInstance() é adicionado para lidar com a instanciação do objeto. Este método toma o lugar de usar new em JavaScript:

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

A implementação em myobject.cc é semelhante ao exemplo anterior:

C++
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo

Mais uma vez, para construir este exemplo, o arquivo myobject.cc deve ser adicionado ao binding.gyp:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

Teste com:

js
// test.js
const createObject = require('./build/Release/addon')

const obj = createObject(10)
console.log(obj.plusOne())
// Imprime: 11
console.log(obj.plusOne())
// Imprime: 12
console.log(obj.plusOne())
// Imprime: 13

const obj2 = createObject(20)
console.log(obj2.plusOne())
// Imprime: 21
console.log(obj2.plusOne())
// Imprime: 22
console.log(obj2.plusOne())
// Imprime: 23

Passando objetos envolvidos

Além de envolver e retornar objetos C++, é possível passar objetos envolvidos por aí, desempacotando-os com a função auxiliar Node.js node::ObjectWrap::Unwrap. Os exemplos a seguir mostram uma função add() que pode receber dois objetos MyObject como argumentos de entrada:

C++
// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
      args[0]->ToObject(context).ToLocalChecked());
  MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
      args[1]->ToObject(context).ToLocalChecked());

  double sum = obj1->value() + obj2->value();
  args.GetReturnValue().Set(Number::New(isolate, sum));
}

void InitAll(Local<Object> exports) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(exports, "createObject", CreateObject);
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo

Em myobject.h, um novo método público é adicionado para permitir o acesso a valores privados após desempacotar o objeto.

C++
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
  inline double value() const { return value_; }

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

A implementação de myobject.cc permanece semelhante à versão anterior:

C++
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

}  // namespace demo

Teste com:

js
// test.js
const addon = require('./build/Release/addon')

const obj1 = addon.createObject(10)
const obj2 = addon.createObject(20)
const result = addon.add(obj1, obj2)

console.log(result)
// Imprime: 30