Advertisement
  1. Code
  2. Node.js

Пишем аддоны для Node.js

Scroll to top
Read Time: 8 min

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Node.js отлично подходит для написания вашего back-end в JavaScript. Но что, если вам нужна некоторая функциональность, которая не предоставляется из коробки, или которая также не может быть выполнена даже с использованием модулей, но доступна в виде библиотеки C / C ++? Ну, вы можете написать аддон, который позволит вам использовать эту библиотеку в вашем JavaScript-коде. Посмотрим, как.

Как вы можете прочитать в документации Node.js, аддоны представляют собой динамически связанные общие объекты, которые могут обеспечить связь для библиотек C/C++. Это означает, что вы можете использовать практически любую библиотеку C/C ++ и создать аддон, который позволит вам использовать его в Node.js.

В качестве примера мы создадим оболочку для стандартного объекта std::string.


Подготовка

Прежде чем мы начнем писать код, вы должны убедиться, что у вас есть все необходимое для компиляции модуля. Вам нужен node-gyp и все его зависимости. Вы можете установить node-gyp, используя следующую команду:

1
npm install -g node-gyp

Что касается зависимостей, то в системах Unix вам понадобятся:

  • Python (2.7, 3.x не будет работать)
  • make
  • C ++ компилятор toolchain (например, gpp или g ++)

Например, на Ubuntu вы можете установить все это с помощью этой команды (Python 2.7 уже должен быть установлен):

1
sudo apt-get install build-essentials

В Windows вам понадобятся:

  • Python (2.7.3, 3.x не работает)
  • Microsoft Visual Studio C ++ 2010 (для Windows XP / Vista)
  • Microsoft Visual Studio C ++ 2012 для Windows Desktop (Windows 7/8)

Express версия Visual Studio отлично работает.


Файл binding.gyp

Этот файл используется node-gyp для создания соответствующих файлов сборки для вашего аддона. Вся документация файла .gyp может быть найдена на их странице Wiki, но для наших целей этот простой файл будет делать:

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

target_name может быть любым именем, которое вам нравится. Массив sources содержит все исходные файлы, которые использует аддон. В нашем примере есть addon.cc, который будет содержать код, необходимый для компиляции нашего аддона и stdstring.cc, который будет содержать наш класс-оболочку.


Класс STDStringWrapper

Начнем с определения нашего класса в файле stdstring.h. Первые две строки должны быть вам знакомы, если вы когда-либо программировали на C ++.

1
#ifndef STDSTRING_H

2
#define STDSTRING_H

Это стандарт include guard. Затем мы должны подключить эти два заголовка:

1
#include <string>

2
#include <node.h>

Первый - для класса std::string, а второй - для всех вещей, связанных с Node и V8.

После этого мы можем объявить наш класс:

1
class STDStringWrapper : public node::ObjectWrap {

Для всех классов, которые мы хотим включить в наш аддон, мы должны расширить класс node::ObjectWrap.

Теперь мы можем начать определять private свойства нашего класса:

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

Помимо конструктора и деструктора, мы также определяем указатель на std::string. Это ядро метода, который можно использовать для склеивания библиотек C/C ++ в Node - мы определяем приватный указатель на класс C/C++, а затем используем этот указатель во всех методах.

Затем мы объявляем статическое свойство constructor, которое будет содержать функцию для создания нашего класса в V8:

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

Для получения дополнительной информации о шаблоне v8::Persistent обратитесь к документации.

Теперь у нас также будет метод New, который будет назначен constructor выше, когда V8 инициализирует наш класс:

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

Каждая функция для V8 будет выглядеть так: она примет ссылку на объект v8 :: Arguments и вернет v8 :: Handle> v8 :: Value> - вот как V8 имеет дело со слабо типизированным JavaScript, когда мы программируем в сильном -тип C ++.

После этого у нас будет два метода, которые будут вставлены в прототип нашего объекта:

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

Метод toString() позволит нам получить значение s_ вместо [Object object], когда мы будем использовать его с обычными строками JavaScript.

Наконец, у нас будет метод инициализации (он будет вызываться V8 для назначения функции constructor), и мы можем закрыть include guard:

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

Объект exports эквивалентен module.export.


Файл stdstring.cc, конструктор и деструктор

Теперь создайте файл stdstring.cc. Сначала мы должны включить наш заголовок:

1
#include "stdstring.h"

И определите свойство constructor (поскольку оно статично):

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

Конструктор для нашего класса просто выделит свойство s_:

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

А деструктор delete его, чтобы избежать утечки памяти:

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

Кроме того, вы должны delete все, что вы создаете с помощью new, каждый раз, когда есть вероятность, что будет выброшено исключение, помните об этом или используйте общие указатели.


Метод Init

Этот метод будет вызываться V8 для инициализации нашего класса (определение constructor, поместите все, что мы хотим использовать в JavaScript в объекте exports):

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

Сначала мы должны создать шаблон функции для нашего метода New:

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

Это похоже на new Function в JavaScript - позволяет нам подготовить наш класс JavaScript.

Теперь мы можем установить имя этой функции, если хотим (если вы опустите это, ваш конструктор будет анонимным, он будет иметь функцию someName () {} вместо function () {}):

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

Мы использовали v8::String::NewSymbol(), который создает специальный тип строки, используемой для имен свойств - это немного экономит время на работу.

После этого мы задаем, сколько полей будет иметь каждый экземпляр нашего класса:

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

У нас есть два метода - add() и toString(), поэтому мы устанавливаем значение в 2.

Теперь мы можем добавить наши методы к прототипу функции:

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());

