Node.js介紹4-Addon

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

可以閱讀node-ffi

調(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è)線程中。

代碼結(jié)構(gòu)

我們看看在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);
}

  1. 從上面代碼我們看出,首先要獲得isolate,下面的api都需要這個(gè)作為參數(shù),從這里可以看出,這些api都很底層還是比較繁瑣的。
  2. 拿參數(shù)Handle<Object>::Cast(args[0])
  3. 返回值給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);

最后我們看一下線程的情況

  1. js執(zhí)行線程: 事件循環(huán)+回調(diào),js代碼的執(zhí)行,都在這里
  2. 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());
}
  1. 這里使用到了FunctionTemplate
  2. tpl->InstanceTemplate()->SetInternalFieldCount(1);設(shè)置有每個(gè)JavaScript對(duì)象有幾個(gè)暴露的函數(shù)或者屬性,這邊只有一個(gè)NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);,注意設(shè)置在原型上
  3. 設(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)鍵地方

  1. IsConstructCall來(lái)判斷是否是用new來(lái)調(diào)用的,或者是用函數(shù)方式直接調(diào)用,這里我們只看new,因?yàn)檫@是我們通常使用JavaScript對(duì)象的方式。
  2. 創(chuàng)建對(duì)象obj->Wrap(args.This());
  3. obj->Wrap(args.This());用來(lái)設(shè)置this指針,我們知道使用new創(chuàng)建對(duì)象的時(shí)候,this就是當(dāng)前創(chuàng)建的對(duì)象。
  4. 最后返回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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容