Пишем асинхронный модуль для node.js с помощью C++

Node.js развивается, и, вполне уже можно экспериментировать с написанием графических приложений либо каких-то консольных утилит и сервисов. В процессе разработки может возникнуть необходимость использовать какие-то системные вызовы, например, к WMI (к WMI нельзя обратиться напрямую из node.js, и запросы WMI могут быть долгими, что заблокирует event loop, и, например, если связь у Вас через веб-сокеты, связь может оборваться). Тут существует несколько вариантов. Можно воспользоваться модулем (например, node-ffi) и попробовать поиграться с ним. Есть ещё способ, точнее, костыль. В Windows существует так называемый WScript (Windows Script Host) — это компонент Windows, предназначенный для запуска, например, JScript, VBScript. JScript может обращаться к WMI напрямую, так что мы имеем возможность запустить child_process, в котором будет работать JScript, и получать от него данные (формировать, например, JSON и отправлять его строкой), но это костыль, бессмысленный и беспощадный. И третий способ — это нативный модуль. Я не буду описывать, как получить данные от WMI, а опишу что-нибудь менее ёмкое. Кому интересно — прошу под кат.

UPD: Настройка окружения

Я использовал для компиляции и настройки VS2010.

Сначала скачиваем исходники node.js, распаковываем и запускаем vcbuild.bat, который лежит в корне. vcbuild.bat создает необходимые проекты для Visual Studio и конфиги. Для работы батника потребуется Python 2.7. Далее устанавливаем node-gyp командой npm install node-gyp.

Теперь создадим проект в Visual Studio (CLR Empty Project), заходим в свойства проекта, тип конфигурации меняем на .dll, расширение на .node, и ставим поддержку CLR-среды. Переходим в раздел каталоги, в каталогах включения добавляем пути до следующих папок
node-v0.8.15\deps\v8\include

node-v0.8.15\deps\uv\include

node-v0.8.15\src


И теперь добавим файл исходного кода. На данном этапе можно выйти из VS (по желанию) и открыть свой любимый Notepad/Sublime/WebStorm.
Теперь перейдём в каталог с исходниками и создадим там файл binding.gyp, этот файл подскажет утилите node-gyp, как и из чего собирать наше приложение. Для моего примера он очень простой и понятный.
{ 
"targets": [ 
                     {
                       "target_name": "getSummAsync",  
  	                "sources": [ "async.cpp" ] 
	             } 
	           ] 
}


Теперь можем компилировать. Прописываем в консоли в директории с binding.gyp строчку node-gyp configure и потом node-gyp build
Теперь наш скомпилированный модуль будет лежать в папке build/Release

Сам пример

Я не буду использовать какие-то системные вызовы, т.к. смысла особого в этом нет, это лишь усложнит пример. И так, начнём.

Для примера мы будем передавать массив целых чисел, считать его сумму, получать положительные элементы и возвращать их пользователю.

Для начала объявим структуру, в которой, в свою очередь, объявим нужные нам структуры данных.

struct Summ_req
{
	vector<int> numbers;
	vector<int> gtz;
	int result;
    Persistent<Function> callback;
};

Это вектор, в котором мы будем хранить наши числа.
vector<int> numbers;

Вектор, в котором будем хранить числа больше нуля.
vector<int> gtz;

Здесь мы будем хранить результат
int result;


Важно понимать, зачем мы будем использовать vector, хотя, вроде бы, можно обойтись стандартными шаблонами v8. Но это не так. Об этом чуть ниже.

В модуле будет 3 функции, основная, которую мы вызываем из node.js, и две другие, которые, собственно, и делают наш модуль асинхронным.
Функции work
getSummAsync принимает два аргумента, наш массив элементов и callback. Проверяем, верны ли параметры, с которыми вызвана функция, и, если верны, кастомизируем их, то есть, чтобы уметь общаться с аргументами, их надо привести к нужному типу.

Local<Function> callback = Local<Function>::Cast(args[1]);
Local<Array> numbers = Local<Array>::Cast(args[0]);


Далее инициализируем структуру и передадим в неё наш callback и массив записываем в вектор.
Summ_req* request = new Summ_req;
request->callback = Persistent<Function>::New(callback);
for (size_t i = 0; i < numbers->Length(); i++) {
	request->numbers.push_back(numbers->Get(i)->Int32Value());
}


Persistent желательно, т.к. всё-таки наш callback используется не только в этой функции.

И запускаем наш воркер в очередь.
uv_queue_work(uv_default_loop(), req, Worker, After);

