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.