Cómo crear complementos C/C++ en Node

Node.js es excelente por muchas razones, una de las cuales es la velocidad con la que puede crear aplicaciones significativas. Sin embargo, como todos sabemos, esto tiene un precio...

Node.js es excelente por muchas razones, una de las cuales es la velocidad con la que puede crear aplicaciones significativas. Sin embargo, como todos sabemos, esto tiene el precio del rendimiento (en comparación con el código nativo). Para evitar esto, puede escribir su código para interactuar con un código más rápido escrito en C o C++. Todo lo que tenemos que hacer es dejar que Node sepa dónde encontrar este código y cómo interactuar con él.

Hay algunas formas de resolver este problema según el nivel de abstracción que desee. Comenzaremos con la abstracción más baja, que es el Nodo Añadir.

Complementos

Un complemento funciona proporcionando el vínculo entre las bibliotecas Node y C/C++. Para el desarrollador típico de Node, esto puede ser un poco complicado, ya que tendrá que comenzar a escribir código C/C++ para configurar la interfaz. Sin embargo, entre este artículo y la documentación de Node, debería poder hacer funcionar algunas interfaces simples.

Hay algunas cosas que debemos repasar antes de que podamos comenzar a crear complementos. En primer lugar, necesitamos saber cómo compilar (algo que los desarrolladores de Node olvidan felizmente) el código nativo. Esto se hace usando nodo-gyp. Luego, hablaremos brevemente sobre yaya, que ayuda a manejar diferentes versiones de API de nodo.

nodo-gyp

Hay muchos tipos diferentes de procesadores (x86, ARM, PowerPC, etc.) e incluso más sistemas operativos con los que lidiar al compilar su código. Afortunadamente, node-gyp maneja todo esto por ti. Como se describe en su página de Github, node-gyp es una "herramienta de línea de comandos multiplataforma escrita en Node.js para compilar módulos complementarios nativos para Node.js". Esencialmente, node-gyp es solo un envoltorio alrededor de estafa, creado por el equipo de Chromium.

El archivo README del proyecto tiene excelentes instrucciones sobre cómo instalar y usar el paquete, por lo que debe leerlo para obtener más detalles. En resumen, para usar node-gyp debe hacer lo siguiente.

Vaya al directorio de su proyecto:

1
$ cd my_node_addon

Genere los archivos de compilación adecuados con el comando configure, que creará un Makefile (en Unix) o vcxproj (en Windows):

1
$ node-gyp configure

Y finalmente, construye el proyecto:

1
$ node-gyp build

Esto generará un directorio /build que contiene, entre otras cosas, el binario compilado.

Incluso cuando se usan abstracciones superiores como el paquete ffi, sigue siendo bueno comprender lo que sucede debajo del capó, por lo que le recomiendo que se tome el tiempo para aprender los entresijos de node-gyp.

en

nan (Native Abstractions for Node) es un módulo que se pasa por alto fácilmente, pero le ahorrará horas de frustración. Entre las versiones de Node v0.8, v0.10 y v0.12, las versiones V8 utilizadas pasaron por algunos cambios importantes (además de los cambios dentro del mismo Node), por lo que nan ayuda a ocultar estos cambios de usted y proporciona una interfaz agradable y consistente.

Esta abstracción nativa funciona proporcionando objetos/funciones C/C++ en el archivo de encabezado #include <nan.h>.

Para usarlo, instala el paquete nan:

1
$ npm install --save nan

Agregue estas líneas a su archivo binding.gyp:

1
2
3
"include_dirs" : [ 
    "<!(node -e \"require('nan')\")"
]

Y estás listo para usar los métodos/funciones de nan.h dentro de tus ganchos en lugar del # original incluir código <node.h>. Te recomiendo encarecidamente que uses nan. No tiene mucho sentido reinventar la rueda en este caso.

Creación del complemento

Antes de comenzar con su complemento, asegúrese de tomarse un tiempo para familiarizarse con las siguientes bibliotecas:

  • La biblioteca V8 JavaScript C++, que se utiliza para interactuar realmente con JavaScript (como crear funciones, llamar a objetos, etc.).
    • NOTE: node.h is the default file suggested, but really nan.h should be used instead
  • libuv, una biblioteca de E/S asíncrona multiplataforma escrita en C. Esta biblioteca es útil para realizar cualquier tipo de E/S (abrir un archivo, escribir a la red, configurando un temporizador, etc.) en sus bibliotecas nativas y necesita hacerlo asíncrono.
  • Bibliotecas de Nodos Internos. Uno de los objetos más importantes que hay que entender es node::ObjectWrap, del que derivan la mayoría de los objetos.

A lo largo del resto de esta sección, lo guiaré a través de un ejemplo real. En este caso, crearemos un enlace a la función pow de la biblioteca <cmath> de C++. Dado que casi siempre debería usar nan, eso es lo que usaré a lo largo de los ejemplos.

Para este ejemplo, en su proyecto adicional debe tener al menos estos archivos presentes:

  • pow.cpp
  • encuadernación.gyp
  • paquete.json

