Введение
Многие уже успели попробовать 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 configure
node-gyp build
cp 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
Статья получилась длиннее, чем я ожидал, но думаю, что многим она может пригодиться при написании своего модуля, или при попытке разобраться, что происходит в модулях других разработчиков что, иногда, не менее важно. Спасибо за внимание.