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

в 10:58, , рубрики: c++, node.js, windows, Песочница, метки: , ,

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, а опишу что-нибудь менее ёмкое. Кому интересно — прошу под кат.

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

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

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

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 отработал, вызывается worker, которая уже может вернуть данные в 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 ]

Это моя первая статья, прошу сильно не ругать.
Если есть вопросы, прошу, задавайте!
Ссылки

Автор: dixoNich

Источник

Поделиться

* - обязательные к заполнению поля