本文详细讲解如何在Node.js中通过C/C++编写动态链接插件。内容涵盖node-gyp环境配置、binding.gyp文件定义、V8对象封装及内存管理,并通过std::string封装实例演示完整开发流程。
Node.js插件开发概述
Node.js在JavaScript后端开发中表现卓越,值得深入探索。若需使用C/C++库中无法直接通过JavaScript实现的功能,可通过编写插件引入这些资源。本文将指导如何创建插件,以便在JS代码中调用其他代码库。
介绍
根据Node.js官方文档,插件是动态链接的共享对象,用于连接JavaScript与C/C++库。这意味着可以引用任何C/C++库内容,并通过插件机制集成到Node.js中。
本文将以封装标准std::string对象为例进行说明。
环境准备与依赖配置
准备工作
在开始编码前,需确保具备编译后续模块所需的所有素材。必须安装node-gyp及其所有依赖项。可通过以下命令安装node-gyp:
npm install -g node-gyp
对于Unix系统,依赖项包括:• Python(需2.7版本,3.x无效)
• make
• C++编译器工具链(如gpp或g++)
例如,在Ubuntu上可通过以下命令安装上述项目(Python 2.7通常已预装):
sudo apt-get install build-essentials
在Windows环境下,需要:
• Python(2.7.3版本,3.x无效)
• 微软Visual Studio C++ 2010(适用于Windows XP/Vista)
• 微软Visual Studio C++ 2012 for Windows Desktop(适用于Windows 7/8)
请注意,Visual Studio的Express版本同样有效。
构建配置文件binding.gyp
binding.gyp文件
该文件由node-gyp使用,用于生成适当的构建文件。可参考维基百科的.gyp文件说明文档,但本例非常简单,只需使用以下代码:
{
"targets": [
{
"target_name": "stdstring",
"sources": [ "addon.cc", "stdstring.cc" ]
}
]
}
其中target_name可设为任意名称。sources数组包含插件所需的所有源文件。本例还包括addon.cc,用于容纳编译插件及stdstring.cc所需的代码,以及封装类。
C++类封装与V8接口定义
STDStringWrapper类
首先,在stdstring.h文件中定义类。若熟悉C++编程,以下两行代码应不陌生。
#ifndef STDSTRING_H
#define STDSTRING_H
这是标准的include guard。接下来,需将以下两个头文件纳入include范围:
#include #include 第一个针对std::string类,第二个包含所有与Node及V8相关的内容。
完成上述步骤后,可声明类:
class STDStringWrapper : public node::ObjectWrap { 所有打算包含在插件中的类必须扩展node::ObjectWrap类。
现在可定义该类的私有属性:
private:
std::string* s_;
explicit STDStringWrapper(std::string s = "");
~STDStringWrapper();
除构造函数与析构函数外,还需为std::string定义一个指针。这是技术的核心,用于连接C/C++代码库与Node——为C/C++类定义私有指针,并在后续方法中利用该指针进行操作。
现在声明constructor静态属性,它将为V8中创建的类提供函数:
static v8::Persistent constructor; 感兴趣者可点击此处参阅模板说明方案以获取更多细节。
还需一个New方法,它将被分配给前述constructor,同时V8将对类进行初始化:
static v8::Handle New(const v8::Arguments& args); 作用于V8的每个函数都应遵循以下要求:接受指向v8::Arguments对象的引用,并返回v8::Handle>v8::Value>——这正是V8处理弱类型JavaScript时使用强类型C++编码的方式。
此后,还需将另外两个方法插入对象原型中:
static v8::Handle add(const v8::Arguments& args);
static v8::Handle toString(const v8::Arguments& args);
其中toString()方法允许在与普通JavaScript字符串共同使用时返回s_的值,而非[Object object]。
最后,引入初始化方法(此方法将由V8调用并指派给constructor函数)并关闭include guard:
public:
static void Init(v8::Handle exports);
};
#endif
其中exports对象在JavaScript模块中的作用等同于module.exports。
实现构造函数与内存管理
stdstring.cc文件、构造函数与析构函数
现在创建stdstring.cc文件。首先需include头文件:
#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初始化与原型方法注册
Init方法
该方法将由V8调用,旨在初始化类(分配constructor,将所有打算在JavaScript中使用的内容安置在exports对象中):
void STDStringWrapper::Init(v8::Handle exports) { 首先,需为New方法创建函数模板:
v8::Local tpl = v8::FunctionTemplate::New(New); 这类似于JavaScript中的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方法与实例化逻辑
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方法
现在创建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