Node.js is outstanding in using JavaScript to write backends, and it is worth trying more. However, if you need some functions that cannot be used directly or even modules that cannot be implemented at all, can you introduce such results from the C/C library? The answer is yes, all you have to do is write a plug-in and use the resources of other code libraries in your own JavaScript code. Let’s start today’s journey of inquiry together.
Introduction
As stated in the official documentation of Node.js, plug-ins are shared objects that are dynamically linked and can connect JavaScript code with C/C libraries. This means we can reference anything from a C/C++ library and incorporate it into Node.js by creating plugins.
As an example, we will create a wrapper for the standard std::string object.
Preparation
Before we start writing, you first need to make sure that you have prepared all the materials required for subsequent module compilation. You need node-gyp and all its dependencies. You can use the following command to install node-gyp:
npm install -g node-gyp
In terms of dependencies, we need to prepare the following items for Unix systems: • Python (requires version 2.7, 3.x will not work properly)
• make
• A C compiler tool chain (such as gpp or g)
For example, on Ubuntu you can use the following command to install all the above projects (Python 2.7 should have been pre-installed):
sudo apt-get install build-essentials
In a Windows system environment, what you need is:
• Python (version 2.7.3, 3.x does not work properly)
• Microsoft Visual Studio C 2010 (for Windows XP/Vista)
• Microsoft Visual Studio C 2012 for Windows Desktop (for Windows 7/8)
I emphasize that the Express version of Visual Studio will also work normally.
binding.gyp file
This file is used by node-gyp to generate appropriate build files for our plugin. You can click here to view the .gyp file documentation provided by Wikipedia, but the example we are going to use today is very simple, so just use the following code:
{ "targets": [ { "target_name": "stdstring", "sources": [ "addon.cc", "stdstring.cc" ] } ] }
The target_name can be set to anything you like. The sources array contains all the source files that the plug-in needs to use. In our example, we also include addon.cc, which is used to contain the code necessary to compile the plug-in and stdstring.cc, plus our wrapper class.
STDStringWrapper class
The first step is to define our own class in the stdstring.h file. If you are familiar with C programming, then you will definitely not be unfamiliar with the following two lines of code.
#ifndef STDSTRING_H #define STDSTRING_H
This is a standard include guard. Next, we need to include the following two headers into the include category:
#include
#include
The first one is for the std::string class, while the second include is for everything related to Node and V8.
After this step is completed, we can declare our own class:
class STDStringWrapper : public node::ObjectWrap {
For all classes we intend to include in the plugin, we must extend the node::ObjectWrap class.
Now we can start defining the private properties of this class:
private: std::string* s_; explicit STDStringWrapper(std::string s = ""); ~STDStringWrapper();
In addition to the constructor and parsing function, we also need to define a pointer for std::string. This is the core of the technology that can be used to interface a C/C code base with Node - we define a private pointer for the C/C class and will use this pointer to implement operations in all subsequent methods.
Now we declare the constructor static property that will provide functions for the class we created in V8:
static v8::Persistent constructor;
Interested friends can click here to refer to the template description plan for more details.
Now we also need a New method, which will be assigned to the constructor mentioned earlier, and V8 will initialize our class:
static v8::Handle New(const v8::Arguments& args);
Every function that acts on V8 should comply with the following requirements: it will accept a reference to a v8::Arguments object and return a v8::Handle>v8::Value> - this is what we are using strongly typed C coding V8 handles weakly typed JavaScript consistently.
After this, we also need to insert two other methods into the prototype of the object:
static v8::Handle add(const v8::Arguments& args); static v8::Handle toString(const v8::Arguments& args);
The toString() method allows us to obtain the value of s_ instead of the value of [Object object] when using it with ordinary JavaScript strings.
Finally, we will introduce the initialization method (this method will be called by V8 and assigned to the constructor function) and turn off the include guard:
public: static void Init(v8::Handle exports); }; #endif
其中exports对象在JavaScript模块中的作用等同于module.exports。
stdstring.cc文件、构造函数与解析函数
现在来创建stdstring.cc文件。我们首先需要include我们的header:
#include "stdstring.h"
下面为constructor定义属性(因为它属于静态函数):
v8::Persistent STDStringWrapper::constructor;
这个为类服务的构造函数将分配s_属性:
STDStringWrapper::STDStringWrapper(std::string s) { s_ = new std::string(s); }
而解析函数将对其进行delete,从而避免内存溢出:
STDStringWrapper::~STDStringWrapper() { delete s_; }
再有,大家必须delete掉所有与new一同分配的内容,因为每一次此类情况都有可能造成异常,因此请牢牢记住上述操作或者使用共享指针。
Init方法
该方法将由V8加以调用,旨在对我们的类进行初始化(分配constructor,将我们所有打算在JavaScript当中使用的内容安置在exports对象当中):
void STDStringWrapper::Init(v8::Handle exports) {
首先,我们需要为自己的New方法创建一个函数模板:
v8::Local tpl = v8::FunctionTemplate::New(New);
这有点类似于JavaScipt当中的new Function——它允许我们准备好自己的JavaScript类。
现在我们可以根据实际需要为该函数设定名称了(如果大家漏掉了这一步,那么构造函数将处于匿名状态,即名称为function someName() {}或者function () {}):
tpl->SetClassName(v8::String::NewSymbol("STDString"));
我们利用v8::String::NewSymbol()来创建一个用于属性名称的特殊类型字符串——这能为引擎的运作节约一点点时间。
在此之后,我们需要设定我们的类实例当中包含多少个字段:
tpl->InstanceTemplate()->SetInternalFieldCount(2);
我们拥有两个方法——add()与toString(),因此我们将数量设置为2。现在我们可以将自己的方法添加到函数原型当中了:
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("add"), v8::FunctionTemplate::New(add)->GetFunction());
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("toString"), v8::FunctionTemplate::New(toString)->GetFunction());
这部分代码量看起来比较大,但只要认真观察大家就会发现其中的规律:我们利用tpl->PrototypeTemplate()->Set()来添加每一个方法。我们还利用v8::String::NewSymbol()为它们提供名称与FunctionTemplate。
最后,我们可以将该构造函数安置于我们的constructor类属性内的exports对象中:
constructor = v8::Persistent::New(tpl->GetFunction()); exports->Set(v8::String::NewSymbol("STDString"), constructor); }
New方法
现在我们要做的是定义一个与JavaScript Object.prototype.constructor运作效果相同的方法:
v8::Handle STDStringWrapper::New(const v8::Arguments& args) {
我们首先需要为其创建一个范围:
v8::HandleScope scope;
在此之后,我们可以利用args对象的.IsConstructCall()方法来检查该构造函数是否能够利用new关键词加以调用:
if (args.IsConstructCall()) {
如果可以,我们首先如下所示将参数传递至std::string处:
v8::String::Utf8Value str(args[0]->ToString()); std::string s(*str);
……这样我们就能将它传递到我们封装类的构造函数当中了:
STDStringWrapper* obj = new STDStringWrapper(s);
在此之后,我们可以利用之前创建的该对象的.Wrap()方法(继承自node::ObjectWrap)来将它分配给this变量:
obj->Wrap(args.This());
最后,我们可以返回这个新创建的对象:
return args.This();
如果该函数无法利用new进行调用,我们也可以直接调用构造函数。接下来,我们要做的是为参数计数设置一个常数:
} else { const int argc = 1;
现在我们需要利用自己的参数创建一个数组:
v8::Local argv[argc] = { args[0] };
然后将constructor->NewInstance方法的结果传递至scope.Close,这样该对象就能在随后发挥作用(scope.Close基本上允许大家通过将对象处理句柄移动至更高范围的方式对其加以维持——这也是函数的起效方式):
return scope.Close(constructor->NewInstance(argc, argv)); } }
add方法
现在让我们创建add方法,它的作用是允许大家向对象的内部std::string添加内容:
v8::Handle STDStringWrapper::add(const v8::Arguments& args) {
首先,我们需要为我们的函数创建一个范围,并像之前那样把该参数转换到std::string当中:
v8::HandleScope scope; v8::String::Utf8Value str(args[0]->ToString()); std::string s(*str);
现在我们需要对该对象进行拆包。我们之前也进行过这种反向封装操作——这一次我们是要从this变量当中获取指向对象的指针。
STDStringWrapper* obj = ObjectWrap::Unwrap(args.This());
接着我们可以访问s_属性并使用其.append()方法:
obj->s_->append(s);
最后,我们返回s_属性的当前值(需要再次使用scope.Close):
return scope.Close(v8::String::New(obj->s_->c_str()));
由于v8::String::New()方法只能将char pointer作为值来接受,因此我们需要使用obj->s_->c_str()来加以获取。
这时大家的插件文件夹中还应该创建出一个build目录。
测试
现在我们可以对自己的插件进行测试了。在我们的插件目录中创建一个test.js文件以及必要的编译库(大家可以直接略过.node扩展):
var addon = require('./build/Release/addon');
下一步,为我们的对象创建一个新实例:
var test = new addon.STDString('test');
下面再对其进行操作,例如添加或者将其转化为字符串:
test.add('!'); console.log('test\'s contents: %s', test);
在运行之后,大家应该在控制台中看到以下执行结果:
结论
我希望大家能在阅读了本教程之后打消顾虑,将创建与测试以C/C++库为基础的定制化Node.js插件视为一项无甚难度的任务。大家可以利用这种技术轻松将几乎任何C/C++库引入Node.js当中。如果大家愿意,还可以根据实际需求为插件添加更多功能。std::string当中提供大量方法,我们可以将它们作为练习素材。
实用链接
感兴趣的朋友可以查看以下链接以获取更多与Node.js插件开发、V8以及C事件循环库相关的资源与详细信息。
• Node.js插件说明文档
• V8说明文档
• libuv (C事件循环库),来自GitHub
英文:http://code.tutsplus.com/tutorials/writing-nodejs-addons--cms-21771