2020年更新:这篇是传统的NodeJS Addon开发方式,现在Node.js还提供了N-API的能力,屏蔽了V8这一层,对Addon开发更加友好了,新项目建议直接上N-API。
引言
Node.js本身已经提供了非常多跨平台的能力, 但对于一些特殊的场景仍不能满足需求. 比如:
- 需要调用特定平台上的API
- 集成某些已有的C/C++编写的动态/静态库
- 有高性能需求或需要使用多线程特性的功能等.
对于这些场景, Node.js也提供了基于V8引擎的扩展能力. 但这种能力的扩展过于依赖V8引擎, 现在Node.js已经开始试验性的提供**N-API的方式来进行C++扩展, 以此来屏蔽不同版本的Node.js、不同Js引擎的差异**. 关于原生C++扩展的开发方式的变化, 这里有一篇很不错的文章.
本文主要介绍更常用和稳定的V8引擎C++扩展, 内容很多摘自官方文档. 利用C++扩展在Github已经有一些有意思的项目, 比如播放声音node-speaker, 封装Qt组件库node-qt等等. 本文以获取windows下的盘符和容量为例来说明.
基础概念
- V8: V8是Node.js默认的JavaScript引擎, 通过JIT编译实现高性能的Js解析执行. Node.js源代码中的deps/v8/include/v8.h, 以及Node使用的V8引擎文档都可以查阅.
- libuv: 一个跨平台的异步线程调用库, 实现了 Node.js 的事件循环、工作线程、以及平台所有的的异步操作的C库. 提供了一个类似 POSIX 多线程的线程抽象,可被用于强化更复杂的需要超越标准事件循环的异步插件. 文档传送门: 这里, 这里还有一个中文教程
- node-gyp: Node.js C++模块的编译工具, npm install -g node-gyp 后可使用命令行进行Node C++扩展的管理和编译等操作. node-gyp是nodejs的一个子项目, 项目地址在这里, 其中也有详细的文档链接.
- node-gyp configure: 生成一个Node C++项目, 需要事先在目录中放入一个binding.gyp文件
- node-gyp build/rebuild/clean: 编译/重新编译/清理项目, 在windows中调用Visual Studio, linux中调用gcc等工具实现编译
- nan: 全称是 Native Abstractions for Node.js, 是一个用于C++扩展开发的npm模块. V8引擎在不断更新迭代, Node.js本身也在更新迭代, 按照当前V8提供的API编写的模块也许过一段时间就无法编译运行了. 于是nan出现了, 它可以屏蔽各个Node以及V8版本的差异, 提供统一的API和宏来进行C++扩展开发. 在实际应用中应该尽量使用nan进行C++扩展开发, 不要使用底层的V8/libuv API
如何编写Binding.gyp
binding.gyp详细的用法见文档, 这里是一个最简单的用法:
1 2 3 4 5 6 7 8
| { "targets": [ { "target_name": "hello", "sources": [ "src/hello.cc" ] } ] }
|
如何使用V8引擎
另外, 这是一个使用V8 API创建JS执行上下文并编译运行HelloWorld的例子.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <v8.h>
using namespace v8; int main(int argc, char* argv[]) { HandleScope handle_scope; Persistent<Context> context = Context::New(); Context::Scope context_scope(context); Handle<String> source = String::New("'Hello' + ', World!'"); Handle<Script> script = Script::Compile(source); Handle<Value> result = script->Run(); context.Dispose(); String::AsciiValue ascii(result); printf("%s\n", *ascii); return 0; }
|
代码实现
编写C++扩展, 首先需要在目录中建立一个binding.gyp文件, 再执行node-gyp configure创建对应平台下的项目, C++文件中使用NODE_SET_METHOD, NODE_MODULE等导出CommonJs模块.
从HelloWorld开始
这是一个官网文档中的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <node.h>
namespace demo {
using v8::FunctionCallbackInfo; using v8::Isolate; using v8::Local; using v8::Object; using v8::String; using v8::Value;
void SayHello(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate(); args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World")); }
void init(Local<Object> exports) { NODE_SET_METHOD(exports, "hello", SayHello); }
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
|
数据类型转换
V8编程中大部分变量都是通过Local模板管理的, 这些变量由V8的GC控制, V8的数据类型很多, 基本与JavaScript的数据类型都有对应, 这里是一张V8引擎数据类型的汇总图
在编写C++扩展时, C++标准库的数据类型转换成V8数据类型的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
Isolate* isolate = args.GetIsolate();
Local<Number> retval = v8::Number::New(isolate, 1000);
Local<String> str = v8::String::NewFromUtf8(isolate, "Hello World!");
Local<Object> obj = v8::Object::New(isolate);
obj->Set(v8::String::NewFromUtf8(isolate, "arg1"), str); obj->Set(v8::String::NewFromUtf8(isolate, "arg2"), retval);
Local<FunctionTemplate> tpl = v8::FunctionTemplate::New(isolate, MyFunction); Local<Function> fn = tpl->GetFunction();
fn->SetName(String::NewFromUtf8(isolate, "theFunction")); obj->Set(v8::String::NewFromUtf8(isolate, "arg3"), fn);
Local<Boolean> flag = Boolean::New(isolate, true); obj->Set(String::NewFromUtf8(isolate, "arg4"), flag);
Local<Array> arr = Array::New(isolate);
arr->Set(0, Number::New(isolate, 1)); arr->Set(1, Number::New(isolate, 10)); arr->Set(2, Number::New(isolate, 100)); obj->Set(String::NewFromUtf8(isolate, "arg5"), arr);
Local<Value> und = Undefined(isolate); obj->Set(String::NewFromUtf8(isolate, "arg6"), und);
Local<Value> null = Null(isolate); obj->Set(String::NewFromUtf8(isolate, "arg7"), null);
args.GetReturnValue().Set(obj);
|
实现获取盘符和磁盘容量
实现获取盘符以及容量, 涉及到一些Windows API, 具体代码如下, 参考了**diskusage模块**的部分代码, 项目的代码已经上传到我的Github(node-disk)中. 这里贴一些关键的代码片段, 包括libuv的异步调用以及windows API调用等.
首先需要定义模块的入口函数, 处理输入参数, 并将调用逻辑封装到libuv中. 关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void GetDiskInfo(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate();
async_req* req = new async_req; req->req.data = req;
if (args.Length() != 2 || !args[0]->IsString() || !args[1]->IsFunction()) { node::ErrnoException(isolate, NULL, "Parameter error", NULL); return; }
String::Utf8Value param(args[0]->ToString()); req->input = std::string(*param); req->isolate = isolate;
Local<Function> callback = Local<Function>::Cast(args[1]); req->callback.Reset(isolate, callback);
uv_queue_work(uv_default_loop(), &req->req, DoAsync, (uv_after_work_cb)AfterAsync); args.GetReturnValue().Set(Boolean::New(isolate, true)); }
void init(Local<Object> exports) { NODE_SET_METHOD(exports, "getDiskInfo", GetDiskInfo); }
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
|
上面的代码中DoAsync是关键的实现入口, 其中可能包括一些复杂的计算或IO操作, 但由于在异步线程中进行, 不会影响node.js的事件循环. 此处要注意的是: 切忌在DoAsync中封装V8引擎的数据, 因为DoAsync中的变量会随着调用栈的推出销毁局部变量, 无法利用回调带回给JS.
DoAsync执行完毕后libuv会触发回调, 也就是代码中的AfterAsync函数, 在这里需要将回调给V8引擎的数据封装好, 并销毁掉不再使用的堆中的变量防止内存泄露. AfterAsync函数的关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| result->Set(String::NewFromUtf8(req->isolate, "total"), String::NewFromUtf8(req->isolate, info->totalSize.c_str())); result->Set(String::NewFromUtf8(req->isolate, "free"), String::NewFromUtf8(req->isolate, info->freeSize.c_str())); result->Set(String::NewFromUtf8(req->isolate, "volumes"), volumes);
Local<Value> argv[2] = { Null(isolate), result };
TryCatch try_catch(isolate);
Local<Object> global = isolate->GetCurrentContext()->Global(); Local<Function> callback = Local<Function>::New(isolate, req->callback);
callback->Call(global, 2, argv); req->callback.Reset();
delete req;
if (try_catch.HasCaught()) { node::FatalException(isolate, try_catch); }
|
V8引擎和libuv的调用核心代码大概就是这些, 具体的获取磁盘容量的实现不再赘述, 传送至Github. 调用Windows API中的GetLogicalDrives, GetDiskFreeSpaceEx等函数代码在node_disk_win.cc文件中.
另外, 这些代码已经发布到了npm上, 可以通过npm install node-disk下载使用, 目前只支持windows平台, 后续打算支持linux和macOS, 以及实现获取更详尽的磁盘信息的API. 如果有人对此有兴趣, 非常欢迎加入node-disk模块的迭代和维护.
贴个图纪念一下1.0.0版本:
总结
本文简单讲解了V8, libuv的一些基础, 并以windows下获取磁盘信息为例阐述了Node.js C++扩展的编写方式. 在实际应用中, 使用nan模块提供的抽象接口开发C++扩展具有更好的兼容性, 此处更多的是学习V8及libuv底层的调用, 因此未使用nan的方式开发. 案例比较简单涉及到C++的部分不深, 以后有机会再去学习一些更深入的C++编程吧.
不同的语言之间没有好坏之分, 适合需求的才是最好的. 很多时候, 为了达到特定的质量属性或实现某些复杂的功能, 需要的是多语言的配合. Node.js能有这么多的应用场景, 其中有一些是离不开C/C++, 甚至是python的. 根据需求扬长避短来进行技术选型才是正道.