Skip to content

Complementos C++

Los complementos son objetos compartidos enlazados dinámicamente escritos en C++. La función require() puede cargar complementos como módulos Node.js ordinarios. Los complementos proporcionan una interfaz entre JavaScript y las bibliotecas C/C++.

Hay tres opciones para implementar complementos:

A menos que exista la necesidad de acceso directo a funcionalidades que no son expuestas por Node-API, utilice Node-API. Consulte Complementos C/C++ con Node-API para obtener más información sobre Node-API.

Cuando no se usa Node-API, la implementación de complementos se vuelve más compleja, requiriendo conocimiento de múltiples componentes y APIs:

  • V8: la biblioteca C++ que Node.js utiliza para proporcionar la implementación de JavaScript. Proporciona los mecanismos para crear objetos, llamar a funciones, etc. La API de V8 está documentada principalmente en el archivo de encabezado v8.h (deps/v8/include/v8.h en el árbol de origen de Node.js), y también está disponible en línea.
  • libuv: La biblioteca C que implementa el bucle de eventos de Node.js, sus hilos de trabajo y todos los comportamientos asíncronos de la plataforma. También sirve como una biblioteca de abstracción multiplataforma, proporcionando un acceso fácil, similar a POSIX, en todos los sistemas operativos principales a muchas tareas comunes del sistema, como la interacción con el sistema de archivos, sockets, temporizadores y eventos del sistema. libuv también proporciona una abstracción de subprocesos similar a los subprocesos POSIX para complementos asíncronos más sofisticados que necesitan ir más allá del bucle de eventos estándar. Los autores de complementos deben evitar bloquear el bucle de eventos con E/S u otras tareas que consumen mucho tiempo descargando el trabajo a través de libuv a operaciones del sistema no bloqueantes, subprocesos de trabajo o un uso personalizado de subprocesos libuv.
  • Bibliotecas internas de Node.js: El propio Node.js exporta APIs de C++ que los complementos pueden usar, la más importante de las cuales es la clase node::ObjectWrap.
  • Otras bibliotecas enlazadas estáticamente (incluida OpenSSL): Estas otras bibliotecas se encuentran en el directorio deps/ en el árbol de origen de Node.js. Solo los símbolos libuv, OpenSSL, V8 y zlib son reexportados intencionadamente por Node.js y pueden ser usados en varios grados por los complementos. Consulte Enlazar a bibliotecas incluidas con Node.js para obtener información adicional.

Todos los siguientes ejemplos están disponibles para descargar y pueden usarse como punto de partida para un complemento.

Hola mundo

Este ejemplo de "Hola mundo" es un addon simple, escrito en C++, que es el equivalente al siguiente código JavaScript:

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

Primero, crea el archivo 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 los addons de Node.js deben exportar una función de inicialización siguiendo el patrón:

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

No hay punto y coma después de NODE_MODULE ya que no es una función (ver node.h).

El module_name debe coincidir con el nombre del archivo binario final (excluyendo el sufijo .node).

En el ejemplo hello.cc, entonces, la función de inicialización es Initialize y el nombre del módulo addon es addon.

Al construir addons con node-gyp, usando la macro NODE_GYP_MODULE_NAME como el primer parámetro de NODE_MODULE() se asegurará de que el nombre del binario final sea pasado a NODE_MODULE().

Los addons definidos con NODE_MODULE() no pueden ser cargados en múltiples contextos o múltiples hilos al mismo tiempo.

Complementos específicos del contexto

Existen entornos en los que puede ser necesario cargar complementos de Node.js varias veces en múltiples contextos. Por ejemplo, el entorno de ejecución de Electron ejecuta varias instancias de Node.js en un solo proceso. Cada instancia tendrá su propia caché de require(), y por lo tanto, cada instancia necesitará un complemento nativo para comportarse correctamente cuando se cargue a través de require(). Esto significa que el complemento debe admitir múltiples inicializaciones.

Un complemento específico del contexto se puede construir utilizando la macro NODE_MODULE_INITIALIZER, que se expande al nombre de una función que Node.js esperará encontrar cuando cargue un complemento. Un complemento puede, por lo tanto, inicializarse como en el siguiente ejemplo:

