Node底層機(jī)制使用C++寫的,所以我們?nèi)绻霐U(kuò)展功能,可以選擇使用C++從底層擴(kuò)展,以前已經(jīng)介紹過何如嵌入V8到自己的程序中,實(shí)際上Node就是把V8和libuv等庫(kù)整合到一起,從而使我們用JavaScript就可以調(diào)用很多C++的庫(kù)來(lái)實(shí)現(xiàn)自己的功能。
可以查看這兩編文章了解一下V8嵌入的一些概念:
嵌入V8的核心概念
嵌入V8的核心概念1
在具體介紹寫addon之前,先要討論一下為啥需要addon,有沒有其他方法。
為什么選擇addon
實(shí)際上要讓JavaScript調(diào)用c++代碼有三種方法:
1.在子進(jìn)程中調(diào)用C++程序
可以閱讀automating-a-c-program-from-a-node-js-web-app
看看下面例子,execFile函數(shù)可以幫助我們執(zhí)行一個(gè)程序。
// 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.調(diào)用C++的dll
調(diào)用的dll需要導(dǎo)出函數(shù)。
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(實(shí)際上addon也是一個(gè)動(dòng)態(tài)鏈接庫(kù))
使用addon在C++這邊需要了解V8和libuv的api,可以說(shuō)是最復(fù)雜的,但是可以讓JavaScript調(diào)用起來(lái)比較簡(jiǎn)單,而且可以實(shí)現(xiàn)異步回調(diào),如果你用上面兩種方法,是不好實(shí)現(xiàn)像node.js這樣回調(diào)的。我們看看回調(diào)的寫法。
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的時(shí)候我們可以使用第三方包裝NAN,但這里我們主要介紹如何直接使用node和v8的api來(lái)做addon。
在node的文檔中,詳細(xì)介紹了各種處理方法。不過我喜歡通過閱讀完整的代碼來(lái)學(xué)習(xí),所以找了一些資料,在這里列出。
對(duì)qt的包裝
代碼比較多,我對(duì)qt沒有很多了解,并沒有看,只是公司在用,列在這里。對(duì)zmq的包裝。使用了NAN。zmq是一個(gè)快速的消息隊(duì)列,里面總結(jié)的各種模式對(duì)開發(fā)分布式程序有指導(dǎo)意義。
ScottFree的demo,一個(gè)老外寫的比較好的blog,有很多例子。
官方文檔demo,比較簡(jiǎn)單,沒有使用到libuv。
大家編譯addon的時(shí)候注意版本和平臺(tái)的關(guān)系,node版本可以用nvm管理。
Scott Frees寫了很多博客介紹node。這里通過閱讀他的代碼來(lái)了解如何寫addon。
例子說(shuō)明
ScotteFree的例子代碼結(jié)構(gòu):
| 文件 | 說(shuō)明 |
|---|---|
| rainfall.js | 使用addon的js代碼 |
| binding.gyp | 編譯腳本 |
| makefile | 編譯腳本 |
| rainfall.cc | c++的邏輯代碼 |
| rainfall_node.cc | 插件,綁定c++邏輯代碼 |
這里面主要的邏輯就是顯示某一經(jīng)度或者緯度的不同日期的降雨量,并進(jìn)行相應(yīng)計(jì)算。因?yàn)橛?jì)算需要耗費(fèi)cpu資源,阻塞主線程,所以希望放到另一個(gè)線程中。

