📜 ⬆️ ⬇️

Writing a module in C ++ for nodejs using the example of working with MySQL


Introduction


Many have already managed to try Node.js, in my opinion, this is a very convenient tool for solving a wide range of tasks. I am most attracted to Node.js by the ability to write JavaScript code and a large set of built-in modules to solve common problems. If something was not in the standard package, then a huge number of additional modules can be found in the npmjs.org repository .

However, there are situations when everything that is there, works or not as you like, or does not work at all in the given conditions, or everything is more banal - just what is necessary for a particular case is missing. I needed a module that can synchronously perform queries to MySQL, with the fourth version. The first tested module worked exclusively with the fifth version; later, of course, there were others, but it was not possible to find the one that allows you to execute queries synchronously.

After studying the documentation, I came to the conclusion that I can write the module I need in C ++ and issue it as addon to node.js, if you are interested in the module creation process, welcome under cat.

Instruments


We will write the module under Linux. From the tools we need:

How all this is put in your distribution, you should know for yourself, except for node-gyp, it is put through npm which comes with the node installation:
npm install node-gyp

Writing a module


So node.js is a platform built on a JavaScript engine from Google V8. Accordingly, in our module we will have to deal with objects of the V8 engine. To simplify your life, it is advisable to have some kind of cheat sheet on the objects of this engine, I chose this one , as well as a reference book on C functions for working with MySQL, for example, you can look here .
')
How do modules start

First you need to create a file, in my case it will be mysql_sync.cc , after we describe the necessary headers and set the namespace:
 #include <node.h> #include <node_buffer.h> #include <v8.h> #include <mysql.h> using namespace v8; 

The operation of any module for node.js begins with the execution of the NODE_MODULE macro, to which the module name and the function name will be passed, which will be executed when the module is connected.
 void init(Handle<Object> target) { target->Set(String::NewSymbol("create"), FunctionTemplate::New(create)->GetFunction()); } NODE_MODULE(mysql_sync, init) 

So, we see that, the init function returns nothing, but an object is passed to it. We add a property to this object with the name create , using the Set method, the name is an object of the V8 String class and created using the static NewSymbol function, into which we pass the required string. The value of this property will be a function that will be created from a function with the name create .
Everything is quite simple, but there is one thing, this function will be called only once when the module is first loaded, after which the node will cache the object that was received at the output of the init function, and will not call it anymore.
If now to add the create function, compile the module, and execute the following code in node
 console.log(require('./mysql_sync.node')); 

As a result, we will see the following result on the screen:
 { create: [Function] } 

The first stage is ready, go to the create function.

Creating the object of our module

The code of the create function is no different:
 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); } 

The return value of this code is an object of class V8 Value. This class is returned by all JavaScript functions, as well as C ++, if they are called from JavaScript. Create a new ret object in which we will store the properties of the object returned by the function. Here, it is desirable for us to initialize a pointer to the MYSQL structure, which will be needed to work with the rest of MySQL functions and somehow store it in our object. Of all that, the Buffer object, which is described in the node.js itself, was the most found for storing the structure. Using the node :: Buffer :: New construction, we created a new object of the required size and put an initialized MYSQL structure there (I know that it would be good to check the returned result, but I don’t want to over-complicate, so some checks will be omitted later) .
In order to store MYSQL in our object but not to give the user access to it, the option of storing the structure in the hidden field of the object was selected using the SetHiddenValue method; it is completely similar to the Set method except for what it creates not a regular property, but a hidden one inaccessible from javascript code. We will also store the connected property in a hidden field, it will come in handy later, and now we will put a V8 Boolean object in it (with the value False). After we add two more functions: connect and query . And at the end we return our object, the function that called it, using scope.Close (ret);
In the intermediate result, we obtain a function that creates a new object, adding to it two hidden properties with service data for this object, and two public properties that store the functions we need.
If you make a stub on the two specified functions and execute the specified code:
 console.log(require('./mysql_sync.node').create()); 

Then we get the following result:
 { connect: [Function], query: [Function]} 

Methods of our module

Now, we will describe the methods of our module:
Connect method:
 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); } 

Then I ran into the first difficulty, we obviously don’t pass the object that caused this method, but the object contains two hidden fields that we need for further work. But the arguments to the function are passed by the V8 Arguments object; after digging into its description, we find that it stores a reference to the object that passed it. To get it, we use the Holder () method, after which we get a hidden field with the MYSQL structure and, using the GetIndexedPropertiesExternalArrayData () method, we get a pointer to the structure itself. Then there is nothing remarkable in the code, there are checks on how many parameters were transmitted and what type. If everything correctly calls the mysql_real_connect () function, we get mysql errors, create an err object and add errors to it as field values. If the parameters are not what we expected, we add our error to the err object. Then we add the err object as the “err” field to the ret object and return this object.

Query method:
 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); } 

From the query function, I initially wanted to get the full result of the sample in and not pull it out one line at a time, so after all the checks for incoming parameters, and for the connection. We execute the query, and add the response to the V8 object. Array rows . Each response line is entered into an object, whose property names are the names of the fields of the query result, and the values ​​of the actual data. Initially, I did this, all data was converted to a V8 String, but then I wanted a more convenient result.
As a result, it was decided that the fields with types:
are converted to a V8 Number which corresponds to a javascript number.

Fields with types:
checked for binary and if it is binary, BLOBs are converted to Buffer, if not, then V8 String.

And fields with types:
It was decided to convert to V8 Date. And here there was a snag. To create a V8 Date object, pass unix timestamp, and mysql returns a field in the format YYYY-MM-DD HH: MM: SS. I didn’t want to write a string parsing and further conversion. At the same time, I remembered that by itself, JavaScript perfectly converts such an entry into a unix timestamp. And since V8 is available to us, then why not use it. To do this, we create a script object of the V8 Script class, using the Script :: Compile method, to which we pass the script string to the new Date (Date.parse (field_m_mysql)) . After that, we call the Run () method, which will return to us the object obtained when executing the JavaScript code. And we in turn will put it in our V8 Array rows. Maybe not very nice, but quite interesting.
Now all that remains is to compile, for this we need to create a binding.gyp file with the following content:
 { "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/'], }, } ] } 
I ask you to pay attention to the fact that strange ways are indicated here (you may be different with you), you can use the command to get them:
mysql_config --include --libs
Now it is necessary to execute:
node-gyp configure
node-gyp build
cp build/Release/mysql_sync.node ./
Our module is ready to use, for the test we will write the following code:
 var mysql = require('./mysql_sync.node').create(); console.log(mysql.connect("localhost", "login", "pass", "test")); console.log(mysql.query("select * from tmp"); 

In the presence of such a user, database and table, we obtain approximately the same result.
 { 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 > } ] } 
Result for a table created with:
 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 ) 


Total:


  1. We got a module ready for use, maybe not the best, but it does what we need, sometimes it’s the most important
  2. Learned how to write your own C ++ modules for Node.js
  3. Learned from the modules to call arbitrary JavaScript code
  4. We can write any other module if we need it.
  5. Gained experience with V8 JavaScript objects

The article was longer than I expected, but I think that it may come in handy for many people when writing their own module, or when trying to figure out what is happening in the modules of other developers, which is sometimes equally important. Thanks for attention.

Source: https://habr.com/ru/post/154007/


All Articles