C++
using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Realizar los pasos de inicialización del complemento aquí. */
}

Otra opción es usar la macro NODE_MODULE_INIT(), que también construirá un complemento específico del contexto. A diferencia de NODE_MODULE(), que se utiliza para construir un complemento alrededor de una función de inicialización de complemento dada, NODE_MODULE_INIT() sirve como la declaración de dicho inicializador que debe ser seguido por un cuerpo de función.

Las siguientes tres variables se pueden usar dentro del cuerpo de la función después de una invocación de NODE_MODULE_INIT():

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

La creación de un complemento específico del contexto requiere una gestión cuidadosa de los datos estáticos globales para garantizar la estabilidad y la corrección. Dado que el complemento puede cargarse varias veces, potencialmente incluso desde diferentes subprocesos, cualquier dato estático global almacenado en el complemento debe protegerse adecuadamente y no debe contener ninguna referencia persistente a objetos JavaScript. La razón de esto es que los objetos JavaScript solo son válidos en un contexto y probablemente causarán un bloqueo cuando se acceda a ellos desde el contexto incorrecto o desde un subproceso diferente al en el que se crearon.

El complemento específico del contexto se puede estructurar para evitar datos estáticos globales realizando los siguientes pasos:

  • Definir una clase que contendrá datos por instancia de complemento y que tenga un miembro estático del formulario
  • Asignar en el montón una instancia de esta clase en el inicializador del complemento. Esto se puede lograr utilizando la palabra clave new.
  • Llamar a node::AddEnvironmentCleanupHook(), pasándole la instancia creada anteriormente y un puntero a DeleteInstance(). Esto asegurará que la instancia se elimine cuando se desmonte el entorno.
  • Almacenar la instancia de la clase en un v8::External, y
  • Pasar el v8::External a todos los métodos expuestos a JavaScript pasándolo a v8::FunctionTemplate::New() o v8::Function::New() que crea las funciones JavaScript respaldadas por nativos. El tercer parámetro de v8::FunctionTemplate::New() o v8::Function::New() acepta el v8::External y lo pone a disposición en la devolución de llamada nativa utilizando el método v8::FunctionCallbackInfo::Data().

Esto asegurará que los datos por instancia de complemento lleguen a cada enlace que se pueda llamar desde JavaScript. Los datos por instancia de complemento también deben pasarse a cualquier devolución de llamada asíncrona que pueda crear el complemento.

El siguiente ejemplo ilustra la implementación de un complemento específico del contexto:

C++
#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // Asegurar que estos datos por instancia de complemento se eliminen en la limpieza del entorno.
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // Datos por complemento.
  int call_count;

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

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // Recuperar los datos por instancia de complemento.
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// Inicializar este complemento para que sea específico del contexto.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // Crear una nueva instancia de `AddonData` para esta instancia del complemento y
  // vincular su ciclo de vida al del entorno Node.js.
  AddonData* data = new AddonData(isolate);

  // Envolver los datos en un `v8::External` para poder pasárselos al método que
  // exponemos.
  Local<External> external = External::New(isolate, data);

  // Exponer el método `Method` a JavaScript, y asegurarse de que recibe los
  // datos por instancia de complemento que creamos anteriormente pasando `external` como el
  // tercer parámetro al constructor `FunctionTemplate`.
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Soporte para Workers

[Historial]

VersiónCambios
v14.8.0, v12.19.0Los ganchos de limpieza ahora pueden ser asíncronos.

Para poder cargarse desde múltiples entornos Node.js, como un hilo principal y un hilo Worker, un complemento necesita:

  • Ser un complemento Node-API, o
  • Declararse como consciente del contexto usando NODE_MODULE_INIT() como se describe anteriormente

Para soportar hilos Worker, los complementos necesitan limpiar cualquier recurso que hayan asignado cuando dicho hilo finaliza. Esto se puede lograr mediante el uso de la función AddEnvironmentCleanupHook():

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

