Introducción a la Creación de Módulos Nativos en Node.js con NAN

Introducción a la Creación de Módulos Nativos en Node.js con NAN

Node.js ha revolucionado la manera en que construimos aplicaciones web, al permitirnos usar JavaScript tanto en el lado del servidor como del cliente. Sin embargo, en ocasiones, es necesario ir más allá de JavaScript y usar código nativo en C++ para realizar operaciones que requieren alto rendimiento o para interactuar con sistemas a nivel de sistema operativo. Para estas situaciones, necesitamos módulos nativos, y herramientas como node-gyp y nan facilitan su implementación

¿Qué es NAN (Native Abstractions for Node.js)?

NAN, o Native Abstractions for Node.js, es una biblioteca de cabecera que simplifica la escritura de módulos nativos para diferentes versiones de Node.js y V8. Esencialmente, NAN proporciona una capa de abstracción que permite escribir código C++ que es compatible con múltiples versiones de Node.js sin tener que preocuparse por los cambios en la API de V8.

Comparación con node-gyp nativo

node-gyp es una herramienta de línea de comandos y una dependencia de muchos paquetes de Node.js. Está diseñada específicamente para compilar y enlazar módulos nativos escritos en C++ para ser usados en aplicaciones de Node.js.

Funciona cruzando las capacidades nativas de compilación de tu sistema con las peculiaridades específicas de la arquitectura de Node.js y V8 (el motor JavaScript que alimenta Node.js). En otras palabras, es el puente entre las bibliotecas nativas de tu sistema y el entorno de Node.js.

Utiliza el archivo binding.gyp, el cual juega un papel crucial en el proceso de construcción de módulos nativos. Es un archivo de configuración basado en JSON que le indica a node-gyp cómo construir el módulo nativo. Define las fuentes, dependencias, flags y otras configuraciones requeridas para la compilación. A menudo, este archivo es lo único que necesitas modificar para adaptar un módulo nativo a diferentes plataformas o entornos.

Si alguna vez has instalado un paquete de Node.js y has notado que se estaban compilando archivos C++ en el fondo, es probable que ese paquete estuviera usando node-gyp (o una herramienta similar) para construir un módulo nativo. Módulos populares como bcrypt, sharp y muchos otros dependen de node-gyp para compilar su código nativo antes de ser utilizados en un proyecto de Node.js.

NAN Va un paso más allá y abstrae las diferencias entre las versiones de la API de V8. Mientras que con node-gyp es necesario hacer ajustes cuando hay cambios en la API de V8 o Node.js, con nan estos ajustes se minimizan.

Veamos un ejemplo de como funciona

Lo primero que debes hacer es inicializar tu proyecto utilizando npm init o yarn init:

yarn init

Con esto se creará un archivo package.json, el cual te permitirá agregar las dependencias que se necesitan para la compilación de los archivos de C++, entonces lo siguiente es instalar node-gyp y nan:

yarn add node-gyp nan -D

El archivo de C++ (addon.cc)

Este archivo se volverá el corazón de nuestro ejemplo, veámos el código y revisemos a como esta compuesto, porque aquí hay algunos nuevos conceptos.

En este ejemplo crearemos 2 nuevas funciones que expondremos en nuestra librería, entonces:

// addon.cc

// incluyendo las librerias de v8
#include <v8.h>
#include <nan.h>
#include <cstdlib>
#include <ctime>

// Método para generar un número aleatorio entre 0 y 1.
NAN_METHOD(RandomNumber) {
    double random_value = static_cast<double>(std::rand()) / RAND_MAX;
    info.GetReturnValue().Set(Nan::New(random_value));
}

// Método utilizado para sumar 2 números dados
NAN_METHOD(Add) {
    // Declarar args
    Nan::NAN_METHOD_ARGS_TYPE args = info;

    v8::Isolate* isolate = args.GetIsolate();
    v8::Local<v8::Context> context = isolate->GetCurrentContext();

    int a = info[0]->IsNumber() ? info[0]->NumberValue(context).ToChecked() : 0;
    int b = info[1]->IsNumber() ? info[1]->NumberValue(context).ToChecked() : 0;
    int sum = a + b;

    info.GetReturnValue().Set(Nan::New(sum));
}

NAN_MODULE_INIT(Init) {
    NAN_EXPORT(target, Add);

    // Inicializando la semilla para la generación de números aleatorios.
    std::srand(std::time(nullptr));
    NAN_EXPORT(target, RandomNumber);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init);

