Пишем модуль на C++ для nodejs на примере работы с MySQL

в 11:35, , рубрики: c++, mysql, node.js, nodejs, метки: , , ,

Пишем модуль на C++ для nodejs на примере работы с MySQL

Введение

Многие уже успели попробовать 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

приводятся к V8 Number который соответствует JavaScript Number.

Поля с типами:

  • MYSQL_TYPE_TINY_BLOB
  • MYSQL_TYPE_MEDIUM_BLOB
  • MYSQL_TYPE_LONG_BLOB
  • MYSQL_TYPE_BLOB

проверяются на бинарность и если это бинарыне BLOB'ы то преобразуются в Buffer, если нет то в V8 String.

А поля с типами:

  • MYSQL_TYPE_TIMESTAMP
  • MYSQL_TYPE_DATE
  • MYSQL_TYPE_TIME
  • MYSQL_TYPE_DATETIME
  • MYSQL_TYPE_YEAR
  • MYSQL_TYPE_NEWDATE

было решено преобразовывать в V8 Date. И вот тут возникла загвоздка. Для создания объекта V8 Date нужно передать unix timestamp а mysql возвращает поле в формате YYYY-MM-DD HH:MM:SS. Писать разбор строки и дальнейшее преобразование не хотелось. За одно вспомнилось, что сам по себе JavaScript отлично преобразует такую запись в unix timestamp. А раз уж нам доступен V8 то почему бы им и не воспользоватся. Для этого мы создали объект script класса V8 Script, делаем это при помощи метода Script::Compile в который передаем строку скрипта new Date(Date.parse(значение_поля_mysql)). После чего вызываем метод Run() который вернёт нам объект полученный при выполнение JavaScript кода. А мы в свою очередь положим его в наш V8 Array rows. Может и не очень красиво зато довольно интересно.
Теперь осталось всё это скомпилировать, для этого нам нужно создать файл 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
)

Итог:

  1. Мы получили готовый для использования модуль, может не самый лучший, но он делает то, что нам нужно это иногда самое важное
  2. Научились писать свои модули на С++ для Node.js
  3. Научились из модулей вызывать произвольный JavaScript код
  4. Можем написать любой другой модуль, если нам это понадобится
  5. Приобрели опыт работы с объектами V8 JavaScript

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

Автор: VBKesha

Поделиться

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