最近的任务是把计划库的API用JavaScript语言调用起来,需要用Node.js的C++扩展,本文简单归总一下node.js addons官方文档https://nodejs.org/api/addons.html

1. 基本知识介绍

  在node.js中,除了用js写代码以外,还可以使用C++编写扩展,这有点类似DLL,动态链接进js代码中。使用上也相当方便,只需用require包含,这和一般的js模块并没有什么区别。C++扩展为js和C++代码的通信提供了一个接口。

  要编写node.js的C++扩展,需要了解一些基本知识:

1. V8: Google出品的大名鼎鼎的V8引擎,它实际上是一个C++类库,用来和 JavaScript 交互,比如创建对象,调用函数等等。V8的API大部分都声明在v8.h头文件中。
2. libuv:一个C实现的事件循环库,node.js使用libuv来实现自己的事件循环、工作线程和所有的异步行为。它是一个跨平台的,高度抽象的lib,提供了简单易用的、POSIX-like的方式来让操作系统和系统任务进行交互。比如和文件系统、sockets、定时器和系统事件。libuv还提供了POSIX threads线程级别的抽象来增强标准事件循环中不具备的复杂异步能力。我们鼓励C++扩展的作者思考如何通过转换I/O或其他耗时操作到非阻塞系统操作来避免阻塞事件循环。
3. node.js内部lib,node.js本身提供了很多C/C++ API来给扩展使用,比如最重要的一个:node::ObjectWrap类。
4. node.js包含了很多静态链接库,比如OpenSSL。这些库都放在node.js代码树的deps/目录下。只有V8和OpenSSL标识符被有意地被node.js重复导出来被各种扩展使用。

  下面快速地来看一个实例。

2. 第一个例子HelloWord

  下面的例子是一个简单的C++扩展,其功能相当于js的如下代码:

module.exports.hello = () => 'world';

  首先创建一个hello.cc:

// hello.cc
#include <node.h> namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value; void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
} void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
} NODE_MODULE(addon, init)
} // namespace demo 

  这个最简单的例子,已经出现了一些我们完全没有接触过的东西。大致解释一下:

1. 函数Method的参数类型是FunctionCallbackInfo<Value>&,FunctionCallbackInfo
2. Isolate,英文意思是“隔离”,在这里Isolate指的是一个独立的V8 runtime,可以理解为一个独立的V8执行环境,它包括了自己的堆管理器、GC等组件。后续的很多操作都要依赖于这个Isolate,后面我们会看到在很多操作中,都会使用Isolate的实例作为一个上下文传入。
(注:一个给定的Isolate在同一时间只能被一个线程访问,但如果有多个不同的Isolate,就可以给多个线程同时访问。不过,一个Isolate还不足以运行脚本,你还需要一个全局对象,一个执行上下文通过指定一个全局对象来定义一个完整的脚本执行环境。因此,可以有多个执行上下文存在于一个Isolate中,而且它们还可以简单安全地共享它们的全局对象。这是因为这个全局对象实际上属于Isolate,而却这个全局对象被Isolate的互斥锁保护着。)
3. 返回值需要用args.GetReturnValue().Set()来设置。
4. 向外导出方法需要在扩展的初始化函数中使用NODE_SET_METHOD(exports, Method_Name, Method);。如果有多个方法需要导出,就写多个NODE_SET_METHOD。

注意到node.js的C++扩展都必须按以下形式导出一个初始化函数(该函数名字可以随便设置一个):

void Initialize(Local<Object> exports);
NODE_MODULE(module_name, Initialize)

NODE_MODULE这行后面并没有分号(;),因为它并不是一个函数,你可以认为这是一个声明。module_name必须匹配最后生成的二进制文件的文件名(不包括.node后缀)。在hello.cc这个例子中,初始化函数是init,扩展模块名是addon。

构建(Building)

  写好源代码后我们就要把它编译成二进制的addon.node文件了。binding.gyp文件用来描述我们模块的构建配置,这个文件的内容是JSON形式的:

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

  实施构建操作需要用到node-gyp,如果尚未安装的话,需要运行(可能要用到sudo):

npm install -g node-gyp 

  来全局安装node-gyp。

  编写完binding.gyp文件,我们使用:

node-gyp configure

  来生成对应项目在当前平台的build目录。这将会在build目录下生成一个Makefile(Unix-like系统)或者一个vcxproj文件(Windows系统)还有一部分其他文件。接着,运行:

node-gyp build

  来生成一个编译过的addon.node文件,这个文件会被放在build/Release/目录下。

  build成功后,这个二进制的C++扩展就可以在node.js中使用require包含进来:

1 // hello.js
2 const addon = require('./build/Release/addon');
3 console.log(addon.hello()); // 'world'

  由于扩展的二进制文件的存放位置会根据编译方式不同而变化(有可能放在build/Debug/目录),所以可以用这种方式来引入扩展:

