Node底层机制使用C++写的,所以我们如果想扩展功能,可以选择使用C++从底层扩展,以前已经介绍过何如嵌入V8到自己的程序中,实际上Node就是把V8和libuv等库整合到一起,从而使我们用JavaScript就可以调用很多C++的库来实现自己的功能。
可以查看这两编文章了解一下V8嵌入的一些概念:
嵌入V8的核心概念
嵌入V8的核心概念1
在具体介绍写addon之前,先要讨论一下为啥需要addon,有没有其他方法。
为什么选择addon
实际上要让JavaScript调用c++代码有三种方法:
1.在子进程中调用C++程序
可以阅读automating-a-c-program-from-a-node-js-web-app
看看下面例子,execFile
函数可以帮助我们执行一个程序。
// standard node module
var execFile = require('child_process').execFile
// this launches the executable and returns immediately
var child = execFile("path to executable", ["arg1", "arg2"],
function (error, stdout, stderr) {
// This callback is invoked once the child terminates
// You'd want to check err/stderr as well!
console.log("Here is the complete output of the program: ");
console.log(stdout)
});
// if the program needs input on stdin, you can write to it immediately
child.stdin.setEncoding('utf-8');
child.stdin.write("Hello my child!\n");
var execFile = require('child_process').execFile
// this launches the executable and returns immediately
var child = execFile("path to executable", ["arg1", "arg2"],
function (error, stdout, stderr) {
// This callback is invoked once the child terminates
// You'd want to check err/stderr as well!
console.log("Here is the complete output of the program: ");
console.log(stdout)
});
// if the program needs input on stdin, you can write to it immediately
child.stdin.setEncoding('utf-8');
child.stdin.write("Hello my child!\n");
2.调用C++的dll
调用的dll需要导出函数。
var ffi = require('ffi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2
// You can also access just functions in the current process by passing a null
var current = ffi.Library(null, {
'atoi': [ 'int', [ 'string' ] ]
});
current.atoi('1234'); // 1234
3.使用addon(实际上addon也是一个动态链接库)
使用addon在C++这边需要了解V8和libuv的api,可以说是最复杂的,但是可以让JavaScript调用起来比较简单,而且可以实现异步回调,如果你用上面两种方法,是不好实现像node.js这样回调的。我们看看回调的写法。
var server = http.createServer(function(req, res) {
++requests;
var stream = fs.createWriteStream(file);
req.pipe(stream);
stream.on('close', function() {
res.writeHead(200);
res.end();
});
}).listen(common.PORT, function() {
.......
如何写Addon
在写addon的时候我们可以使用第三方包装NAN,但这里我们主要介绍如何直接使用node和v8的api来做addon。
在node的文档中,详细介绍了各种处理方法。不过我喜欢通过阅读完整的代码来学习,所以找了一些资料,在这里列出。
对qt的包装
代码比较多,我对qt没有很多了解,并没有看,只是公司在用,列在这里。对zmq的包装。使用了NAN。zmq是一个快速的消息队列,里面总结的各种模式对开发分布式程序有指导意义。
ScottFree的demo,一个老外写的比较好的blog,有很多例子。
官方文档demo,比较简单,没有使用到libuv。
大家编译addon的时候注意版本和平台的关系,node版本可以用nvm管理。
Scott Frees写了很多博客介绍node。这里通过阅读他的代码来了解如何写addon。
例子说明
ScotteFree的例子代码结构:
文件 | 说明 |
---|---|
rainfall.js | 使用addon的js代码 |
binding.gyp | 编译脚本 |
makefile | 编译脚本 |
rainfall.cc | c++的逻辑代码 |
rainfall_node.cc | 插件,绑定c++逻辑代码 |
这里面主要的逻辑就是显示某一经度或者纬度的不同日期的降雨量,并进行相应计算。因为计算需要耗费cpu资源,阻塞主线程,所以希望放到另一个线程中。
我们看看在js中怎么使用插件的,先知道目标是啥,在看代码的时候可以带着问题思考。
1. 创建对象rainfall
我们可以使用require去加载插件
var rainfall = require("./cpp/build/Release/rainfall");
var location = {
latitude : 40.71, longitude : -74.01,
samples : [
{ date : "2015-06-07", rainfall : 2.1 },
{ date : "2015-06-14", rainfall : 0.5},
{ date : "2015-06-21", rainfall : 1.5},
{ date : "2015-06-28", rainfall : 1.3},
{ date : "2015-07-05", rainfall : 0.9}
] };
2. 计算平均降雨量
我们传递一个JavaScript对象给c++使用
console.log("Average rain fall = " + rainfall.avg_rainfall(location) + "cm");
3. 计算降雨数据(不关心,没仔细看算法)
从C++返回JavaScript对象
console.log("Rainfall Data = " + JSON.stringify(rainfall.data_rainfall(location)));
4. 同步计算
传递数组给C++,返回数组
var results = rainfall.calculate_results(locations);
print_rain_results(results);
5. 异步计算
rainfall.calculate_results_async(locations, print_rain_results);
上面只有最后一个函数calculate_results_async
是异步计算,所以我们着重看看这个函数怎么实现的。下面过过代码。
代码分析
头文件
#include <node.h>
#include <v8.h>
#include <uv.h>
#include "rainfall.h"
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <thread>
using namespace v8;
通过头文件可以看到addon需要和v8,libuv,node打交道。
导出函数
下面的代码很容易看出是把函数加入到exports中。exports就是js中的对象。
void init(Handle <Object> exports, Handle<Object> module) {
NODE_SET_METHOD(exports, "avg_rainfall", AvgRainfall);
NODE_SET_METHOD(exports, "data_rainfall", RainfallData);
NODE_SET_METHOD(exports, "calculate_results", CalculateResults);
NODE_SET_METHOD(exports, "calculate_results_sync", CalculateResultsSync);
NODE_SET_METHOD(exports, "calculate_results_async", CalculateResultsAsync);
}
NODE_MODULE(rainfall, init)
拿到值和返回值
- 拿参数中的JavaScript对象,返回double
void AvgRainfall(const v8::FunctionCallbackInfo<v8::Value>& args) {
Isolate* isolate = args.GetIsolate();
location loc = unpack_location(isolate, Handle<Object>::Cast(args[0]));
double avg = avg_rainfall(loc);
Local<Number> retval = v8::Number::New(isolate, avg);
args.GetReturnValue().Set(retval);
}
- 从上面代码我们看出,首先要获得isolate,下面的api都需要这个作为参数,从这里可以看出,这些api都很底层还是比较繁琐的。
- 拿参数
Handle<Object>::Cast(args[0])
- 返回值给JavaScript:
args.GetReturnValue().Set(retval);
- 拿参数中JavaScript对象,返回对象
void RainfallData(const v8::FunctionCallbackInfo<v8::Value>& args) {
Isolate* isolate = args.GetIsolate();
location loc = unpack_location(isolate, Handle<Object>::Cast(args[0]));
rain_result result = calc_rain_stats(loc);
Local<Object> obj = Object::New(isolate);
pack_rain_result(isolate, obj, result);
args.GetReturnValue().Set(obj);
}
我们看到拿对象是一样的,这里Local<Object> obj = Object::New(isolate);
是关键代码。创建了一个V8的对象。然后返回。
- 传递返回数组
void CalculateResults(const v8::FunctionCallbackInfo<v8::Value>&args) {
Isolate* isolate = args.GetIsolate();
std::vector<location> locations;
std::vector<rain_result> results;
// extract each location (its a list)
Local<Array> input = Local<Array>::Cast(args[0]);
unsigned int num_locations = input->Length();
for (unsigned int i = 0; i < num_locations; i++) {
locations.push_back(unpack_location(isolate, Local<Object>::Cast(input->Get(i))));
}
// Build vector of rain_results
results.resize(locations.size());
std::transform(locations.begin(), locations.end(), results.begin(), calc_rain_stats);
// Convert the rain_results into Objects for return
Local<Array> result_list = Array::New(isolate);
for (unsigned int i = 0; i < results.size(); i++ ) {
Local<Object> result = Object::New(isolate);
pack_rain_result(isolate, result, results[i]);
result_list->Set(i, result);
}
// Return the list
args.GetReturnValue().Set(result_list);
}
从代码中我们看出来下面两行代码分别表示拿数据和返回数组
Local<Array> input = Local<Array>::Cast(args[0]);
Local<Array> result_list = Array::New(isolate);
- 异步
node强的地方就是大部分api都是异步的,那么我们来看看他是怎么做到的,我们知道底层c的api都是同步,所以node必须的包装并使用线程来支持异步。我们看看代码。
void CalculateResultsAsync(const v8::FunctionCallbackInfo<v8::Value>&args) {
Isolate* isolate = args.GetIsolate();
Work * work = new Work();
work->request.data = work;
// extract each location (its a list) and store it in the work package
// locations is on the heap, accessible in the libuv threads
Local<Array> input = Local<Array>::Cast(args[0]);
unsigned int num_locations = input->Length();
for (unsigned int i = 0; i < num_locations; i++) {
work->locations.push_back(unpack_location(isolate, Local<Object>::Cast(input->Get(i))));
}
// store the callback from JS in the work package so we can
// invoke it later
Local<Function> callback = Local<Function>::Cast(args[1]);
work->callback.Reset(isolate, callback);
// kick of the worker thread
uv_queue_work(uv_default_loop(),&work->request,WorkAsync,WorkAsyncComplete);
args.GetReturnValue().Set(Undefined(isolate));
}
我们看一下关键代码
Work * work = new Work();//堆上创建数据,可以在线程间共享
uv_queue_work(uv_default_loop(),&work->request,WorkAsync,WorkAsyncComplete);
这里把要做的工作放在队列里面了,所以不会阻塞当前线程。WorkAsync
是在工作线程中运行的,WorkAsyncComplete
是回调,由livuv触发,回到工作线程。再来看看WorkAsync
:
struct Work {
uv_work_t request;
Persistent<Function> callback;
std::vector<location> locations;
std::vector<rain_result> results;
};
// called by libuv worker in separate thread
static void WorkAsync(uv_work_t *req)
{
Work *work = static_cast<Work *>(req->data);
// this is the worker thread, lets build up the results
// allocated results from the heap because we'll need
// to access in the event loop later to send back
work->results.resize(work->locations.size());
std::transform(work->locations.begin(), work->locations.end(), work->results.begin(), calc_rain_stats);
// that wasn't really that long of an operation, so lets pretend it took longer...
std::this_thread::sleep_for(chrono::seconds(3));
}
注意从uv_work_t
拿到我们要操作的数据,线程之间可以共享堆上的数据,所以这里访问没有问题。
再看看回调如何执行。
// called by libuv in event loop when async function completes
static void WorkAsyncComplete(uv_work_t *req,int status)
{
Isolate * isolate = Isolate::GetCurrent();
// Fix for Node 4.x - thanks to https://github.com/nwjs/blink/commit/ecda32d117aca108c44f38c8eb2cb2d0810dfdeb
v8::HandleScope handleScope(isolate);
Local<Array> result_list = Array::New(isolate);
Work *work = static_cast<Work *>(req->data);
// the work has been done, and now we pack the results
// vector into a Local array on the event-thread's stack.
for (unsigned int i = 0; i < work->results.size(); i++ ) {
Local<Object> result = Object::New(isolate);
pack_rain_result(isolate, result, work->results[i]);
result_list->Set(i, result);
}
// set up return arguments
Handle<Value> argv[] = { result_list };
// execute the callback
// https://stackoverflow.com/questions/13826803/calling-javascript-function-from-a-c-callback-in-v8/28554065#28554065
Local<Function>::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
// Free up the persistent function callback
work->callback.Reset();
delete work;
}
注意看一下关键代码,我们新建了一个function并调用,这个函数就是callback。
Local<Function>::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
最后我们看一下线程的情况
- js执行线程: 事件循环+回调,js代码的执行,都在这里
-
libuv启动的线程:用来做i/o和计算,比如读取一个文件,这样我们就不会被慢速的i/o拖累了。
从上图可以看到CalculateResultsAsync结束的时候,V8 Locals全部都会销毁,所以我们的回调需要是Persistent。
Persistent<Function> callback;
可以在workthread里面访问v8的内存吗?
答案是不能,v8不能多线程访问,如果需要多线程访问,需要加锁,而node在启动的时候在主线程就会获得锁,可以在node.cc中的start函数看到
Locker locker(node_isolate);
所以工作线程是没机会获得锁的。所以上面使用的copy数据的方法。具体的说明可以看这个文章
包装对象
由于上面并没有说明如何包装C++对象并返回给js,这里又切回官方文档demo,说明如何包装C++对象,然后再JavaScript中用new去新建对象。
本文引用的代码是在红框范围内:
- addon.cc
#include <node.h>
#include "myobject.h"
using namespace v8;
void InitAll(Handle<Object> exports) {
MyObject::Init(exports);
}
NODE_MODULE(addon, InitAll)
可以看到宏还是那些宏,只是现在调用了类MyObject的静态方法Init来导出函数。
- myobject.cc
这个文件要看的比较多,我们先看init函数
void MyObject::Init(Handle<Object> exports) {
Isolate* isolate = Isolate::GetCurrent();
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
constructor.Reset(isolate, tpl->GetFunction());
exports->Set(String::NewFromUtf8(isolate, "MyObject"),
tpl->GetFunction());
}
- 这里使用到了
FunctionTemplate
, -
tpl->InstanceTemplate()->SetInternalFieldCount(1);
设置有每个JavaScript对象有几个暴露的函数或者属性,这边只有一个NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
,注意设置在原型上 - 设置构造函数
constructor
再看看New函数,这个函数会在JavaScript使用new关键字创建对象的时候被调用。
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
args.GetReturnValue().Set(cons->NewInstance(argc, argv));
}
}
我们看到几个关键地方
-
IsConstructCall
来判断是否是用new来调用的,或者是用函数方式直接调用,这里我们只看new,因为这是我们通常使用JavaScript对象的方式。 - 创建对象
obj->Wrap(args.This());
-
obj->Wrap(args.This());
用来设置this指针,我们知道使用new创建对象的时候,this就是当前创建的对象。 - 最后返回this
最后我们看看如何使用。
-- addon.js
var addon = require('bindings')('addon');
var obj = new addon.MyObject(10);
console.log( obj.plusOne() ); // 11
console.log( obj.plusOne() ); // 12
console.log( obj.plusOne() ); // 13
我们看到这次换了一种方式去加载c++模块,在node内部,调用native的都是这样的,我们写addon的时候,可以不这样加载。
再看看plus函数
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
可以看到使用了ObjectWrap::Unwrap
函数,和上面的wrap函数对应,另外C++中plusone第一个参数this,所以可以访问内部私有变量。
好了,其他的几个demo都大同小异,这里就不写下来了,希望这篇文章能帮助大家理解node addon的原理。
总结
- 本文介绍了ScotteFree的例子,掌握了JavaScript和C++传递数据的方法。
- 理清了js线程和工作线程的区别。
- 在现实环境中,v8接口和libuv的接口都会改变,这给我们编写addon带来了麻烦,NAN库可以帮我们解决,所以如果真的要写addon,应该看看NAN。
本文参考了以下文章:
https://nodejs.org/api/addons.html#addons_wrapping_c_objects
https://developers.google.com/v8/embed?hl=en#accessing-dynamic-variables
http://code.tutsplus.com/tutorials/writing-nodejs-addons--cms-21771
http://blog.scottfrees.com/c-processing-from-node-js
https://blog.scottfrees.com/how-not-to-access-node-js-from-c-worker-threads
http://blog.scottfrees.com/c-processing-from-node-js-part-4-asynchronous-addons
http://blog.scottfrees.com/c-processing-from-node-js-part-2
http://blog.scottfrees.com/c-processing-from-node-js-part-3-arrays