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 работает нормально.
Файл binding.gyp
Этот файл используется 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 , который будет содержать наш класс-оболочку.
Класс STDStringWrapper
Мы начнем с определения нашего класса в файле 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
Теперь создайте файл 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 , каждый раз, когда есть вероятность, что будет сгенерировано исключение, так что имейте это в виду или используйте общие указатели .
Метод 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.
Теперь мы можем установить имя этой функции, если захотим (если вы пропустите это, ваш конструктор будет анонимным, он будет иметь 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);
}
|
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
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
Теперь давайте создадим метод 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() для его получения.
Метод toString
Последний необходимый метод позволит нам преобразовать объект в 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.