网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
我们可以将它放在项目根目录。
配置文件的内容是一个大JSON,可以非常简单:
{
"targets": [
{
"target\_name": "meshtool",
"sources": [ “./meshfilereader.cpp”,"./index.cpp"]
}]
}
以上配置指定了一个叫meshtool的编译目标, 该目标需要编译的cpp文件为 index.cpp和meshfilereader.cpp.
接着在项目放binding.gyp的目录运行 node-gyp configure, 该目录下会生成一个build的目录,内含构建所需的规则和makefile.
于是我们再运行node-gyp build, 在运行目录/build/Release下我们便可以找到所需的.node文件了。
2. nan (Native abstraction for NodeJS)
当你开始写Addon不久以后,你便会发现一个令人抓狂的现实:NodeJS 的 0.12.x,0.11.x和0.10.x因为使用的V8版本不同,存在严重的API不兼容的情况。不管你愿不愿意,你发现只能做到令其中一个版本通过编译。你开始苦恼C++不能像JS代码那样,不用改一行代码就同时满足所有NodeJS版本, 直到你发现了nan.
nan 是老外的一个项目(不是"Not A Number"),解决了不同NodeJS版本间API的不兼容问题,同样可以用npm安装:
npm install nan
//or
npm install -g nan
也可以直接配置到package.json中。
安装完后要在binding.gyp中配置好nan的头文件依赖,修改如下:
{
"targets": [
{
"target\_name": "meshtool",
"sources": [ “./meshfilereader.cpp”,"./index.cpp"],
"include\_dirs" : [
"<!(node -e \"require('nan')\")"
}]
}
之所以include_dirs要这么写是因为这样可以自适应nan模块安装在全局和安装在本地的情况。如果确定安装方式的话,也可以直接写路径。
至此万事俱备,只欠代码。
编写Addon的C++代码
写代码前的预备知识
写Addon之前,建议先要了解一下Google V8的API,至少要了解以下的一些概念:
JS 基本类型 对应的V8原生C++ 原生类型 :
Javascript V8
Number v8::Number, v8::Integer
String v8::String
Array v8::Array
Object v8::Object
Function v8::Function
JS基本类型和V8的原生类型之间实际是等价的,也就是C++层从JS层获取到的JS基本对象和返回JS层的结果,都是以v8的上述的原生类型形式。
JS句柄 v8::Handle 它相当于一个智能指针,所有上面的C++的原生类型都是由Handle来引用的,相当于JS中的那个var 变量,因此不管是从JS层获取到的原生类型对象还是在C++内部构造出的原生类型对象,都是以 v8::Handle 形式给出来的。
v8::Handle分为两种,v8::Local和v8::Persistant, 前者只在当前Scope中有效,后者是代表全局变量。
v8::Local 的Scope由 HandleScope管理,由离最近的HandleScope分配,并随HandleScope生命周期结束而结束。而v8::Persistant的生命周期由自己的New和Dispose方法管理。
生命周期结束的Handle,其指向的对象会随时被垃圾收集回收。
JS方法的C++表示
必须为为全局函数或静态方法,根据V8版本不同固定为如下的形式:
V8 3.11
v8::Handle<v8::Value> AnyMethodName(v8::Argument args)
V8 3.28
void AnyMethodName(v8::Argument args)
JS方法的传入参数 v8::Argument
不管什么样的JS函数,其C++方法的传入参数都是一个v8::Argument对象,这是因为v8::Arugment本身就是一个list, 内含可变数量的实际参数,如果想取第i个传入参数,只需要使用args[i] 即可。另外还可以通过args.This()获取this对象。
了解完概念,我们试着写一个输出一个方法的Addon
和写普通的JS模块一样,Addon的代码需要确定模块的输出,这里就是借助Nan写输出一个叫parseMesh的JS方法的Addon:
NAN\_METHOD(ParseMesh)
{
NanScope();
if(args.Length() < 2 || !args[0]->IsString() || !args[1]->IsFunction())
{
return NanThrowError("Bad Arguments");
}
Handle<String> filename = args[0].As<String>();
Handle<Function> callback = args[1].As<Function>();
...
NanReturnUndefined();
}
void init(Handle<Object> exports)
{
NODE\_SET\_METHOD(exports,"parseMesh",ParseMesh);
}
NODE\_MODULE(meshtool, init);
以上代码最后一行定义模块名称meshtool,和加载它的时候调用的初始化方法init.
而初始化方法中则设置了输出的函数名parseMesh, 而实际接受parseMesh调用的C++方法即ParseMesh。
再来看这个ParseMesh方法,由于前面所说,因为v8::Argument的存在,所有JS的函数在C++层的方法参数和返回值都是一致的,所以它可以被一个NAN_METHOD的宏来处理,该宏根据Node版本将方法展开成对应的形式。保证ParseMesh方法可以在初始化中注册为任意版本JS函数parseMesh的的实现。
最后的NanReturnUndefined()表示该方法返回undefined (没有返回值即返回undefined). Nan还有很多其他的Return形式可以使用。
到此我们已经可以用C++ Addon来输出简单的JS函数了,这对于大多数情况已经够用。然而NodeJS还提供了一些更高大上的东西,比如输出一个自定义的JS的类型,或者在C++中使用多线程,并异步执行回调等。
进阶
进阶1: 输出一个JS包装类型
前一篇只提到了如何输出一个JS方法,但有的时候如果我们想输出的是一个C++的对象呢,这种情况在想要包装一个现有的C++库到JS的时候出现的尤其频繁。
如果我们仅有输出C++方法成为JS函数一条路,那也有笨办法,用C++代码表示:
//C++
class Body
{
Body();
void Move();
};
NAN\_METHOD(CreateBody)
{
NanScope();
Body\* handle = new Body();
NanReturnValue(NanNew<Integer>(reinterpret\_cast<int>(handle)));
}
NAN\_METHOD(BodyMove)
{
//check arguments
Body\* handle = reinterpret\_cast<Body\*>(args[0].As<Integer>()->intValue());
handle->Move();
}
NAN\_METHOD(DestroyBody)
{
//check arguments
Body\* handle = reinterpret\_cast<Body\*>(args[0].As<Integer>()->intValue());
delete handle;
}
void init(Handle<Object> exports) {
NODE\_SET\_METHOD(exports,"createBody",CreateBody);
NODE\_SET\_METHOD(exports,"bodyMove",BodyMove);
NODE\_SET\_METHOD(exports,"destroyBody", DestroyBody);
}
NODE\_MODULE(native_body, init)
相应的使用native addon的JS代码:
var native=require("native\_body");
var handle = native.createBody();
native.bodyMove(handle);
native.bodyDestroy(handle);
其实就是将一个Body对象的指针作为JS的一个int变量让JS层持有,每当要操作该对象时,重新将该指针传回。
但是这样的实现有很多缺点:
- 所有的JS方法需要传入一个额外的由CreateBody得到的handle。
- 用完必须显式调用BodyDestroy, 否则会内存泄露。
- 不安全,如果黑客通过外部传入特定地址的handle, 内部也会将它当做Body指针而执行对应方法,轻则程序崩溃,重则程序行为被控制。(不过这个问题可以通过向外部提供‘间接’地址解决,不展开了)
NodeJS对这种需求提供了比较完美的解决方案- ObjectWrap , 通过自定义C++ class继承ObjectWrap,NodeJS可以输出和自定义JS类型等价的对象。上面的代码可以改成这样:
class BodyWrap : public ObjectWrap
{
Body\* internalBody_;
public:
BodyWrap():
internalBody\_(new Body())
{
}
~BodyWrap()
{
delete internalBody_;
}
static NAN\_METHOD(New){
NanScope();
// arg check is omitted for brevity
BodyWrap \*jsBody = new BodyWrap();
jsBody->Wrap(args.This());
NanReturnValue(args.This());
}
static NAN\_METHOD(Move)
{
//check arguments
BodyWrap\* thisObj = ObjectWrap::Unwrap<BodyWrap>(args.This());
thisObj->internalBody_->Move();
}
};
void init(Handle<Object> exports) {
NanScope();
Local<FunctionTemplate> t = NanNew<FunctionTemplate>(BodyWrap::New);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(NanNew<String>("Body"));
NODE\_SET\_PROTOTYPE\_METHOD(t, "move", BodyWrap::Move);
exports->Set(NanNew<String>("Body"), t->GetFunction());
}
NODE\_MODULE(native_body, init)
相应的JS代码:
var Body = require("native\_body").Body;
var b = new Body();
b.move();
从JS代码可以看到已经不存在什么handle了,需要Body实例的时候可以直接new 出来,在该实例上调用方法,对应的C++ Body类型的方法就会执行,这和普通的JS自定义class完全没什么区别。
另外还可以注意到一点,JS代码中没有执行任何类似于DestroyBody的方法。那C++的Body实例何时释放呢?-- 在上面这个代码中,当new出来的JS实例 b被垃圾回收时,C++ Body实例会被自然的析构。
进阶2: 使用多线程异步计算
通常使用C++ Addon的场景,都是计算密集的任务,另外从前面的一些实例代码可以看出,C++到JS之间的数据传递中,是有很多装箱/拆箱的消耗的(如从v8::Number 到double),因此我们为了避免这种损耗,通常希望在C++中做尽量多的事情,而不希望将任务过度切分,因此如果全部在主线程执行,无可避免的会对主线程造成阻塞。解决方案则是将主要的计算任务,放在另一个线程中执行,再将结果数据在主线程中通过回调交回给JS。
假设前面的Body对象多了一个计算量很高的 checkCollision方法检查是否与其他物体碰撞,并返回布尔值。如何将checkCollision放到其他线程,再将结果返回主线程呢?我们需要用到另一个NodeJS的基础库:libuv.
下面这个示例演示了如何创建一个线程来运行checkCollision,并在线程中使用uv_aync_send方法将结果带回到主线程回调给JS层。
class Body
{
...
bool checkCollision();
};
struct BodyContext
{
Body\* body;
uv_async_t async;
bool result;
NanCallback \*callback;
uv_thread_t tid;
};
![img](https://img-blog.csdnimg.cn/img_convert/9c069930b9f381618574895c7f5e5795.png)
![img](https://img-blog.csdnimg.cn/img_convert/fbc8c72bbf24a35fadcb90c02a33da20.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**
\*callback;
uv_thread_t tid;
};
[外链图片转存中...(img-5tLbGkJK-1715594881600)]
[外链图片转存中...(img-mVL4RbfD-1715594881600)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**