Escribiendo módulos nativos de Node.js

Aunue tengamos tiempo y experiencia programando en JS, existen ocasiones en que el rendimiento de JavaScript no es suficiente, por lo que se debe depender más de los módulos nativos de Node.js. Sigue leyendo para que te enteres cómo hacer esto.

Si bien las extensiones nativas definitivamente no son un tema para principiantes, recomendaría este artículo a todos los desarrolladores de Node.js, para así obtener un poco de conocimiento sobre cómo funcionan.

Casos de uso comunes de los módulos nativos de Node.js

El conocimiento sobre módulos nativos es útil cuando estás agregando una extensión nativa como una dependencia, ¡Lo que ya podría haber hecho!

Solo revisa un poco la lista de algunos módulos populares que usan extensiones nativas. Estás usando al menos uno de ellos, ¿O me equivoco?

  • https://github.com/wadey/node-microtime
  • https://github.com/node-inspector
  • https://github.com/node-inspector/v8-profiler
  • http://www.nodegit.org/

Hay algunas razones por las que uno podría considerar la creación de módulos Node.js nativos, que incluyen, entre otros:

  • Aplicaciones de rendimiento crítico: Seamos honestos, Node.js es excelente para realizar operaciones de entrada y salida asíncronas, pero cuando se trata de un verdadero cálculo de números, no es una elección tan buena.
  • Conexión a las API de nivel inferior, por ejemplo: sistema operativo.
  • Creando un puente entre las bibliotecas C o C ++ y Node.js

¿Cuáles son los módulos nativos?

Los complementos de Node.js son objetos compartidos enlazados dinámicamente y escritos en C o C ++, que se pueden cargar en Node.js usando la función require (), y se usan como si fueran un módulo ordinario de Node.js.

Esto significa que (si se hace correctamente) las peculiaridades de C / C ++ se pueden ocultar al consumidor del módulo. Lo que verán en cambio es que su módulo es un módulo Node.js, como si lo hubiera

Node.js se ejecuta en el motor V8 JavaScript, que es un programa en C por sí solo. Podemos escribir código que interactúe directamente con este programa en C en su propio idioma, lo cual es genial porque podemos evitar una gran cantidad de costosos costos de serialización y comunicación.

Además, en una entrada de blog anterior que hemos aprendido sobre el costo del colector de basura Node.js. Aunque la recolección de basura se puede evitar por completo si decides administrar la memoria tu mismo (porque C y C ++ no tiene un concepto de GC), creará problemas de memoria mucho más fácilmente.

Escribir extensiones nativas requiere conocimiento sobre uno o más de los siguientes temas:

  • Libuv
  • V8
  • Node.js internals

Todos ellos tienen una excelente documentación. Si estás entrando en este campo, te recomiendo leerlos.

Sin más preámbulos, vamos a comenzar con el tema fuerte de este post:

Prerrequisitos

  • Para sistema operativo Linux:
    1. Usar Python ( te recomiendo usar la v2.7, ya que la v3.xx no es compatible)
    2. Make.
    3. Usar una adecuada cadena de herramientas de compilación de C o C ++, como GCC
  • Para sistema operativo Mac:
    1. Tener Xcode instalado: asegúrate de que no solo lo instale, sino que lo inicie al menos una vez y acepte sus términos y condiciones; de lo contrario, ¡no funcionará!
  • Para sistema operativo Windows:
    1. Ejecutar cmd.exe como administrador e introduce el comando npm install --global --production windows-build-tools, que instalará todo por ti.
    2. Otra opción es instalar Visual Studio: (tiene todas las herramientas de construcción de C / C ++ preconfiguradas)
    3. O utiliza el subsistema Linux provisto por la última compilación de Windows. Con eso, sigue las instrucciones de LINUX anteriores.

Creando nuestra extensión nativa Node.js

Vamos a crear nuestro primer archivo para la extensión nativa. Podemos usar la extensión .cc que significa que es C con clases, o la extensión .cpp que es la predeterminada para C ++. La Guía de estilo de Google recomienda .cc, así que para este tutorial seguiré con ella.

En este momento vamos a ver el archivo completo para luego explicarlo línea por línea.


#include <node.h>

const int maxValue = 10;
int numberOfCalls = 0;

void WhoAmI(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
auto message = v8::String::NewFromUtf8(isolate, "Yo soy Node Hero!");
args.GetReturnValue().Set(message);
}

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

if (!args[0]->IsNumber()) {
isolate->ThrowException(v8::Exception::TypeError(
v8::String::NewFromUtf8(isolate, "El argumento debe ser un número")));
return;
}

double argsValue = args[0]->NumberValue();
if (numberOfCalls + argsValue > maxValue) {
isolate->ThrowException(v8::Exception::Error(
v8::String::NewFromUtf8(isolate, "¡El mostrador se fue por las nubes!")));
return;
}