getSummAsync
static Handle<Value> getSummAsync (const Arguments& args)
{

    HandleScope scope;

    if (args.Length() < 2 || !args[0]->IsArray())
    {
        return ThrowException(Exception::TypeError(String::New("Bad arguments")));
    }


    if (args[1]->IsFunction())
    {
        Local<Function> callback = Local<Function>::Cast(args[1]);
    	Local<Array> numbers = Local<Array>::Cast(args[0]);

        Summ_req* request = new Summ_req;
        request->callback = Persistent<Function>::New(callback);
    	for (size_t i = 0; i < numbers->Length(); i++) {
    		request->numbers.push_back(numbers->Get(i)->Int32Value());
    	}
        uv_work_t* req = new uv_work_t();
        req->data = request;

        uv_queue_work(uv_default_loop(), req, Worker, After);
    }
    else
    {
        return ThrowException(Exception::TypeError(String::New("Callback missing")));
    }

    return Undefined();
}


В функции Worker, думаю, всё понятно. Считаем числа и возвращаем результаты в структуру. Теперь о том, почему мы используем вектор, а не средства v8. Функция Worker работает в отдельном потоке, а node.js и v8 позволяют лишь один поток для выполнения js, то есть нельзя создать массив v8 в отдельном потоке.

Worker
void Worker(uv_work_t* req)
{
    Summ_req* request = (Summ_req*)req->data;
    request->result = 0;
    for (vector<int>::iterator it = request->numbers.begin(); it != request->numbers.end(); ++it) {
   		request->result += *it;
   		if (*it > 0) {
   			request->gtz.push_back(*it);
   		}
    }
   // request->result = request->int1 + request->int2;
}


Теперь функция After. После того, как Worker отработал, вызывается функция After, которая уже может вернуть данные в node.js.
Здесь, а не в функции Worker, мы получим результирующий массив, по причине, о которой я говорил выше.
 Handle<Value> argv[2];

Сюда мы поместим возвращаемые значения
 request->callback->Call(Context::GetCurrent()->Global(), 2, argv);

И вызовем наш callback с параметрами, которые записали в argv.
After
void After(uv_work_t* req)
{
    HandleScope scope;

    Summ_req* request = (Summ_req*)req->data;
    delete req;

    Handle<Value> argv[2];

    argv[0] = Integer::New(request->result);
    Local<Array> gtz = Array::New();
    size_t i = 0;
    for (vector<int>::iterator it = request->gtz.begin(); it != request->gtz.end(); ++it) {
    	gtz->Set(i, Integer::New(*it));
    	i++;
    }
    argv[1] = gtz;
    TryCatch try_catch;

    request->callback->Call(Context::GetCurrent()->Global(), 2, argv);

    if (try_catch.HasCaught()) 
    {
        FatalException(try_catch);
    }

    request->callback.Dispose();

    delete request;
}


Теперь можем вызвать из node.js наш модуль, предварительно скомпилировав его с помощью утилиты node-gyp.
var foo = require('./getSummAsync.node')

foo.getSummAsync([1,2,3,6,-5],function(a, b){
  console.log(a, b);
});

Результат
7 [ 1, 2, 3, 6 ]


Это моя первая статья, прошу сильно не ругать.
Если есть вопросы, прошу, задавайте!
Ссылки
  • +25
  • 11.7k
  • 5
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 5

    0
    Было бы здорово в начале немного подробнее изложить общие моменты интеграции и настройку окружения (IDE и компилятор).
      +2
      Добавил в самое начало.
        0
        Спасибо, большое! Для меня это очень полезно.
      0
      Очень было бы хорошо выложить проект на гитхабе, а здесь дать ссылку
      чтоб иметь полное представление о модуле.
      А так… если что-то не учел, то ничего не получится и статья ушла в карзину.

      >Я использовал для компиляции и настройки VS2010.
      Не единым окном живет человечество, многие разработчики седят под линуксом или иной осью, по этому хорошо бы как минимум дать ссылку на компилирование под другие ОС
        0
        >Сам пример
        все хорошо, но я бы сперва перед описанием примера описал бы, как будет организована асинхронная обработка в целом,
        для более понятного восприятия, а потом перешел бы уже к деталям. Когда я был студентом, нас учили писать научные работы (статья мало чем отличается от научной работы) по следующему плану, пригодится в будущем ;):
        — введение в проблему,
        — если есть то обзор существующих решений
        — описание решения проблемы в целом
        — частные описания и конкретные предложения, действия и т.д
        — возможные выводы и заключение

        Данный комментарий прошу не считать как критику, а как пожелание в дальнейшей работе
        Если приглядеться, то большинство авторов статей с рейтингом придерживается такого плана.

        В целом статья полезная, спасибо.

        Only users with full accounts can post comments. Log in, please.