
Введение
Многие уже успели попробовать Node.js, на мой взгляд, это очень удобный инструмент, для решения большого круга задач. Меня в Node.js, прежде всего, привлекает возможность писать код на JavaScript и большой набор встроенных модулей для решения часто возникающих задач. Если чего-то не оказалось в стандартной поставке, то огромное количество дополнительных модулей можно найти в репозитории npmjs.org
Однако, бывают ситуации, когда всё, что там имеется, работает или не так, как хочется, или вообще не работает в заданных условиях, или же всё куда банальнее — просто отсутствует то, что необходимо для конкретного случая. Мне понадобился модуль, который может синхронно выполнять запросы к MySQL, причём четвёртой версии. Первый испытанный модуль работал исключительно с пятой версией, позже конечно нашлись другие, но найти тот, который позволяет выполнять запросы синхронно так и не удалось.
После изучения документации, я пришёл к выводу что, могу написать нужный мне модуль на C++ и оформить его как addon к node.js, если вам интересно познакомится с процессом создания модуля, добро пожаловать под кат.
Инструменты
Модуль мы будем писать под Linux. Из инструментов нам понадобится:
- mysql
- node
- node-gyp
- GCC
Как всё это ставится в вашем дистрибутиве, вы должны знать сами, за исключением node-gyp он ставится через npm который идёт вместе с установкой node:
npm install node-gypПишем модуль
Итак node.js — это платформа, построенная на JavaScript движке от Google V8. Соответственно, в нашем модуле нам придётся иметь дело с объектами движка V8. Чтобы упростить себе жизнь, желательно иметь какую-ни будь шпаргалку по объектам этого движка, я выбрал вот эту, а также справочник по функциям Си для работы с MySQL, например можно посмотреть здесь.
С чего начинаются модули
Для начала нужно создать файл, в моём случае это будет mysql_sync.cc, после опишем необходимые заголовки и зададим нэймспейс:
#include <node.h> #include <node_buffer.h> #include <v8.h> #include <mysql.h> using namespace v8;
Работа любого модуля для node.js начинается с выполнения макроса NODE_MODULE, в который передаётся имя модуля и имя функции, которая будет выполнятся в момент подключения модуля.
void init(Handle<Object> target) { target->Set(String::NewSymbol("create"), FunctionTemplate::New(create)->GetFunction()); } NODE_MODULE(mysql_sync, init)
Итак, мы видим что, функция init ничего не возвращает, но в неё передаётся объект. Мы добавляем свойство к этому объекту с именем create, при помощи метода Set, имя является объектом класса V8 String и создаётся при помощи статической функции NewSymbol, в которую мы передаём нужную строку. Значением этого свойства будет функция, которая будет создана из функции c именем create.
Всё достаточно просто, но есть одно но, данная функция будет вызвана только один раз при первой загрузке модуля, после чего node прокэширует у себя объект, который получен на выходе из функции init, и больше вызывать её не будет.
Если сейчас дописать функцию create, cкомпилировать модуль, и выполнить следующий код в node
console.log(require('./mysql_sync.node'));
В результате на экране увидим вот такой результат:
{ create: [Function] }
Первый этап готов, переходим к функции create.
Создание объекта нашего модуля
Код функции create ничем сложным не отличается:
Handle<Value> create(const Arguments& args) { HandleScope scope; Handle<Object> ret = Object::New(); node::Buffer *buf; buf = node::Buffer::New((char *)mysql_init(NULL), sizeof(MYSQL)); ret->SetHiddenValue(String::NewSymbol("MYSQL"), buf->handle_); ret->SetHiddenValue(String::NewSymbol("connected"), Boolean::New(0)); ret->Set(String::NewSymbol("connect"), FunctionTemplate::New(connect)->GetFunction()); ret->Set(String::NewSymbol("query"), FunctionTemplate::New(query)->GetFunction()); return scope.Close(ret); }
Возвращаемым значением этого кода является объект класса V8 Value. Этот класс возвращают все функции JavaScript, а также С++, если они вызываются из JavaScript. Создаём новый объект ret, в котором будем хранить свойства возвращаемого функцией объекта. Тут нам, желательно, проинициализировать указатель на структуру MYSQL, которая понадобится для работы с остальными функциями MySQL и как-то её хранить в нашем объекте. Из всего, найденного более всего для хранения структуры подходил объект Buffer, который описан в самой node.js. С помощью конструкции node::Buffer::New мы создали новый объект нужного размера и положили туда проинициализированную структуру MYSQL (я знаю, что тут хорошо было бы проверить возвращаемый результат, но не хочется переусложнять, поэтому дальше будут опущены некоторые проверки).
Для того чтобы хранить MYSQL в нашему объекте но не давать пользователю к ней доступ был выбран вариант хранение структуры в скрытом поле объекта это делается при помощи метода SetHiddenValue, он полностью аналогичен методу Set ��а исключение того что создаёт не обычное свойство, а скрытое, то есть недоступное из JavaScript кода. Также в скрытом поле мы будем хранить свойство connected, оно нам пригодится позже, а сейчас положим в него объект V8 Boolean ( со значением False). После мы добавляем ещё две функции: connect и query. И в конце возвращаем наш объект, вызвавшей его функции, используя scope.Close(ret);
В промежуточном итоге мы полуаем функцию, которая создаёт новый объект, добавляя в него два срытых свойства со служебными данными для этого объекта, и два публичных свойства, в которых хранятся нужные нам функции.
Если сделать заглушки на две указанные функции и выполнить указанный код:
console.log(require('./mysql_sync.node').create());
То получим следующий результат:
{ connect: [Function], query: [Function]}
Методы нашего модуля
Теперь, опишем методы нашего модуля:
Метод connect:
Handle<Value> connect(const Arguments& args) { HandleScope scope; Handle<Object> ret = Object::New(); Handle<Object> err = Object::New(); MYSQL *mysql; bool ok=true; mysql = (MYSQL *)args.Holder()->GetHiddenValue(String::NewSymbol("MYSQL"))-> ToObject()->GetIndexedPropertiesExternalArrayData(); if(args.Length()==4){ for(int i=0; i<4; i++) if(!args[i]->IsString()) ok=false; } else { ok=false; } if(ok == true){ String::AsciiValue host(args[0]->ToString()); String::AsciiValue user(args[1]->ToString()); String::AsciiValue pass(args[2]->ToString()); String::AsciiValue db(args[3]->ToString()); mysql_real_connect(mysql, *host, *user, *pass, *db, 0, NULL, 0); args.Holder()->SetHiddenValue(String::NewSymbol("connected"), Boolean::New(1)); err->Set(String::NewSymbol("id"), Uint32::New(mysql_errno(mysql))); err->Set(String::NewSymbol("text"), String::NewSymbol(mysql_error(mysql))); } else { err->Set(String::NewSymbol("id"), Uint32::New(65535)); err->Set(String::NewSymbol("text"), String::NewSymbol("Incorect parametrs of function")); } ret->Set(String::NewSymbol("err"), err); return scope.Close(ret); }
Тут я столкнулся с первой трудностью, нам явно никак не передаётся объект который вызвал этот метод, а ведь в объекте хранится два скрытых поля которые нам необходимы для дальнейшей работы. Но аргументы в функцию передаются объектом V8 Arguments, покопавшись в его описании, находим, что он хранит ссылку на объект, который его передал. Чтобы её получить используем метод Holder(), после чего получаем скрытое поле со структурой MYSQL и при помощи метода GetIndexedPropertiesExternalArrayData() получаем указатель на на саму структуру. Дальше ничего примечательного в коде нет, идут проверки на то сколько было передано параметров и какого типа. Если всё правильно вызываем функцию mysql_real_connect(), получаем ошибки mysql, создаём объект err и складываем туда ошибки как значения полей. Если параметры не те что ожидали добавляем свою ошибку в объект err. Потом добавляем объект err как поле «err» к объекту ret и возвращаем этот объект.
Метод query:
Handle<Value> query(const Arguments& args) { HandleScope scope; Handle<Object> ret = Object::New(); Handle<Object> err = Object::New(); Handle<Array> rows = Array::New(); Handle <Script> script; Handle<Object> obj_row; node::Buffer *buf; MYSQL *mysql; MYSQL_RES *res; MYSQL_ROW row; MYSQL_FIELD *fields; unsigned int num_fields; bool ok=true; mysql = (MYSQL *)args.Holder()->GetHiddenValue( String::NewSymbol("MYSQL"))->ToObject()->GetIndexedPropertiesExternalArrayData(); if(!args.Holder()->GetHiddenValue(String::NewSymbol("connected"))->BooleanValue()){ err->Set(String::NewSymbol("id"), Uint32::New(65534)); err->Set(String::NewSymbol("text"), String::NewSymbol("You need to connect before any query")); ret->Set(String::NewSymbol("err"), err); ok = false; } if(ok == true){ if(args.Length()!=1){ ok=false; }else{ if(!args[0]->IsString()) ok=false; } if(ok == false){ err->Set(String::NewSymbol("id"), Uint32::New(65535)); err->Set(String::NewSymbol("text"), String::NewSymbol("Incorect parametrs of function")); } } if(ok == true){ String::AsciiValue query(args[0]->ToString()); if(mysql_query(mysql, *query)==0){ res = mysql_store_result(mysql); num_fields = mysql_num_fields(res); fields = mysql_fetch_fields(res); while ( (row = mysql_fetch_row(res)) ){ obj_row = Object::New(); for(unsigned int i=0; i<num_fields; i++){ switch(fields[i].type){ case MYSQL_TYPE_DECIMAL: case MYSQL_TYPE_TINY: case MYSQL_TYPE_SHORT: case MYSQL_TYPE_LONG: case MYSQL_TYPE_LONGLONG: case MYSQL_TYPE_INT24: case MYSQL_TYPE_FLOAT: case MYSQL_TYPE_DOUBLE: obj_row->Set(String::NewSymbol(fields[i].name), Number::New( (row[i])? atof(row[i]):0) ); break; case MYSQL_TYPE_TIMESTAMP: case MYSQL_TYPE_DATE: case MYSQL_TYPE_TIME: case MYSQL_TYPE_DATETIME: case MYSQL_TYPE_YEAR: case MYSQL_TYPE_NEWDATE: script = Script::Compile( String::NewSymbol("")->Concat( String::NewSymbol("")->Concat( String::NewSymbol("new Date(Date.parse('"), String::NewSymbol( (row[i])? row[i]:"" ) ), String::NewSymbol("'))")) ); obj_row->Set(String::NewSymbol(fields[i].name), script->Run()); break; case MYSQL_TYPE_TINY_BLOB: case MYSQL_TYPE_MEDIUM_BLOB: case MYSQL_TYPE_LONG_BLOB: case MYSQL_TYPE_BLOB: if((fields[i].flags & BINARY_FLAG)){ buf = node::Buffer::New(row[i], mysql_fetch_lengths(res)[i]); obj_row->Set(String::NewSymbol(fields[i].name), buf->handle_); break; } default: obj_row->Set(String::NewSymbol(fields[i].name), String::NewSymbol( (row[i])? row[i]:"") ); break; } } rows->Set(rows->Length(),obj_row); } mysql_free_result(res); }; ret->Set(String::NewSymbol("inserted_id"), Uint32::New(mysql_insert_id(mysql))); ret->Set(String::NewSymbol("info"), String::NewSymbol( (mysql_info(mysql)) ? mysql_info(mysql) :"" )); ret->Set(String::NewSymbol("affected_rows"), Uint32::New(mysql_affected_rows(mysql))); err->Set(String::NewSymbol("id"), Uint32::New(mysql_errno(mysql))); err->Set(String::NewSymbol("text"), String::NewSymbol(mysql_error(mysql))); } ret->Set(String::NewSymbol("err"), err); ret->Set(String::NewSymbol("rows"), rows); return scope.Close(ret); }
От функции query мне изначально хотелось получать полный результат выборки в а не вытаскивать его по одной строке, поэтому после всех проверок на входящие параметры, и на установленность соединения. Мы выполняем запрос, и ответ складываем в объект V8 Array rows. Каждая строка ответа заносится в объект, именами свойств которого являются имена полей результата запроса, а значениями собственно полученные данные. Изначально я делал так, все данные преобразовывались в V8 String, но потом захотелось более удобного результата.
В итоге было принято решение, что поля c типами:
- MYSQL_TYPE_DECIMAL
- MYSQL_TYPE_TINY
- MYSQL_TYPE_SHORT
- MYSQL_TYPE_LONG
- MYSQL_TYPE_LONGLONG
- MYSQL_TYPE_INT24
- MYSQL_TYPE_FLOAT
- MYSQL_TYPE_DOUBLE
Поля с типами:
- MYSQL_TYPE_TINY_BLOB
- MYSQL_TYPE_MEDIUM_BLOB
- MYSQL_TYPE_LONG_BLOB
- MYSQL_TYPE_BLOB
А поля с типами:
- MYSQL_TYPE_TIMESTAMP
- MYSQL_TYPE_DATE
- MYSQL_TYPE_TIME
- MYSQL_TYPE_DATETIME
- MYSQL_TYPE_YEAR
- MYSQL_TYPE_NEWDATE
Теперь осталось всё это скомпилировать, для этого нам нужно создать файл binding.gyp с таким содержанием:
Прошу обратить внимание, на то что здесь указаны странные пути (у вас они, возможно, будут другими), чтобы их получить можно воспользоваться командой:{ "targets": [ { "target_name": "mysql_sync", "sources": [ "mysql_sync.cc" ], "include_dirs": [ '/server/daemons/mysql/include/mysql/' ], "link_settings": { 'libraries': ['-lmysqlclient -L/server/daemons/mysql/lib/mysql/'], 'library_dirs': ['/server/daemons/mysql/lib/mysql/'], }, } ] }
mysql_config --include --libsТеперь осталось выполнить:
node-gyp configurenode-gyp buildcp build/Release/mysql_sync.node ./Наш модуль готов к использованию, для теста напишем следующий код:
var mysql = require('./mysql_sync.node').create(); console.log(mysql.connect("localhost", "login", "pass", "test")); console.log(mysql.query("select * from tmp");
При наличии такого пользователя, базы и таблицы получим примерно такой результат.
Результат для таблицы созданной при помощи:{ err: { id: 0, text: '' } } { inserted_id: 0, info: '', affected_rows: 1, err: { id: 0, text: '' }, rows:[ { number: 1558.235, varchar: 'test1', text: 'blob text2, blod: <SlowBuffer 31>, date: Wed Oct 03 2012 00:00:00 GMT+0400 (MSK), boolean: 1, tst: <SlowBuffer > } , { number: 2225, varchar: 'test2', text: 'blob text2, blod: <SlowBuffer 32>, date: Wed Oct 04 2012 00:00:00 GMT+0400 (MSK), boolean: 0, tst: <SlowBuffer > } ] }
CREATE TABLE `tmp` ( `number` double NOT NULL default '0', `varchar` varchar(10) NOT NULL default '', `text` text NOT NULL, `blod` longblob NOT NULL, `date` datetime NOT NULL default '0000-00-00 00:00:00', `boolean` tinyint(1) NOT NULL default '0', `tst` longblob )
Итог:
- Мы получили готовый для использования модуль, может не самый лучший, но он делает то, что нам нужно это иногда самое важное
- Научились писать свои модули на С++ для Node.js
- Научились из модулей вызывать произвольный JavaScript код
- Можем написать любой другой модуль, если нам это понадобится
- Приобрели опыт работы с объектами V8 JavaScript
Статья получилась длиннее, чем я ожидал, но думаю, что многим она может пригодиться при написании своего модуля, или при попытке разобраться, что происходит в модулях других разработчиков что, иногда, не менее важно. Спасибо за внимание.
