最近的任务是把计划库的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. jQuery+PHP+Ajax动态数字统计展示实例

    jQuery+PHP+Ajax实现的一款动态数字统计展示实例,本例是在页面上动态展示了当前在线用户数,当然了,你可以应用到其他更多场景中. 首先我们在#number放置要统计的数字: <div ...

  2. Error: EACCES: permission denied, open '/Users/qinmengjiao/WebstormProjects/m-kbs-app/.babelrc

    表示没有访问这个文件的权限 执行命令 sudo chown -R $(whoami) ~/WebstormProjects/m-kbs-app/.babelrc 就可以解决上面的问题 以下是chown ...

  3. 关于windows nginx不能启动问题的解决,史上最坑系列之一(原文)

    我是直接在官方网址下载windows1.6稳定版的nginx,之所以下载它是因为在window下方便学习,更好的在linux安装和学习nginx. 下载到D:\nginx学习\,解压它,并进入启动它 ...

  4. 一周搞定模拟电路P3_电容_记录

    1 电容的介绍 什么是电容 它有两个电极板,和中间板所夹的介质封装而成,具有特定功能的电子器件. 电容的作用 旁路.去耦.滤波和储能的作用 2 旁路电容的作用 1)使输入电压均匀化,减少噪声对后级的影 ...

  5. opencv python:Canny边缘提取

    Canny是边缘提取算法,在1986年提出的 是一个很好的边缘检测器 Canny算法介绍 非最大信号抑制: 高低阈值连接: example import cv2 as cv import numpy ...

  6. Hybrid App 开发快速指南

    链接:https://blog.csdn.net/valada/article/details/81639658

  7. SDNU_ACM_ICPC_2020_Winter_Practice_2nd

    A - [The__Flash]的矩阵 给你一个m×n的整数矩阵,在上面找一个x×y的子矩阵,使子矩阵中所有元素的和最大. Input输入数据的第一行为一个正整数T,表示有T组测试数据.每一组测试数据 ...

  8. 消息队列(二)--- RocketMQ-NameServer阅读

    概述   所有broker在启动的时候都会向NameServer进行注册,对它进行发送心跳包. 源码阅读 我们先从 NamesrvStartup这个类分析 public static void mai ...

  9. 基于SILVACO ATLAS的a-IGZO薄膜晶体管二维器件仿真(03)

    今天逛ResearchGate的时候发现了一个不错的Atlas入门教程:Step by step with ATLAS Silvaco点击链接免费下载.. Atlas代码结构 当然可能有一点太基础了. ...

  10. Ansible自动化搭建及工具集和常见模块、命令详情(重点)

    一.ansible介绍 1.ansible简介 官方的title是“Ansible is Simple IT Automation”——简单的自动化IT工具. Ansible跟其他IT自动化技术的区别 ...