Verás algunos términos que están directamente relacionados con la creación de módulos nativos en Node.js usando la biblioteca NAN y la API V8, te explico:

  1. NAN_METHOD:

    • Es una macro proporcionada por NAN para simplificar la declaración de funciones que serán expuestas a JavaScript desde C++.
    • Reemplaza la firma típica que se requeriría para definir tal función, facilitando la escritura y manteniendo la compatibilidad con diferentes versiones de Node.js/V8.
  2. NAN_MODULE_INIT:

    • Es una macro que define la firma de una función de inicialización para un módulo.
    • Esta función se llama cuando se carga tu módulo y es donde exportarías todas las funciones y objetos que deseas que estén disponibles desde JavaScript.
    • La función recibe un argumento target que representa el objeto que será exportado.
  3. NAN_EXPORT:

    • Es una macro que se utiliza dentro de la función definida con NAN_MODULE_INIT.
    • Se usa para exportar funciones nativas al objeto que será requerido en JavaScript.
    • Por ejemplo, NAN_EXPORT(target, RandomNumber); significa que puedes llamar a la función RandomNumber desde JavaScript una vez que importes el módulo.
  4. NODE_MODULE:

    • Es una macro definida por Node.js que se utiliza para registrar un módulo nativo.
    • Esencialmente, le dice a Node.js cuál es la función de inicialización que debe llamarse cuando se carga el módulo.
    • Por ejemplo, NODE_MODULE(NODE_GYP_MODULE_NAME, Init); registra el módulo con la función Init como su función de inicialización.
  5. info:

    • Cuando defines una función con NAN_METHOD, se pasa automáticamente un argumento llamado info a esa función.
    • info es una instancia de Nan::FunctionCallbackInfo<v8::Value>.
    • Te permite acceder a los argumentos pasados desde JavaScript, al objeto this relacionado con la llamada, establecer el valor de retorno, y más.
  6. Context (o v8::Context):

    • En el motor V8 de JavaScript (que es el que utiliza Node.js), un "contexto" representa un entorno de ejecución independiente.
    • Cada contexto tiene su propio conjunto global de objetos y funciones.
    • Los contextos son cruciales cuando se trabaja con módulos nativos porque te permiten interactuar con el mundo JavaScript desde C++.
    • En nuestro código, v8::Local<v8::Context> context = isolate->GetCurrentContext(); obtiene el contexto actual de ejecución.

Se supone que al utilizar nan, no es necesario utilizar el archivo binding.gyp, pero en mi caso fue necesario ya que tuve que especificar la carpeta donde se encuentran los archivos de nan.

// binding.gyp
{
  'targets': [
    {
      // Especificando directorios incluidos.
      "include_dirs" : ["<!(node -e \"require('nan')\")"],

      // Nombre del objetivo de compilación.
      'target_name': 'addon',

      // Especificando los archivos fuente.
      'sources': [ 'addon.cc' ],

      // Configuraciones específicas de compilación.
      'cflags!': [ '-fno-exceptions' ],
      'cflags_cc!': [ '-fno-exceptions' ],
    }
  ]
}

Una vez que se tiene el archivo addon.cc y el archivo de configuracion de node-gyp, simplemente hay que compilar:

./node_modules/.bin/node-gyp configure rebuild

Y esto produce una carpeta y el archivo addon.node que es el resultado de la compilación.

JavaScript (index.js)

// index.js

// Importando el módulo nativo compilado.
const addon = require('./build/Release/addon');

// Invocando la función nativa 'Add' y mostrando el resultado en consola.
console.log("Add", addon.Add(1, 2));
console.log("RandomNumber", addon.RandomNumber());

Ejecutamos nuestro archivo index.js y vemos el resultado en la consola.

node index.js
Add 3
RandomNumber 0.8909509414299163

Ventajas del uso de NAN:

  1. Compatibilidad a través de versiones: NAN asegura que el código funcione a través de múltiples versiones de Node.js y V8.

  2. Abstracción de API: No necesitas entender todos los cambios de la API de V8.

  3. Mayor productividad: Reduce la necesidad de mantenimiento cuando hay actualizaciones de Node.js.

  4. Facilita la migración: Si ya tienes módulos nativos escritos, NAN puede facilitar la migración entre diferentes versiones de Node.js.

Conclusión:

Mientras que Node.js nos ha proporcionado una plataforma robusta y eficiente para desarrollar aplicaciones, hay ocasiones en las que necesitamos el rendimiento y las capacidades que solo el código nativo puede proporcionar y herramientas como node-gyp y nan facilitan esta integración; específicamente, NAN nos ofrece una forma eficiente y menos dolorosa de mantener y desarrollar módulos nativos a lo largo del tiempo y a través de diferentes versiones de Node.js. Si planeas trabajar con módulos nativos, definitivamente vale la pena considerar NAN como parte de tu caja de herramientas.

Happy coding! :D


Photo by Pawel Czerwinski on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?