Advertisement
  1. Code
  2. Node.js

Escribir complementos de Node.js

Scroll to top
Read Time: 9 min

() translation by (you can also view the original English article)

Node.js es excelente para escribir tu back-end en JavaScript. Pero, ¿qué sucede si necesitas alguna funcionalidad que no se proporciona de inmediato, o que tampoco se puede lograr incluso usando módulos, pero que sí está disponible en forma de una biblioteca C/C++? Bueno, sorprendentemente, puedes escribir un complemento que te permitirá usar esta biblioteca en tu código JavaScript. Vemos cómo.

Como puedes leer en la documentación de Node.js, los complementos son objetos compartidos vinculados dinámicamente que pueden proporcionar conexiones con las bibliotecas C/C++. Esto significa que puedes tomar prácticamente cualquier biblioteca C/C++ y crear un complemento que te permitirá usarlo en Node.js.

Como ejemplo, crearemos un contenedor para el objeto estándar std::string.


Preparación

Antes de comenzar a escribir, debes asegurarte de tener todo lo que necesitas para compilar el módulo más tarde. Necesita node-gyp y todas sus dependencias. Puedes instalar node-gyp usando el siguiente comando:

1
npm install -g node-gyp

En cuanto a las dependencias, en los sistemas Unix necesitarás:

  • Python (2.7, 3.x no funcionará)
  • make
  • Un compilador de C++ (como gpp o g++)

Por ejemplo, en Ubuntu puedes instalar todo esto usando este comando (Python 2.7 ya debería estar instalado):

1
sudo apt-get install build-essentials

En Windows necesitarás:

  • Python (2.7.3, 3.x no funcionará)
  • Microsoft Visual Studio C++ 2010 (para Windows XP/Vista)
  • Microsoft Visual Studio C++ 2012 para escritorio de Windows (Windows 7/8)

La versión Express de Visual Studio funciona bien.


El archivo binding.gyp

Este archivo es utilizado por node-gyp para generar archivos de compilación apropiados para tu complemento. La documentación completa del archivo .gyp se puede encontrar en su página Wiki, pero para nuestros propósitos, este simple archivo servirá:

1
{
2
	"targets": [
3
		{
4
			"target_name": "stdstring",
5
			"sources": [ "addon.cc", "stdstring.cc" ]
6
		}
7
	]
8
}

El target_name puede ser cualquier nombre que desees. La matriz sources contiene todos los archivos fuente que utiliza el complemento. En nuestro ejemplo, addon.cc contendrá el código necesario para compilar nuestro complemento y stdstring.cc va a tener nuestra clase contenedora.


La clase STDStringWrapper

Comenzaremos definiendo nuestra clase en el archivo stdstring.h. Las dos primeras líneas deberían resultarte familiares si alguna vez has programado en C++.

1
#ifndef STDSTRING_H

2
#define STDSTRING_H

Este es un protector de inclusión estándar. A continuación, tenemos que incluir estos dos encabezados:

1
#include <string>

2
#include <node.h>

El primero es para la clase std::string y el segundo 'include' es para todas las cosas relacionadas con Node y V8.

Después de eso, podemos declarar nuestra clase:

1
class STDStringWrapper : public node::ObjectWrap {

Para todas las clases que queremos incluir en nuestro complemento, debemos extender la clase node::ObjectWrap.

Ahora podemos empezar a definir propiedades privadas de nuestra clase mediante private:

1
	private:
2
		std::string* s_;
3
4
		explicit STDStringWrapper(std::string s = "");
5
		~STDStringWrapper();

Además del constructor y el destructor, también definimos un puntero a std::string. Este es el núcleo de la técnica que se puede usar para conectar bibliotecas C/C++ con Node. Nosotros definimos un puntero privado a la clase C/C++ y luego operamos en ese puntero en todos los métodos.

Después, declaramos la propiedad estática constructor, que contendrá la función que creará nuestra clase en V8:

1
        static v8::Handle<v8::Value> New(const v8::Arguments& args);

Consulta la documentación de la plantilla v8::Persistent para obtener más información.

Ahora también tendremos un método New, que será asignado al constructor anterior, cuando V8 inicialice nuestra clase:

1
	static v8::Handle New(const v8::Arguments& args);

Cada función para V8 se verá así: aceptará una referencia al objeto v8::Arguments y devolverá un v8::Handle>v8::Value> - así es como V8 trata con JavaScript de manera débil cuando programamos de manera fuerte en C++.

Después de eso, tendremos dos métodos que se insertarán en el prototipo de nuestro objeto:

1
    static v8::Handle<v8::Value> add(const v8::Arguments& args);
2
    static v8::Handle<v8::Value> toString(const v8::Arguments& args);

El método toString() nos permitirá obtener el valor de s_ en lugar de [Object object] cuando lo usemos con cadenas JavaScript normales.

Finalmente, tendremos el método de inicialización (este será llamado por V8 para asignar la función constructor) y podemos cerrar la protección de inclusión:

1
public:
2
	static void Init(v8::Handle<v8::Object> exports);
3
};
4
5
#endif