Esta función agrega un gancho que se ejecutará antes de que una instancia dada de Node.js se cierre. Si es necesario, dichos ganchos se pueden eliminar antes de que se ejecuten usando RemoveEnvironmentCleanupHook(), que tiene la misma firma. Las funciones de devolución de llamada se ejecutan en orden LIFO (Last-In, First-Out).

Si es necesario, hay un par adicional de sobrecargas de AddEnvironmentCleanupHook() y RemoveEnvironmentCleanupHook(), donde el gancho de limpieza toma una función de devolución de llamada. Esto se puede usar para cerrar recursos asíncronos, como cualquier manejador libuv registrado por el complemento.

El siguiente 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;

// Nota: En una aplicación del mundo real, no confíe en datos estáticos/globales.
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());  // assert VM is still alive
  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);
}

// Inicializar este complemento para que sea consciente del contexto.
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);
}

Prueba en JavaScript ejecutando:

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

Compilación

Una vez escrito el código fuente, debe compilarse en el archivo binario addon.node. Para ello, cree un archivo llamado binding.gyp en el nivel superior del proyecto que describa la configuración de compilación del módulo utilizando un formato similar a JSON. Este archivo es utilizado por node-gyp, una herramienta escrita específicamente para compilar complementos de Node.js.

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

Una versión de la utilidad node-gyp se incluye y distribuye con Node.js como parte de npm. Esta versión no está disponible directamente para que la utilicen los desarrolladores y solo tiene la finalidad de admitir la capacidad de utilizar el comando npm install para compilar e instalar complementos. Los desarrolladores que deseen utilizar node-gyp directamente pueden instalarlo utilizando el comando npm install -g node-gyp. Consulte las instrucciones de instalación de node-gyp para obtener más información, incluidos los requisitos específicos de la plataforma.

Una vez creado el archivo binding.gyp, utilice node-gyp configure para generar los archivos de compilación del proyecto apropiados para la plataforma actual. Esto generará un archivo Makefile (en plataformas Unix) o un archivo vcxproj (en Windows) en el directorio build/.

A continuación, invoque el comando node-gyp build para generar el archivo compilado addon.node. Este se colocará en el directorio build/Release/.

Cuando se utiliza npm install para instalar un complemento de Node.js, npm utiliza su propia versión incluida de node-gyp para realizar este mismo conjunto de acciones, generando una versión compilada del complemento para la plataforma del usuario a demanda.

Una vez compilado, el complemento binario se puede utilizar desde Node.js apuntando require() al módulo addon.node compilado:

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

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

Debido a que la ruta exacta al binario del complemento compilado puede variar según cómo se compile (es decir, a veces puede estar en ./build/Debug/), los complementos pueden utilizar el paquete bindings para cargar el módulo compilado.

Si bien la implementación del paquete bindings es más sofisticada en la forma en que localiza los módulos de complementos, esencialmente utiliza un patrón try…catch similar a:

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

Enlaces a bibliotecas incluidas con Node.js

Node.js utiliza bibliotecas estáticamente enlazadas como V8, libuv y OpenSSL. Todos los complementos deben enlazarse con V8 y pueden enlazarse también con cualquiera de las otras dependencias. Normalmente, esto es tan simple como incluir las instrucciones #include <...> apropiadas (por ejemplo, #include <v8.h>) y node-gyp encontrará automáticamente los encabezados apropiados. Sin embargo, hay algunas advertencias que deben tenerse en cuenta:

  • Cuando se ejecuta node-gyp, detectará la versión específica de lanzamiento de Node.js y descargará el archivo tarball fuente completo o solo los encabezados. Si se descarga la fuente completa, los complementos tendrán acceso completo al conjunto completo de dependencias de Node.js. Sin embargo, si solo se descargan los encabezados de Node.js, solo estarán disponibles los símbolos exportados por Node.js.
  • node-gyp se puede ejecutar usando la bandera --nodedir apuntando a una imagen de fuente local de Node.js. Usando esta opción, el complemento tendrá acceso al conjunto completo de dependencias.

Carga de complementos usando require()