1 try {
2 return require('./build/Release/addon.node');
3 } catch (err) {
4 return require('./build/Debug/addon.node');
5 }

链接node.js依赖

  node.js使用一些静态链接库,比如V8、libuv和OpenSSL。所有扩展都必须链接V8,还有可能需要链接一些其他的库。典型情况下,使用#include <...>来include这些库(比如链接V8就是#include <v8.h>),node-gyp会自动找到这些库。然而,有几个注意事项需要说明:

1. node-gyp运行时,它会检测node.js的版本并且下载全部源码文件或者只是下载头文件。如果下载了全部源码文件,扩展就可以使用node.js的所有依赖,如果仅仅下载了头文件,则只有node.js导出的那些东西可以被使用。
2. node-gyp可以使用--nodedir选项来指定本地node.js映像,使用这个选项时,扩展可以使用全部的node.js依赖。

使用require加载C++扩展

  经过编译的node.js C++扩展的后缀名是.node(类似.so和.dll),require()函数会查找这些.node文件像初始化动态链接库那样初始化它们。

  当使用reqiure()时,.node后缀可以被省略。需要注意的是,node.js在使用reqiure()加载模块时,会优先加载js后缀的文件。比如说一个目录下有一个addon.js和一个addon.node,当使用require('addon')时,node.js会优先加载addon.js。

函数参数

  C++扩展可以暴露函数和对象出来让node.js访问。当从js中调用C++扩展中的函数时,实参和返回值必须映射到C/C++事先声明好的代码中。以下代码展示了C++扩展代码如何读取从js传递过来的函数实参和如何返回值:

// addon.cc
#include < node.h > namespace demo {
using v8: :Exception;
using v8: :FunctionCallbackInfo;
using v8: :Isolate;
using v8: :Local;
using v8: :Number;
using v8: :Object;
using v8: :String;
using v8: :Value; // This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo < Value > &args) {
Isolate * isolate = args.GetIsolate(); // Check the number of arguments passed.
if (args.Length() < 2) {
// Throw an Error that is passed back to JavaScript
isolate - >ThrowException(Exception: :TypeError(String: :NewFromUtf8(isolate, "Wrong number of arguments")));
return;
} // Check the argument types
if (!args[0] - >IsNumber() || !args[1] - >IsNumber()) {
isolate - >ThrowException(Exception: :TypeError(String: :NewFromUtf8(isolate, "Wrong arguments")));
return;
} // Perform the operation
double value = args[0] - >NumberValue() + args[1] - >NumberValue();
Local < Number > num = Number: :New(isolate, value); // Set the return value (using the passed in
// FunctionCallbackInfo<Value>&)
args.GetReturnValue().Set(num);
} void Init(Local < Object > exports) {
NODE_SET_METHOD(exports, "add", Add);
} NODE_MODULE(addon, Init)
} // namespace demo

  编译成功后,这个扩展可以被node.js使用require()包含并使用:

1 // test.js
2 const addon = require('./build/Release/addon');
3 console.log('This should be eight:', addon.add(3, 5));

回调函数

  一种很常见的做法是从js传递回调函数给C++调用,下面这个示例展示了如何做:

// addon.cc
#include < node.h > namespace demo { using v8: :Function;
using v8: :FunctionCallbackInfo;
using v8: :Isolate;
using v8: :Local;
using v8: :Null;
using v8: :Object;
using v8: :String;
using v8: :Value; void RunCallback(const FunctionCallbackInfo < Value > &args) {
Isolate * isolate = args.GetIsolate();
Local < Function > cb = Local < Function > ::Cast(args[0]);
const unsigned argc = 1;
Local < Value > argv[argc] = {
String: :NewFromUtf8(isolate, "hello world")
};
cb - >Call(Null(isolate), argc, argv);
} void Init(Local < Object > exports, Local < Object > module) {
NODE_SET_METHOD(module, "exports", RunCallback);
} NODE_MODULE(addon, Init) } // namespace demo

 解释:

1. 传递回调函数,其实和传递普通参数没什么大的区别,使用

Local<Function> cb = Local<Function>::Cast(args[0]);

 可以获得这个回调函数。然后需要显式声明这个回调函数的参数个数和参数数组:

const unsigned argc = 1;
Local<Value> argv[argc] = { String::NewFromUtf8(isolate, "hello world") };

 调用这个回调函数需要传入isolate、参数个数argc、参数数组argv:

cb->Call(Null(isolate), argc, argv);

2. Init函数和之前有点不同,上面这个扩展的Init()使用了两个参数的形式(之前都是单参数),其中第二个参数接受一个module对象: 

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", RunCallback); // 相当于直接导出整个模块作为方法
}

这将允许扩展使用单个函数的形式代替之前往exports中添加函数作为属性的方式来完全地重写exports。因此可以直接用扩展的名字作为函数名来调用,这适用于此扩展只对外暴露一个方法的情况: 

