Статьи

Написание аддонов Node.js

Node.js отлично подходит для написания вашего бэкенда на 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 не будет работать)
  • сделать
  • набор инструментов компилятора C ++ (например, 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 (Windows 7/8)

Экспресс-версия Visual Studio работает нормально.


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

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

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


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

1
2
#ifndef STDSTRING_H
#define STDSTRING_H

Это стандартная защита . Далее мы должны включить эти два заголовка:

1
2
#include <string>
#include <node.h>

Первый предназначен для std::string а второй — для всего, что связано с Node и V8 .

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

1
class STDStringWrapper : public node::ObjectWrap {

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

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

1
2
3
4
5
private:
       std::string* s_;
 
       explicit STDStringWrapper(std::string s = «»);
       ~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
2
static v8::Handle<v8::Value> add(const v8::Arguments& args);
   static v8::Handle<v8::Value> toString(const v8::Arguments& args);

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

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

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

Объект exports эквивалентен module.exports в модулях JavaScript.


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

1
#include «stdstring.h»

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

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

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

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

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

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

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


Этот метод будет вызван 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.

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

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

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

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

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

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

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

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

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

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

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

Теперь мы определим метод, который будет действовать как 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
2
v8::String::Utf8Value str(args[0]->ToString());
   std::string s(*str);

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

1
STDStringWrapper* obj = new STDStringWrapper(s);

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

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

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

1
return args.This();

Если функция не была вызвана с использованием new , мы просто вызовем конструктор, как это было бы. Далее, давайте создадим константу для количества аргументов:

1
2
} else {
       const int argc = 1;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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
2
return scope.Close(v8::String::New(obj->s_->c_str()));
}

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

1
node-gyp configure

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

1
node-gyp build

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

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

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

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

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

1
var test = new addon.STDString(‘test’);

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

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

Это должно привести к чему-то вроде следующего в консоли после запуска:

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


Посетите следующие ресурсы для получения дополнительной информации о разработке дополнений Node.js, V8 и библиотеке циклов событий C.