Node.js调用C/C++

标签:node.js v8 c/c++

占坑先。最近在看node的源码,心血来潮,整理一下Node.js调用C++的方法。

ps:简书不支持页内跳转,略坑。


<h2 id="catalog">目录</h2>


Node.js是在V8引擎基础上,做了封装和改进,让JavaScript能够运行在服务端。

JavaScript的基本类型、函数、对象都在V8中实现了。如果要改写自己的某些模块为C/C++或封装的C/C++库以便Node.js调用,需要先了解V8conceptstructureapi(戳我)。

Let us begin!


<h2 id="install">安装</h2>

install node-gyp with npm install node-gyp -g

具体安装需要的步骤可以参见其github主页。

  • On Unix:
  • python (v2.7 recommended, v3.x.x is not supported)
  • make
  • A proper C/C++ compiler toolchain, like GCC
  • On Mac OSX:
  • python (as Unix) (already installed on Mac OSX, if you install v3.x.x replaced, you can install with npm install node-gyp -g --python=python2.7
  • Xcode (easily installed)
  • Windows (never mind) (/= _ =)/~┴┴

返回目录


<h2 id="hello_world">一个简单的例子</h2>

从一个简单的例子开始,说明JavaScript调用C/C++的流程。示例函数包含3部分:

  • 配置文件
  • js文件
  • cc文件

配置文件

binding.gyp(一定要叫这个名字 (/= _ =)/~┴┴)

简单的配置如下:

{
  "targets": [
    {
      "target_name": "test",
      "sources": [ "test.cc" ]
    }
  ]
}

详细的方式(戳我)。

js文件

test.jshello接受两个参数,一个字符串,一个回调函数:

var test = require('./build/Release/test');
test.hello('test', function(data) {
  console.log(data);
});

cc文件

test.cc,文件应用两个头部node.hv8.h,整体结构和nodemodule非常相似,都是先定义Function/Object,然后export

#include <node.h>
#include <v8.h>

using namespace v8;

// 传入了两个参数,args[0] 字符串,args[1] 回调函数
void hello(const FunctionCallbackInfo<Value>& args) {
  // 使用 HandleScope 来管理生命周期
  Isolate* isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);

  // 判断参数格式和格式
  if (args.Length() < 2 || !args[0]->IsString()) {
    isolate->ThrowException(Exception::TypeError(
      String::NewFromUtf8(isolate, "Wrong arguments")));
    return;
  }

  // callback, 使用Cast方法来转换
  Local<Function> callback = Local<Function>::Cast(args[1]);
  Local<Value> argv[1] = {
    // 拼接String
    String::Concat(Local<String>::Cast(args[0]), String::NewFromUtf8(isolate, " world"))
  };
  // 调用回调, 参数: 当前上下文,参数个数,参数列表
  callback->Call(isolate->GetCurrentContext()->Global(), 1, argv);
}

// 相当于在 exports 对象中添加 { hello: hello }
void init(Handle<Object> exports) {
  NODE_SET_METHOD(exports, "hello", hello);
}

// 将 export 对象暴露出去
// 原型 `NODE_MODULE(module_name, Initialize)`
NODE_MODULE(test, init);

编译和使用

在项目根目录下使用:

node-gyp configure
node-gyp build

在mac上,如果python版本不对,可以带上--python=python2.7

返回目录


Hello World讲解完毕,下面整理下V8的相关知识。


<h2 id="v8">Google V8引擎</h2>

JavaScript的所有基本对象都在v8.h中定义了,找不到api的时候直接看源码即可。

<h3 id="v8_value">Value</h3>

DataJavaScript中各种结构的基础,ValueJavaScript中各种对象的基类。

JavaScript 类型在 C++ 中均有对应的自定义类型,如 StringObjectDateArray 等,严格遵守在 JavaScript 中的继承关系。

Data继承树
Data继承树

返回目录

<h3 id="v8_handle">Handle</h3>

V8里使用Handle类型来托管 JavaScript 对象,与C++std::sharedpointer类似,Handle类型间的赋值均是直接传递对象引用,但不同的是,V8使用自己的GC 来管理对象生命周期,而不是智能指针常用的引用计数。

C++中使用这些类型时,必须使用Handle 托管,以使用GC来管理它们的生命周期,而不使用原生栈和堆。

Handle
Handle

Handle分为LocalPresistentLocal可以被HandleScope管理。

Presistent相当于全局,不受HandleScope管理,需要Persistent::New/Persistent::Dispose配对使用,相当于C++new/delete

Persistent::MakeWeak:如果一个对象的唯一引用Handle是一个Persistent,则可以使用MakeWeak方法来如果该引用,该方法可以触发GC对被引用对象的回收。

返回目录

<h3 id="v8_handle_scope">HandleScope</h3>

HandleScope则相当于用来装Handle(Local)的容器,当HandleScope生命周期结束的时候,Handle也将会被释放,会引起Heap中对象引用的更新。

HandleScope是分配在栈上,不能通过New的方式进行创建。对于同一个作用域内可以有多个HandleScope,新的HandleScope将会覆盖上一个HandleScope,并对Handle(Local)进行管理。

所以在编写C++函数时,开头都是固定的写法:

Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);