1 // test.js
2 const addon = require('./build/Release/addon');
3 addon((msg) => {
4 console.log(msg); // 'hello world'
5 });

  

node.js的C++入门的更多相关文章

  1. Node.js API快速入门

    Node.js API 快速入门 一.事件EventEmitter const EventEmitter = require('events'); class MyEmitter extends Ev ...

  2. Node.js学习笔记(2) - Node.js安装及入门hello world

    今天来简单的记录一下Node.js的安装配置以及简单的入门 一.Node.js的安装 1.windows下的安装 windows下的安装很简单,只需要去官网http://nodejs.org中,找到w ...

  3. Node.js+Express配置入门

    Node.js是一个Javascript运行环境(runtime).实际上它是对Google V8引擎进行了封装.V8引 擎执行Javascript的速度非常快,性能非常好.Node.js对一些特殊用 ...

  4. Node.js web快速入门 -- KoaHub.js

    介绍 KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架.可以直接在项目里使用 ES6/7(Generator Function, Class, Async & ...

  5. node.js,express入门看详细篇

    先最简单的代码 安装 npm install express app.js 代码内容 const express = require('express') const app = express() ...

  6. Node.js安装和入门 - 2行代码让你能够启动一个Server

    转自:http://josh-persistence.iteye.com/blog/1979552  备忘 Node.js是一个轻松构建快速,可扩展的网络应用平台建立在Chrome的JavaScrip ...

  7. node.js安装与入门使用

    一个基于 Chrome V8 引擎的 JavaScript 运行环境. Node.js 的包管理器 npm,是全球最大的开源库生态系统. 提供事件驱动和非阻塞I/O API,可优化应用程序的吞吐量和规 ...

  8. Node.js 模块系统入门

    在编程领域中,模块是自包含的功能单元,可以跨项目共享和重用.它们使开发人员的生活更加轻松,因为我们可以使用它来增加应用程序的功能,而不必亲自编写这些功能.它还让我们可以组织和解耦代码,从而使应用程序更 ...

  9. Node.js 之 express 入门 ejs include公共部分

    1. 直接进入express安装 因为之前有一篇文章我已经讲过怎么安装node了 而网上的教程也是非常多.所有直接进入到express.教程简陋 由于我比较笨 所有只要写到我自己明白就行. 这里有个教 ...

随机推荐

  1. oop(面向对象)中的内置函数

    oop中的内置函数 ​ 类中存在一些名字带有双下划线__开头的内置函数, 这些函数会在某些时候被自动调用,例如之前学习的迭代器__init__函数 一.isinstance(obj, cls) 检查o ...

  2. JavaScript 事件 继承链 元素和结点

    继承链: HTMLElement->Element->Node->EventTarget->Object document和body:document代表整个文档,body是指 ...

  3. C语言:假定输入的字符串只包含字母和*号,fun函数:除了尾部的*号以外,将字符的其他*号进行全部删除,形参p已经指向字符串中最后一个字母。-利用折半查找整数m在有序数组中的位置,若找到,返回下标值,否则返回-1。

    //假定输入的字符串只包含字母和*号,fun函数:除了尾部的*号以外,将字符的其他*号进行全部删除,形参p已经指向字符串中最后一个字母. #include <stdio.h> void f ...

  4. LeetCode练题——35. Search Insert Position

    1.题目 35. Search Insert Position Easy 1781214Add to ListShare Given a sorted array and a target value ...

  5. vim 一些操作

    在ESC下 gg # 光标跳到开头 dG # 删除光标后的数据 dd # 删除光标所在行 gg dG # 删除全部 (光标跳到开头&删除光标后的数据) x # 删除当前光标下的字符 i # 编 ...

  6. Perl 笔记

    目录 Perl 学习 常用记录 基础 1. 运行perl 2. 字符串 3. 变量 4. 条件 5. 循环 6. 运算符 7. 时间日期 8. 子程序(函数) 9. 引用 10. 格式化输出 11. ...

  7. Jmeter进行分布式性能测试

    由于Jmeter本身的瓶颈,当需要模拟数以千计的并发用户时,使用单台机器模拟所有的并发用户就有些力不从心,甚至还会引起JAVA内存溢出的错误.要解决这个问题,可以使用分布式测试,运行多台机器运行所谓的 ...

  8. springmvc中整合mongodb副本集配置文件

    配置文件jdbc.properties: mongo.hostport=192.168.100.100:28007,192.168.100.110:28008,192.168.100.120:2800 ...

  9. cemtos安装python

    mkdir python3cd python3/yum -y install gcc*yum install zlib-devel bzip2-devel openssl-devel ncurses- ...

  10. Docker安装部署ELK教程(Elasticsearch+Kibana+Logstash+Filebeat)

    Elasticsearch 是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等. Logstash 是一个完全开 ...