我們看看在js中怎么使用插件的,先知道目標(biāo)是啥,在看代碼的時(shí)候可以帶著問題思考。
1. 創(chuàng)建對(duì)象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. 計(jì)算平均降雨量
我們傳遞一個(gè)JavaScript對(duì)象給c++使用
console.log("Average rain fall = " + rainfall.avg_rainfall(location) + "cm");
3. 計(jì)算降雨數(shù)據(jù)(不關(guān)心,沒仔細(xì)看算法)
從C++返回JavaScript對(duì)象
console.log("Rainfall Data = " + JSON.stringify(rainfall.data_rainfall(location)));
4. 同步計(jì)算
傳遞數(shù)組給C++,返回?cái)?shù)組
var results = rainfall.calculate_results(locations);
print_rain_results(results);
5. 異步計(jì)算
rainfall.calculate_results_async(locations, print_rain_results);
上面只有最后一個(gè)函數(shù)calculate_results_async是異步計(jì)算,所以我們著重看看這個(gè)函數(shù)怎么實(shí)現(xiàn)的。下面過過代碼。
代碼分析
頭文件
#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打交道。
導(dǎo)出函數(shù)
下面的代碼很容易看出是把函數(shù)加入到exports中。exports就是js中的對(duì)象。
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)
拿到值和返回值
- 拿參數(shù)中的JavaScript對(duì)象,返回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都需要這個(gè)作為參數(shù),從這里可以看出,這些api都很底層還是比較繁瑣的。
- 拿參數(shù)
Handle<Object>::Cast(args[0]) - 返回值給JavaScript:
args.GetReturnValue().Set(retval);
- 拿參數(shù)中JavaScript對(duì)象,返回對(duì)象
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);
}
我們看到拿對(duì)象是一樣的,這里Local<Object> obj = Object::New(isolate);是關(guān)鍵代碼。創(chuàng)建了一個(gè)V8的對(duì)象。然后返回。
- 傳遞返回?cái)?shù)組
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);
}
從代碼中我們看出來(lái)下面兩行代碼分別表示拿數(shù)據(jù)和返回?cái)?shù)組
Local<Array> input = Local<Array>::Cast(args[0]);
Local<Array> result_list = Array::New(isolate);
- 異步
node強(qiáng)的地方就是大部分api都是異步的,那么我們來(lái)看看他是怎么做到的,我們知道底層c的api都是同步,所以node必須的包裝并使用線程來(lái)支持異步。我們看看代碼。
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));
}
我們看一下關(guān)鍵代碼
Work * work = new Work();//堆上創(chuàng)建數(shù)據(jù),可以在線程間共享
uv_queue_work(uv_default_loop(),&work->request,WorkAsync,WorkAsyncComplete);
這里把要做的工作放在隊(duì)列里面了,所以不會(huì)阻塞當(dāng)前線程。WorkAsync是在工作線程中運(yùn)行的,WorkAsyncComplete是回調(diào),由livuv觸發(fā),回到工作線程。再來(lái)看看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拿到我們要操作的數(shù)據(jù),線程之間可以共享堆上的數(shù)據(jù),所以這里訪問沒有問題。
再看看回調(diào)如何執(zhí)行。
// 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;
}
注意看一下關(guān)鍵代碼,我們新建了一個(gè)function并調(diào)用,這個(gè)函數(shù)就是callback。
Local<Function>::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
最后我們看一下線程的情況
- js執(zhí)行線程: 事件循環(huán)+回調(diào),js代碼的執(zhí)行,都在這里
-
libuv啟動(dòng)的線程:用來(lái)做i/o和計(jì)算,比如讀取一個(gè)文件,這樣我們就不會(huì)被慢速的i/o拖累了。
線程情況
從上圖可以看到CalculateResultsAsync結(jié)束的時(shí)候,V8 Locals全部都會(huì)銷毀,所以我們的回調(diào)需要是Persistent。
Persistent<Function> callback;
可以在workthread里面訪問v8的內(nèi)存嗎?
答案是不能,v8不能多線程訪問,如果需要多線程訪問,需要加鎖,而node在啟動(dòng)的時(shí)候在主線程就會(huì)獲得鎖,可以在node.cc中的start函數(shù)看到
Locker locker(node_isolate);
所以工作線程是沒機(jī)會(huì)獲得鎖的。所以上面使用的copy數(shù)據(jù)的方法。具體的說(shuō)明可以看這個(gè)文章
包裝對(duì)象
由于上面并沒有說(shuō)明如何包裝C++對(duì)象并返回給js,這里又切回官方文檔demo,說(shuō)明如何包裝C++對(duì)象,然后再JavaScript中用new去新建對(duì)象。
本文引用的代碼是在紅框范圍內(nèi):

- addon.cc
#include <node.h>
#include "myobject.h"
using namespace v8;
void InitAll(Handle<Object> exports) {
MyObject::Init(exports);
}
NODE_MODULE(addon, InitAll)
可以看到宏還是那些宏,只是現(xiàn)在調(diào)用了類MyObject的靜態(tài)方法Init來(lái)導(dǎo)出函數(shù)。
- myobject.cc
這個(gè)文件要看的比較多,我們先看init函數(shù)
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);設(shè)置有每個(gè)JavaScript對(duì)象有幾個(gè)暴露的函數(shù)或者屬性,這邊只有一個(gè)NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);,注意設(shè)置在原型上 - 設(shè)置構(gòu)造函數(shù)
constructor
再看看New函數(shù),這個(gè)函數(shù)會(huì)在JavaScript使用new關(guān)鍵字創(chuàng)建對(duì)象的時(shí)候被調(diào)用。
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));
}
}
我們看到幾個(gè)關(guān)鍵地方
-
IsConstructCall來(lái)判斷是否是用new來(lái)調(diào)用的,或者是用函數(shù)方式直接調(diào)用,這里我們只看new,因?yàn)檫@是我們通常使用JavaScript對(duì)象的方式。 - 創(chuàng)建對(duì)象
obj->Wrap(args.This()); -
obj->Wrap(args.This());用來(lái)設(shè)置this指針,我們知道使用new創(chuàng)建對(duì)象的時(shí)候,this就是當(dāng)前創(chuàng)建的對(duì)象。 - 最后返回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內(nèi)部,調(diào)用native的都是這樣的,我們寫addon的時(shí)候,可以不這樣加載。
再看看plus函數(shù)
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函數(shù),和上面的wrap函數(shù)對(duì)應(yīng),另外C++中plusone第一個(gè)參數(shù)this,所以可以訪問內(nèi)部私有變量。
好了,其他的幾個(gè)demo都大同小異,這里就不寫下來(lái)了,希望這篇文章能幫助大家理解node addon的原理。
總結(jié)
- 本文介紹了ScotteFree的例子,掌握了JavaScript和C++傳遞數(shù)據(jù)的方法。
- 理清了js線程和工作線程的區(qū)別。
- 在現(xiàn)實(shí)環(huán)境中,v8接口和libuv的接口都會(huì)改變,這給我們編寫addon帶來(lái)了麻煩,NAN庫(kù)可以幫我們解決,所以如果真的要寫addon,應(yīng)該看看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