返回目录

<h3 id="v8_context">Context</h3>

Context是JavaScript的执行环境。每个JavaScript都必须执行在一个Context中。Context有多个,而且可以在不同的Context中进行切换。

Hello中,我们使用了isolate->GetCurrentContext()来获取上下文。

或者可以这样新建一个Context

// 申请一个context
Persistent<Context> context = Context::New(); 
// 切换
Context::Scope context_scope(context);

下面这张图就讲解的比较清晰:

Context
Context

返回目录


<h2 id="practice">Practice</h2>

Hello World中,展示了参数argscallbackThrowException的用法。下面来讲一下其他例子。

官方教程中有很多例子,可以看看。

<h3 id="cc_class">Class</h3>

下面通过一个例子说明C++类的使用。

binding.gyp

{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "point.cc"]
    }
  ]
}

addon.cc

addon.cc,这个是module.exports

#include <node.h>
#include "point.h"

using namespace v8;

void InitAll(Handle<Object> exports) {
  Point::Init(exports);
}

NODE_MODULE(addon, InitAll);

C++ 头文件

定义一个Point类:

#ifndef POINT_H
#define POINT_H

#include <node.h>
#include <node_object_wrap.h>

// 继承node的ObjectWrap,一般自定义C++类都应该继承node的ObjectWrap
class Point : public node::ObjectWrap {
public:
    // 静态方法,用于注册类和方法
    static void Init(v8::Handle<v8::Object> exports);
    
private:
    // 不允许转换构造函数进行的隐式转换
    explicit Point(double _x = 0, double _y = 0);
    ~Point();
    // new方法
    static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
    // move(x, y)
    static void Move(const v8::FunctionCallbackInfo<v8::Value>& args);
    // toString方法
    static void toString(const v8::FunctionCallbackInfo<v8::Value>& args);
    static v8::Persistent<v8::Function> constructor;
    // fields
    double _x;
    double _y;
};

#endif

C++ 文件

point.cc,具体的Point实现如下:

#include <iostream>
#include <sstream>
#include <string>
#include "point.h"

using namespace std;
using namespace v8;

Persistent<Function> Point::constructor;
// 构造函数
Point::Point(double _x, double _y) {
    this->_x = _x;
    this->_y = _y;
}

Point::~Point() {
}

void Point::Init(Handle<Object> exports) {
    Isolate* isolate = Isolate::GetCurrent();
    
    // Function模板
    Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
    // 类名
    tpl->SetClassName(String::NewFromUtf8(isolate, "Point"));
    // InternalField
    tpl->InstanceTemplate()->SetInternalFieldCount(1);
    
    // 设置Prototype函数
    NODE_SET_PROTOTYPE_METHOD(tpl, "move", Move);
    NODE_SET_PROTOTYPE_METHOD(tpl, "toString", toString);
    // 设置constructor
    constructor.Reset(isolate, tpl->GetFunction());
    // export `Point`
    exports->Set(String::NewFromUtf8(isolate, "Point"),
                 tpl->GetFunction());
}

void Point::New(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = Isolate::GetCurrent();
    HandleScope scope(isolate);
    
    if (args.IsConstructCall()) {
        // 使用new调用 `new Point(...)`
        double _x = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
        double _y = args[1]->IsUndefined() ? 0 : args[1]->NumberValue();
        // new一个对象
        Point* point = new Point(_x, _y);
        // 包装this指针
        point->Wrap(args.This());
        args.GetReturnValue().Set(args.This());
    } else {
        // 使用`Point(...)`
        const int argc = 2;
        Local<Value> argv[argc] = { args[0], args[1] };
        // 使用constructor构建Function
        Local<Function> cons = Local<Function>::New(isolate, constructor);
        args.GetReturnValue().Set(cons->NewInstance(argc, argv));
    }
}

void Point::Move(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = Isolate::GetCurrent();
    HandleScope scope(isolate);
    // 解封this指针
    Point* point = ObjectWrap::Unwrap<Point>(args.Holder());
    // 参数
    double _x = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
    double _y = args[1]->IsUndefined() ? 0 : args[1]->NumberValue();
    // move
    point->_x += _x;
    point->_y += _y;
}

void Point::toString(const v8::FunctionCallbackInfo<v8::Value>& args) {
    Isolate* isolate = Isolate::GetCurrent();
    HandleScope scope(isolate);
    // 解封this指针
    Point* point = ObjectWrap::Unwrap<Point>(args.Holder());
    // c++流处理
    ostringstream oss;
    oss << "(" << point->_x << ", " << point->_y << ")";
    // 设置返回值
    args.GetReturnValue().Set(String::NewFromUtf8(isolate, oss.str().c_str()));
}

InternalField

JS new一个对象的时候,C++中也会同步的new一个对象并将该指针保存在C++内部,并维护这个指针list,这就是V8 InternalField的作用。所有需要跟JS绑定的C++指针都存在这个InternalField中,其实就是一个list,一个V8 Object可以拥有任意数量的InternalField

Wrap和Unwrap

Move方法中用到了Wrap方法,toString中用到了Unwrap方法。WrapUnwrap是专门用来专门用来封装C++指针的。

返回目录

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

推荐阅读更多精彩内容