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

代码结构

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

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

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

  1. js执行线程: 事件循环+回调,js代码的执行,都在这里
  2. 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());
}
  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<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));
  }
}

我们看到几个关键地方

  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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容