Пользователь
0,1
рейтинг
8 октября 2012 в 15:35

Разработка → Пишем модуль на C++ для nodejs на примере работы с MySQL из песочницы tutorial


Введение


Многие уже успели попробовать 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
карма
7,7
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • +5
    И ни одного упоминания
    github.com/Sannis/node-mysql-libmysqlclient
    написанного Sannis
  • +5
    найти тот, который позволяет выполнять запросы синхронно так и не удалось

    Потому что это никогда не бывает нужно.

    Я вам честно скажу: пытаться программировать синхронно в node.js — это ошибка #1 начинающих разработчиков в node.js. Этот путь никуда не приведет, это точно тупик. Нужно расслабиться и попробовать впитать в себя идеологию асинхронного программирования. Это непросто, но когда это произойдет — вам будет значительно комфортнее работать в node.js.
    • +2
      Хочу добавить, что последовательное выполнение чего-либо отлично реализуется при помощи async.series или async.forEachSeries, если этого очень хочется. А потом, глядишь, и какие-то шаги можно через parallel запускать — ещё и быстрее будет.
  • 0
    Но аргументы в функцию передаются объектом V8 Arguments, покопавшись в его описании, находим, что он хранит ссылку на объект, который его передал. Чтобы её получить используем метод Holder(), после чего получаем скрытое поле со структурой MYSQL и при помощи метода GetIndexedPropertiesExternalArrayData() получаем указатель на на саму структуру.
    Главное использовать именно args->Holder(), а не args->This().
    • 0
      Ну и вдогонку: MYSQL_TYPE_LONGLONG может не уместиться в Number, а Date.parse можно из контекста взять, а не создавать новый скрипт на каждое значение.
      • –1
        А можно пример как взять из контекста Date.parse?
        И ещё вопрос можно ли как-то отследить удаление объекта?
        • +1
          В исходном коде node-mysqlclient есть. Как раз, помню, это был самый долгий открытый issue :)
        • 0
          Ну что ж вы никак по конкретным ссылкам, которые вам давно советуют, не перейдёте?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.