Node.js介绍4-Addon

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

可以阅读node-ffi

调用的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资源,阻塞主线程,所以希望放到另一个线程中。

Node.js介绍4-Addon_第1张图片
代码结构

我们看看在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 
#include 
#include 
#include "rainfall.h"
#include 
#include 
#include 
#include 
#include 
#include 

using namespace v8;

通过头文件可以看到addon需要和v8,libuv,node打交道。

导出函数

下面的代码很容易看出是把函数加入到exports中。exports就是js中的对象。

void init(Handle  exports, Handle 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& args) {
  Isolate* isolate = args.GetIsolate();

  location loc = unpack_location(isolate, Handle::Cast(args[0]));
  double avg = avg_rainfall(loc);

  Local retval = v8::Number::New(isolate, avg);
  args.GetReturnValue().Set(retval);
}

 
 
  1. 从上面代码我们看出,首先要获得isolate,下面的api都需要这个作为参数,从这里可以看出,这些api都很底层还是比较繁琐的。
  2. 拿参数Handle::Cast(args[0])
  3. 返回值给JavaScript:args.GetReturnValue().Set(retval);

    • 拿参数中JavaScript对象,返回对象
    void RainfallData(const v8::FunctionCallbackInfo& args) {
      Isolate* isolate = args.GetIsolate();
    
      location loc = unpack_location(isolate, Handle::Cast(args[0]));
      rain_result result = calc_rain_stats(loc);
    
      Local obj = Object::New(isolate);
      pack_rain_result(isolate, obj, result);
    
      args.GetReturnValue().Set(obj);
    }
     
     

    我们看到拿对象是一样的,这里Local obj = Object::New(isolate);是关键代码。创建了一个V8的对象。然后返回。


    • 传递返回数组
    void CalculateResults(const v8::FunctionCallbackInfo&args) {
        Isolate* isolate = args.GetIsolate();
        std::vector locations;
        std::vector results;
    
        // extract each location (its a list)
        Local input = Local::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::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 result_list = Array::New(isolate);
        for (unsigned int i = 0; i < results.size(); i++ ) {
          Local 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 input = Local::Cast(args[0]);
     Local result_list = Array::New(isolate);
    

    • 异步
      node强的地方就是大部分api都是异步的,那么我们来看看他是怎么做到的,我们知道底层c的api都是同步,所以node必须的包装并使用线程来支持异步。我们看看代码。
    void CalculateResultsAsync(const v8::FunctionCallbackInfo&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 input = Local::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::Cast(input->Get(i))));
        }
    
        // store the callback from JS in the work package so we can
        // invoke it later
        Local callback = Local::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 callback;
    
      std::vector locations;
      std::vector results;
    };
    
    // called by libuv worker in separate thread
    static void WorkAsync(uv_work_t *req)
    {
        Work *work = static_cast(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 result_list = Array::New(isolate);
        Work *work = static_cast(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 result = Object::New(isolate);
          pack_rain_result(isolate, result, work->results[i]);
          result_list->Set(i, result);
        }
    
        // set up return arguments
        Handle argv[] = { result_list };
    
        // execute the callback
        // https://stackoverflow.com/questions/13826803/calling-javascript-function-from-a-c-callback-in-v8/28554065#28554065
        Local::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
    
        // Free up the persistent function callback
        work->callback.Reset();
        delete work;
    
    }
     
     

    注意看一下关键代码,我们新建了一个function并调用,这个函数就是callback。

        Local::New(isolate, work->callback)->Call(isolate->GetCurrentContext()->Global(), 1, argv);
    

    最后我们看一下线程的情况

    1. js执行线程: 事件循环+回调,js代码的执行,都在这里
    2. libuv启动的线程:用来做i/o和计算,比如读取一个文件,这样我们就不会被慢速的i/o拖累了。


      Node.js介绍4-Addon_第2张图片
      线程情况

    从上图可以看到CalculateResultsAsync结束的时候,V8 Locals全部都会销毁,所以我们的回调需要是Persistent。

    Persistent callback;
    

    可以在workthread里面访问v8的内存吗?

    答案是不能,v8不能多线程访问,如果需要多线程访问,需要加锁,而node在启动的时候在主线程就会获得锁,可以在node.cc中的start函数看到

      Locker locker(node_isolate);
    

    所以工作线程是没机会获得锁的。所以上面使用的copy数据的方法。具体的说明可以看这个文章

    包装对象

    由于上面并没有说明如何包装C++对象并返回给js,这里又切回官方文档demo,说明如何包装C++对象,然后再JavaScript中用new去新建对象。

    本文引用的代码是在红框范围内:


    Node.js介绍4-Addon_第3张图片
    代码
    • addon.cc
    
    #include 
    #include "myobject.h"
    
    using namespace v8;
    
    void InitAll(Handle exports) {
      MyObject::Init(exports);
    }
    
    NODE_MODULE(addon, InitAll)
    
     
     

    可以看到宏还是那些宏,只是现在调用了类MyObject的静态方法Init来导出函数。


    • myobject.cc
      这个文件要看的比较多,我们先看init函数
    void MyObject::Init(Handle exports) {
      Isolate* isolate = Isolate::GetCurrent();
    
      // Prepare constructor template
      Local 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);设置有每个JavaScript对象有几个暴露的函数或者属性,这边只有一个NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);,注意设置在原型上
    3. 设置构造函数constructor

    再看看New函数,这个函数会在JavaScript使用new关键字创建对象的时候被调用。

    void MyObject::New(const FunctionCallbackInfo& 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 argv[argc] = { args[0] };
        Local cons = Local::New(isolate, constructor);
        args.GetReturnValue().Set(cons->NewInstance(argc, argv));
      }
    }
    

    我们看到几个关键地方

    1. IsConstructCall来判断是否是用new来调用的,或者是用函数方式直接调用,这里我们只看new,因为这是我们通常使用JavaScript对象的方式。
    2. 创建对象obj->Wrap(args.This());
    3. obj->Wrap(args.This());用来设置this指针,我们知道使用new创建对象的时候,this就是当前创建的对象。
    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内部,调用native的都是这样的,我们写addon的时候,可以不这样加载。


    再看看plus函数

    void MyObject::PlusOne(const FunctionCallbackInfo& args) {
      Isolate* isolate = Isolate::GetCurrent();
      HandleScope scope(isolate);
    
      MyObject* obj = ObjectWrap::Unwrap(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

    你可能感兴趣的:(Node.js介绍4-Addon)