El archivo C++ no necesita llamarse pow.cpp, pero el nombre generalmente refleja que es un complemento o su función específica.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// pow.cpp
#include <cmath>
#include <nan.h>

void Pow(const Nan::FunctionCallbackInfo<v8::Value>& info) {

    if (info.Length() < 2) {
        Nan::ThrowTypeError("Wrong number of arguments");
        return;
    }

    if (!info[0]->IsNumber() || !info[1]->IsNumber()) {
        Nan::ThrowTypeError("Both arguments should be numbers");
        return;
    }

    double arg0 = info[0]->NumberValue();
    double arg1 = info[1]->NumberValue();
    v8::Local<v8::Number> num = Nan::New(pow(arg0, arg1));

    info.GetReturnValue().Set(num);
}

void Init(v8::Local<v8::Object> exports) {
    exports->Set(Nan::New("pow").ToLocalChecked(),
                 Nan::New<v8::FunctionTemplate>(Pow)->GetFunction());
}

NODE_MODULE(pow, Init)

Tenga en cuenta que no hay punto y coma (;) al final de NODE_MODULE. Esto se hace intencionalmente ya que NODE_MODULE no es realmente una función, es una macro.

El código anterior puede parecer un poco abrumador al principio para aquellos que no han escrito nada de C++ por un tiempo (o nunca), pero realmente no es demasiado difícil de entender. La función Pow es la esencia del código donde verificamos la cantidad de argumentos pasados, los tipos de argumentos, llamamos a la función nativa pow y devolvemos el resultado a la aplicación Node. El objeto info contiene todo lo que necesitamos saber sobre la llamada, incluidos los argumentos (y sus tipos) y un lugar para devolver el resultado.

La función Init en su mayoría solo asocia la función Pow con el
"pow", y la macro NODE_MODULE en realidad maneja el registro del complemento con Node.

El archivo package.json no es muy diferente de un módulo Node normal. Aunque no parece ser necesario, la mayoría de los módulos Addon tienen "gypfile": true configurado dentro de ellos, pero el proceso de compilación parece funcionar bien sin él. Esto es lo que usé para este ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "name": "addon-hook",
  "version": "0.0.0",
  "description": "Node.js Addon Example",
  "main": "index.js",
  "dependencies": {
    "nan": "^2.0.0"
  },
  "scripts": {
    "test": "node index.js"
  }
}

A continuación, este código debe integrarse en un archivo 'pow.node', que es el binario del complemento. Para hacer esto, deberá decirle a node-gyp qué archivos necesita compilar y el nombre de archivo resultante del binario. Si bien hay muchas otras opciones/configuraciones que puede usar con node-gyp, para este ejemplo no necesitamos muchas. El archivo binding.gyp puede ser tan simple como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "targets": [
        {
            "target_name": "pow",
            "sources": [ "pow.cpp" ],
            "include_dirs": [
                "<!(node -e \"require('nan')\")"
            ]
        }
    ]
}

Ahora, usando node-gyp, genere los archivos de compilación de proyecto apropiados para la plataforma dada:

1
$ node-gyp configure

Y finalmente, construye el proyecto:

1
$ node-gyp build

Esto debería resultar en la creación de un archivo pow.node, que residirá en el directorio build/Release/. Para usar este enlace en el código de su aplicación, simplemente require en el archivo pow.node (sin la extensión '.node'):

1
2
3
var addon = require('./build/Release/pow');

console.log(addon.pow(4, 2));      // Prints '16'

Interfaz de función externa de nodo {#interfaz de función externa de nodo}

Nota: El paquete ffi se conocía anteriormente como node-ffi. Asegúrese de agregar el nuevo nombre ffi a sus dependencias para evitar mucha confusión durante npm install :)

Si bien la funcionalidad Addon proporcionada por Node le brinda toda la flexibilidad que necesita, no todos los desarrolladores/proyectos la necesitarán. En muchos casos, una abstracción como ffi funcionará bien y, por lo general, requiere muy poca o ninguna programación en C/C++.

ffi solo carga bibliotecas dinámicas, lo que puede ser limitante para algunos, pero también hace que los ganchos sean mucho más fáciles de configurar.

1
2
3
4
5
6
7
var ffi = require('ffi');

var libm = ffi.Library('libm', {
    'pow': [ 'double', [ 'double', 'double' ] ]
});

console.log(libm.pow(4, 2));   // 16

El código anterior funciona especificando la biblioteca para cargar (libm), y específicamente qué métodos cargar desde esa biblioteca (pow). La línea [ 'double', [ 'double', 'double' ] ] le dice a ffi cuál es el tipo de devolución y los parámetros del método, que en este caso son dos parámetros double y un double devuelto .

Conclusión

Si bien puede parecer intimidante al principio, crear un complemento realmente no es tan malo después de haber tenido la oportunidad de trabajar con un pequeño ejemplo como este por su cuenta. Cuando sea posible, sugeriría conectarse a una biblioteca dinámica para que la creación de la interfaz y la carga del código sean mucho más fáciles, aunque para muchos proyectos esto puede no ser posible o la mejor opción.

¿Hay algún ejemplo de bibliotecas para las que le gustaría ver enlaces? ¡Cuéntanos en los comentarios!