numberOfCalls += argsValue;

auto currentNumberOfCalls =
v8::Number::New(isolate, static_cast<double>
(numberOfCalls));

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

void Initialize(v8::Local<v8::Object> exports) {
NODE_SET_METHOD(exports, "whoami", WhoAmI);
NODE_SET_METHOD(exports, "increment", Increment);
}

NODE_MODULE(module_name, Initialize)

Ahora vamos a ver el archivo línea por línea

#include <node.h>

El include en C ++ es como require()en JavaScript. Extraerá todo del archivo dado, pero en lugar de vincularlo directamente a la fuente, en C ++ tenemos el concepto de archivos de encabezado.

Podemos declarar la interfaz exacta en los archivos de encabezado sin implementación y luego podemos incluir las implementaciones por su archivo de encabezado. El enlazador C ++ se encargará de vincular estos dos juntos. Piensa en ello como un archivo de documentación que describe tu contenido, que puede ser reutilizado desde su código.

void WhoAmI(const
v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
auto message = v8::String::NewFromUtf8(isolate, "Yo soy Node Hero!");
args.GetReturnValue().Set(message);
}
[php]

Debido a que esta será una extensión nativa, el espacio de nombres v8 está disponible para su uso. Ten en cuenta la notación <strong>v8::</strong>, que se utiliza para acceder a la interfaz del v8. Si no desea incluir <strong>v8::</strong> antes de usar cualquiera de los tipos provistos por el v8, puede agregarlo usando <strong>using</strong> <strong>v8;</strong> a la parte superior del archivo. Luego, puede omitir todos los especificadores <strong>v8::</strong> de espacio de nombres de sus tipos, pero esto puede introducir colisiones de nombres en el código, así que ten cuidado al usarlos. Para ser 100% claros, usaré la notación <strong>v8::</strong> para todos los tipos de v8 en el código mostrado.

En nuestro código de ejemplo, tenemos acceso a los argumentos con los que se llamó a la función (desde JavaScript), a través del objeto <strong>args</strong> que también nos proporciona toda la información relacionada con la llamada.

Con <strong>v8::Isolate*</strong> estamos obteniendo acceso al alcance de JavaScript actual para nuestra función. Los ámbitos funcionan igual que en JavaScript: podemos asignar variables y vincularlas a la vida útil de ese código específico. No tenemos que preocuparnos por des-asignar estos trozos de memoria, porque los asignamos como si estuviéramos en JavaScript, y el recolector de basura se encargará de ellos automáticamente.

[php]

function () {
var a = 1;
} // envergadura


Vía args.GetReturnValue() accedemos al valor de retorno de nuestra función. Podemos configurarlo para lo que queramos siempre que sea desde el espacio v8:: de nombres.

C ++ tiene tipos incorporados para almacenar enteros y cadenas, pero JavaScript solo entiende sus propiostipos de objetos v8::. Mientras estemos en el ámbito del mundo de C ++, podemos utilizar los que están integrados en C ++, pero cuando tratamos con objetos de JavaScript e interoperabilidad con el código de JavaScript, tenemos que transformar los tipos de C ++ en otros que se entienden. por el contexto de JavaScript. Estos son los tipos que están expuestos en v8 :: namespace como v8::Stringo v8::Object.


void WhoAmI(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
auto message = v8::String::NewFromUtf8(isolate, "Yo soy Node Hero!");
args.GetReturnValue().Set(message);
}


Veamos el segundo método en nuestro archivo que incrementa un contador por un argumento proporcionado hasta un límite superior de 10.

Esta función también acepta un parámetro de JavaScript. Cuando está aceptando parámetros de JavaScript, debe tener cuidado porque son objetos escritos con holgura. (Probablemente ya estés acostumbrado a esto en JavaScript).

La matriz de argumentos contiene v8::Objects, por lo que son todos objetos de JavaScript, pero ten cuidado con estos, porque en este contexto nunca podemos estar seguros de lo que pueden contener. Tenemos que verificar explícitamente los tipos de estos objetos. Afortunadamente, hay métodos auxiliares que se agregan a estas clases para determinar su tipo antes de la conversión de tipos.

Para mantener la compatibilidad con el código JavaScript existente, debemos lanzar algún error si el tipo de argumentos es incorrecto. Para lanzar un error de tipo, tenemos que crear un objeto de error con el constructor
v8::Exception::TypeError(). El siguiente bloque lanzará un TypeError si el primer argumento no es un número.


if (!args[0]->IsNumber()) {
isolate->ThrowException(v8::Exception::TypeError(
v8::String::NewFromUtf8(isolate, "El argumento debe ser un número")));
return;
}

En JavaScript ese fragmento se vería como:


If (typeof arguments[0] !== ‘number’) {
throw new TypeError(‘El argumento debe ser un número’)
}