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.