La extensión de nombre de archivo del binario del complemento compilado es .node (a diferencia de .dll o .so). La función require() está escrita para buscar archivos con la extensión de archivo .node e inicializarlos como bibliotecas dinámicamente enlazadas.

Al llamar a require(), la extensión .node generalmente se puede omitir y Node.js seguirá encontrando e inicializando el complemento. Sin embargo, una advertencia es que Node.js primero intentará localizar y cargar módulos o archivos JavaScript que compartan el mismo nombre base. Por ejemplo, si hay un archivo addon.js en el mismo directorio que el binario addon.node, entonces require('addon') dará precedencia al archivo addon.js y lo cargará en su lugar.

Abstracciones nativas para Node.js

Cada uno de los ejemplos ilustrados en este documento utilizan directamente las APIs de Node.js y V8 para implementar addons. La API de V8 puede haber cambiado, y de hecho ha cambiado, dramáticamente de una versión de V8 a la siguiente (y de una versión principal de Node.js a la siguiente). Con cada cambio, es posible que los addons deban actualizarse y recompilarse para continuar funcionando. El programa de lanzamientos de Node.js está diseñado para minimizar la frecuencia y el impacto de dichos cambios, pero Node.js puede hacer poco para garantizar la estabilidad de las APIs de V8.

Las Abstracciones Nativas para Node.js (o nan) proporcionan un conjunto de herramientas que se recomienda a los desarrolladores de addons utilizar para mantener la compatibilidad entre las versiones pasadas y futuras de V8 y Node.js. Consulte los ejemplos de nan ejemplos para ver una ilustración de cómo se puede utilizar.

Node-API

[Estable: 2 - Estable]

Estable: 2 Estabilidad: 2 - Estable

Node-API es una API para construir addons nativos. Es independiente del entorno de ejecución de JavaScript subyacente (por ejemplo, V8) y se mantiene como parte de Node.js. Esta API será estable en la Interfaz Binaria de Aplicación (ABI) a través de las versiones de Node.js. Su objetivo es aislar los addons de los cambios en el motor de JavaScript subyacente y permitir que los módulos compilados para una versión se ejecuten en versiones posteriores de Node.js sin recompilación. Los addons se construyen/empaquetan con el mismo enfoque/herramientas descritos en este documento (node-gyp, etc.). La única diferencia es el conjunto de APIs que utiliza el código nativo. En lugar de utilizar las APIs de V8 o las Abstracciones Nativas para Node.js, se utilizan las funciones disponibles en la Node-API.

La creación y el mantenimiento de un addon que se beneficia de la estabilidad ABI proporcionada por Node-API conlleva ciertas consideraciones de implementación.

Para utilizar Node-API en el ejemplo anterior de "Hola mundo", reemplace el contenido de hello.cc con lo siguiente. Todas las demás instrucciones siguen siendo las mismas.

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

Las funciones disponibles y cómo utilizarlas se documentan en Addons C/C++ con Node-API.

Ejemplos de complementos

A continuación, se presentan algunos ejemplos de complementos destinados a ayudar a los desarrolladores a comenzar. Los ejemplos usan las API de V8. Consulte la referencia de V8 en línea para obtener ayuda con las diversas llamadas de V8, y la Guía para integradores de V8 para obtener una explicación de varios conceptos utilizados, como controladores, ámbitos, plantillas de funciones, etc.

Cada uno de estos ejemplos utiliza el siguiente archivo binding.gyp:

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

En los casos en que haya más de un archivo .cc, simplemente agregue el nombre de archivo adicional a la matriz sources:

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

Una vez que el archivo binding.gyp esté listo, los complementos de ejemplo se pueden configurar y compilar usando node-gyp:

bash
node-gyp configure build

Argumentos de la función

Los complementos normalmente expondrán objetos y funciones a las que se puede acceder desde JavaScript que se ejecuta dentro de Node.js. Cuando las funciones se invocan desde JavaScript, los argumentos de entrada y el valor de retorno deben asignarse a y desde el código C/C++.