El objeto exports es equivalente a module.exports en módulos de JavaScript.


El archivo stdstring.cc, constructor y destructor

Ahora crea el archivo stdstring.cc. Primero tenemos que incluir nuestro encabezado:

1
#include "stdstring.h"

Y definir la propiedad constructor (ya que es estática):

1
v8::Persistent<v8::Function> STDStringWrapper::constructor;

El constructor de nuestra clase solo asignará la propiedad s_:

1
STDStringWrapper::STDStringWrapper(std::string s) {
2
	s_ = new std::string(s);
3
}

Y el destructor lo eliminará mediante delete, para evitar una pérdida de memoria:

1
STDStringWrapper::~STDStringWrapper() {
2
	delete s_;
3
}

Además, debes eliminar todo lo que asignes con new, cada vez que exista la posibilidad de que se lance una excepción, así que ten esto en cuenta o usa punteros compartidos.


El método Init

Este método será llamado por V8 para inicializar nuestra clase (asigna el constructor, pone todo lo que queramos usar en JavaScript en el objeto exports):

1
void STDStringWrapper::Init(v8::Handle<v8::Object> exports) {

Primero tenemos que crear una plantilla de función para nuestro método New:

1
v8::Local<v8::FunctionTemplate> tpl = v8::FunctionTemplate::New(New);

Esta es una especie de función nueva en JavaScript como new Function y nos permite preparar nuestra clase de JavaScript.

Ahora podemos establecer el nombre de esta función si queremos (si omites esto, tu constructor será anónimo, tendría la función someName() {} en lugar de function() {}):

1
tpl->SetClassName(v8::String::NewSymbol("STDString"));

Usamos v8::String::NewSymbol() que crea un tipo especial de cadena que se usa para los nombres de las propiedades; esto le ahorra al motor un poco de tiempo.

Después de eso, establecemos cuántos campos tendrá cada instancia de nuestra clase:

1
tpl->InstanceTemplate()->SetInternalFieldCount(2);

Tenemos dos métodos: add() y toString(), así que lo configuramos en 2.

Ahora podemos agregar nuestros métodos al prototipo de la función:

1
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("add"), v8::FunctionTemplate::New(add)->GetFunction());
2
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("toString"), v8::FunctionTemplate::New(toString)->GetFunction());

Esto parece mucho código, pero cuando miras de cerca, verás un patrón allí: usamos tpl->PrototypeTemplate()->Set() para agregar cada uno de los métodos. También le damos a cada uno de ellos un nombre (usando v8::String::NewSymbol()) y una FunctionTemplate.

Finalmente, podemos poner el constructor en la propiedad constructor de nuestra clase y en el objeto exports:

1
    constructor = v8::Persistent<v8::Function>::New(tpl->GetFunction());
2
	exports->Set(v8::String::NewSymbol("STDString"), constructor);
3
}

El método New

Ahora vamos a definir el método que se comportará como JavaScript Objeto.prototype.constructor:

1
v8::Handle<v8::Value> STDStringWrapper::New(const v8::Arguments& args) {

Primero tenemos que crear un alcance para ello:

1
	v8::HandleScope scope;

Después de eso, podemos usar el método .IsConstructCall() del objeto args para verificar si la función constructora fue llamada usando la palabra clave new:

1
	if (args.IsConstructCall()) {

Si es así, primero convierte el argumento pasado a std::string así:

1
    v8::String::Utf8Value str(args[0]->ToString());
2
    std::string s(*str);

... para que podamos pasarlo al constructor de nuestra clase contenedora:

1
		STDStringWrapper* obj = new STDStringWrapper(s);

Después de eso, podemos usar el método .Wrap() del objeto que creamos (que se hereda de node::ObjectWrap) para asignarlo a la variable this:

1
		obj->Wrap(args.This());

Finalmente, podemos devolver el objeto recién creado:

1
		return args.This();

Si la función no fue llamada usando new, simplemente invocaremos al constructor como sería. A continuación, creemos una constante para el recuento de argumentos:

1
	} else {
2
		const int argc = 1;

Ahora creemos una matriz con nuestro argumento:

1
v8::Handle<v8::Value> STDStringWrapper::add(const v8::Arguments& args) {

Y pasa el resultado del método constructor constructor->NewInstance a scope.Close, para que el objeto se pueda usar más tarde (scope.Close básicamente te permite preservar el identificador de un objeto moviéndolo al alcance superior; así es como trabajan las funciones ):

1
		return scope.Close(constructor->NewInstance(argc, argv));
2
	}
3
}

El método add

Ahora creemos el método add que te permitirá agregar algo al std::string interno de nuestro objeto:

1
v8::Handle<v8::Value> STDStringWrapper::add(const v8::Arguments& args) {

Primero tenemos que crear un alcance para nuestra función y convertir el argumento a std::string como hicimos antes:

1
	v8::HandleScope scope;
2
3
	v8::String::Utf8Value str(args[0]->ToString());
4
	std::string s(*str);

Ahora tenemos que desenvolver el objeto. Esto es lo contrario del ajuste que hicimos anteriormente; esta vez obtendremos el puntero a nuestro objeto desde la variable this:

1
STDStringWrapper* obj = ObjectWrap::Unwrap<STDStringWrapper>(args.This());

Luego podemos acceder a la propiedad s_ y usar su método .append():

1
	obj->s_->append(s);

Finalmente, devolvemos el valor actual de la propiedad s_ (nuevamente, usando scope.Close):

1
	return scope.Close(v8::String::New(obj->s_->c_str()));
2
}

Dado que el método v8::String::New() acepta solo char pointer como valor, tenemos que usar obj->s_->c_str() para obtenerlo.


El método toString

El último método necesario nos permitirá convertir el objeto a un String de JavaScript:

1
v8::Handle<v8::Value> STDStringWrapper::toString(const v8::Arguments& args) {

Es similar al anterior, tenemos que crear el alcance:

1
	v8::HandleScope scope;

Desenvuelve el objeto:

1
STDStringWrapper* obj = ObjectWrap::Unwrap<STDStringWrapper>(args.This());

Y devuelve la propiedad s_ como v8::String:

1
	return scope.Close(v8::String::New(obj->s_->c_str()));
2
}

Compilando

Lo último que debemos hacer antes de usar nuestro complemento es, por supuesto, compilar y vincular. Implicará solo dos comandos. Primero:

1
node-gyp configure

Esto creará la configuración de compilación adecuada para tu sistema operativo y procesador (Makefile en UNIX y vcxproj en Windows). Para compilar y vincular la biblioteca, simplemente llama a:

1
node-gyp build

Si todo va bien, deberías ver algo como esto en tu consola:

Y debería haber un directorio build creado en la carpeta de tu complemento.

Pruebas

Ahora podemos probar nuestro complemento. Crea un archivo test.js en la carpeta de tu complemento y requiere la biblioteca compilada (puedes omitir la extensión .node):

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

A continuación, crea una nueva instancia de nuestro objeto:

1
var test = new addon.STDString('test');

Y haz algo con él, como agregarlo o convertirlo en una cadena:

1
test.add('!');
2
console.log('test\'s contents: %s', test);

Esto debería resultar en algo como lo siguiente en la consola, después de ejecutarlo:

Conclusión

Espero que después de leer este tutorial ya no pienses que es difícil crear, compilar y probar complementos personalizados de Node.js basados en bibliotecas C/C++. Con esta técnica, puedes migrar casi cualquier biblioteca C/C++ a Node.js con facilidad. Si lo deseas, puedes agregar más funciones al complemento que creamos. Hay muchos métodos en std::string para que practiques.


Enlaces útiles

Consulta los siguientes recursos para obtener más información sobre el desarrollo de complementos en Node.js, V8 y la biblioteca de bucles de eventos C.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.