Много кода, но при внимательном рассмотрении вы увидите шаблон: мы используем tpl->PrototypeTemplate()->Set(), чтобы добавить каждый из методов. Мы также даем каждому из них имя (используя v8::String::NewSymbol()) и FunctionTemplate.

Наконец, мы можем поместить конструктор в свойство constructor нашего класса и в объект exports:

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

Метод New

Теперь мы определим метод, который будет действовать как объект JavaScript Object.prototype.constructor:

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

Сначала мы должны создать для этого область:

1
	v8::HandleScope scope;

После этого мы можем использовать метод .IsConstructCall() объекта args, чтобы проверить, была ли вызвана функция-конструктор с использованием ключевого слова new:

1
	if (args.IsConstructCall()) {

Если это так, давайте сначала преобразуем аргумент, переданный в std::string следующим образом:

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

... так что мы можем передать его конструктору нашего класса-оболочки:

1
		STDStringWrapper* obj = new STDStringWrapper(s);

После этого мы можем использовать метод .Wrap() объекта, который мы создали (который унаследован от node::ObjectWrap), чтобы присвоить его переменной this:

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

Наконец, мы можем вернуть вновь созданный объект:

1
		return args.This();

Если функция не была вызвана с помощью new, мы просто вызовет конструктор. Затем создадим константу для аргумента count:

1
	} else {
2
		const int argc = 1;

Теперь давайте создадим массив с нашим аргументом:

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

И передайте результат метода constructor->NewInstance в scope.Close, так что объект может быть использован позже (scope.Close в основном позволяет вам сохранить дескриптор объекта, переместив его в более высокую область действия - вот как работают функции ):

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

Метод add

Теперь давайте создадим метод add, который позволит вам добавить что-то во внутреннюю std::string нашего объекта:

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

Сначала мы должны создать область для нашей функции и преобразовать аргумент в std::string, как это было раньше:

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

Теперь нам нужно развернуть объект. Это обратная сторона обертывания, которую мы сделали ранее - на этот раз мы получим указатель на наш объект из переменной this:

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

Затем мы можем получить доступ к свойству s_ и использовать его метод .append():

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

Наконец, мы возвращаем текущее значение свойства s_ (опять же, используя scope.Close):

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

Поскольку метод v8::String::New() принимает только char pointer в качестве значения, мы должны использовать obj->s_->c_str() для его получения.


Метод toString

Последний необходимый метод позволит нам преобразовать объект в JavaScript String:

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

Это похоже на предыдущий метод, мы должны создать область действия:

1
	v8::HandleScope scope;

Разверните объект:

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

И верните свойство s_ как v8::String:

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

Сборка

Последнее, что нужно сделать, прежде чем мы будем использовать наш аддон, конечно же, является компиляцией и сборкой. Это будет включать только две команды. Первая:

1
node-gyp configure

Это создаст соответствующую конфигурацию сборки для вашей ОС и процессора (Makefile в UNIX и vcxproj в Windows). Чтобы скомпилировать и связать библиотеку, просто вызовите:

1
node-gyp build

Если все пойдет хорошо, вы должны увидеть что-то подобное в консоли:

И должен быть создан каталог build, созданный в папке вашего аддона.

Тестирование

Теперь мы можем протестировать наш аддон. Создайте файл test.js в папке вашего аддона и подключите скомпилированную библиотеку (вы можете опустить расширение .node):

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

Затем создайте новый экземпляр нашего объекта:

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

И сделайте что-нибудь с ним, например, добавив или преобразова его в строку:

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

Это приведет к следующему примеру консоли:

Вывод

Надеюсь, что после прочтения этого урока вы больше не будете думать, что создавать, собирать и тестировать собственные библиотеки на базе C/C ++, Node.js сложно. Используя эту технику, вы можете легко переносить практически любую библиотеку C/C++ в Node.js. Если вы хотите, вы можете добавить дополнительные функции в аддон, который мы создали. Существует множество методов в std::string, с которыми вы можете попрактиковаться.


Полезные ссылки

Ознакомьтесь со следующими ресурсами для получения дополнительной информации о Node.js addon development, V8 и библиотеке циклов событий 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.