El siguiente ejemplo ilustra cómo leer los argumentos de función pasados desde JavaScript y cómo devolver un 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 es la implementación del método "add"
// Los argumentos de entrada se pasan usando la estructura
// const FunctionCallbackInfo<Value>& args
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Verificar el número de argumentos pasados.
  if (args.Length() < 2) {
    // Lanzar un Error que se pasa de vuelta a JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Número incorrecto de argumentos").ToLocalChecked()));
    return;
  }

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

  // Realizar la operación
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Establecer el valor de retorno (usando el pasado
  // 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

Una vez compilado, el complemento de ejemplo se puede requerir y usar desde Node.js:

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

console.log('Esto debería ser ocho:', addon.add(3, 5))

Funciones de devolución de llamada (Callbacks)

Es una práctica común dentro de los complementos pasar funciones JavaScript a una función C++ y ejecutarlas desde allí. El siguiente ejemplo ilustra cómo invocar dichas funciones de devolución de llamada:

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 ejemplo utiliza un formulario de Init() de dos argumentos que recibe el objeto module completo como segundo argumento. Esto permite que el complemento sobrescriba completamente exports con una sola función en lugar de agregar la función como una propiedad de exports.

Para probarlo, ejecute el siguiente JavaScript:

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

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

En este ejemplo, la función de devolución de llamada se invoca de forma síncrona.

Fábrica de objetos

Los complementos pueden crear y devolver nuevos objetos desde una función C++, como se ilustra en el siguiente ejemplo. Se crea y devuelve un objeto con una propiedad msg que hace eco de la cadena pasada a 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 probarlo en 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 funciones

Otro escenario común es crear funciones JavaScript que envuelven funciones C++ y devolverlas a 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 esto para hacerlo 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 probar:

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

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

Envolviendo objetos C++

También es posible envolver objetos/clases C++ de una manera que permita que se creen nuevas instancias usando el operador new de 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

Luego, en myobject.h, la clase envoltorio hereda 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

En myobject.cc, implemente los diversos métodos que se van a exponer. En el siguiente código, el método plusOne() se expone agregándolo al prototipo del constructor:

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 field for the MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

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

  // Prototype
  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()) {
    // 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 =
        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 ejemplo, el archivo myobject.cc debe agregarse al binding.gyp:

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

Pruébelo con:

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

El destructor de un objeto envoltorio se ejecutará cuando el objeto sea recolectado por el recolector de basura. Para las pruebas del destructor, hay indicadores de línea de comandos que se pueden usar para que sea posible forzar la recolección de basura. Estos indicadores son proporcionados por el motor subyacente de JavaScript V8. Están sujetos a cambios o eliminación en cualquier momento. No están documentados por Node.js o V8, y nunca deben usarse fuera de las pruebas.

Durante el cierre del proceso o los subprocesos de trabajo, el motor JS no llama a los destructores. Por lo tanto, es responsabilidad del usuario realizar un seguimiento de estos objetos y garantizar una destrucción adecuada para evitar fugas de recursos.

Fábrica de objetos encapsulados

Alternativamente, es posible usar un patrón de fábrica para evitar la creación explícita de instancias de objetos usando el operador new de JavaScript:

js
const obj = addon.createObject()
// en lugar de:
// const obj = new addon.Object();

Primero, el método createObject() se implementa en 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

En myobject.h, se agrega el método estático NewInstance() para manejar la instanciación del objeto. Este método reemplaza el uso de new en 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

La implementación en myobject.cc es similar al ejemplo 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

Una vez más, para construir este ejemplo, el archivo myobject.cc debe agregarse a binding.gyp:

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

Pruébelo con:

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

Pasar objetos encapsulados

Además de encapsular y devolver objetos C++, es posible pasar objetos encapsulados utilizando la función auxiliar de Node.js node::ObjectWrap::Unwrap. El siguiente ejemplo muestra una función add() que puede tomar dos 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

En myobject.h, se agrega un nuevo método público para permitir el acceso a valores privados después de desencapsular el 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

La implementación de myobject.cc permanece similar a la versión 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

Pruébalo con:

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