最近开发涉及到了一些Node.js调用C++的地方,于是网上搜了一下,发现网上好多文章都是比较片面的东西,没法直接使用。于是花点时间总结一下。
Android开发中Java 调用C++的部分叫JNI,Rust语言中调用C++的部分叫FFI,Node.js中调用C++的部分叫C++ Addons。
本文总结Node.js使用非N-API方式调用C++函数的示例,主要针对node 8版本,不同版本会有api差异。
主要内容有:1. 工程框架HelloWorld; 2. 两种语言间不同类型怎么转换; 3. 回调函数和异常处理;4. 如何包裹C++类函数。

Node.js 调用C++方法,其实是调用 C++ 代码生成的动态库,可以使用require() 函数加载到Node.js中,就像使用普通的Node.js模块一样。

Node.js官方提供了两种调用C++的方法一种是引用v8.h等头文件直接使用相关函数,另一种是使用其包裹的Native Abstractions for Node.js (nan)进行开发。鉴于node.js版本升级实在是太快了(Ubuntu 18.04 apt 最新版是node 8, Ubuntu 20.04 apt 最新版是node 10,官方最新版是node 15),官方推荐使用第二种方法。

但是由于我们现有项目使用的是第一种方法,且使用的是node 8版本,所以这篇文章主要介绍基于node version 8的直接引用v8.h头文件调用C++的方式,可能也会夹杂一些其他版本的说明。不同版本的node.js提供的原生接口函数形式会有一些差异,详细说明可以参考Node.js官方文档,那里示例比较齐全,我也是参考的官方文档整理的。

Hello World

我们码农都知道 HelloWorld 意味着什么,所以这一节主要通过 HelloWorld 来介绍一下这个工作流程。
先说一下工程目录结构,通常把C++代码放在src目录下面,一级目录下有个binding.gyp文件,这个是C++代码的编译脚本,使用node-gyp进行编译,binging.gyp 就是 node-gyp 的编译脚本,准确一些比喻的话 这个 node-gyp 类似 cmake,binding.gyp 类似 CMakeLists.txt,都是先生成 Makefile 再进行编译的。

HelloWorld├── binding.gyp├── index.js└── src└── hello.cc

C++ 文件

接下来看看hello.cc中的代码

#include <node.h>using namespace v8;// 一个能返回JS字符串"Hello World!"的函数
void sayHello(const FunctionCallbackInfo<Value> &args) {Isolate *isolate = args.GetIsolate();args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World!"));
}// 和js模块一样,有两种初始化函数
// 导出方式类似  exports.Hello = sayHello;
void Initialize(Local<Object> exports) {NODE_SET_METHOD(exports, "Hello", sayHello);
}// 导出方式类似 module.exports = sayHello;
void Initialize2(Local<Object> exports, Local<Object> module) {NODE_SET_METHOD(module, "exports", sayHello);
}// 注意:
// NODE_MODULE()后面没有分号,因为它不是一个函数
// 官方文档说模块名称(这里是hello)必须与最终二进制文件的文件名匹配(不包括.node后缀),不过不匹配好像也行
NODE_MODULE(hello, Initialize)  // 这里我们使用第一种导出方法进行注册

编译脚本 binding.gyp

然后看看编译脚本 binding.gyp 的内容,这是一个JSON结构的文本,我们示例的模块名为hello,sources 后面是C++源码。

{'targets': [{'target_name': 'hello','sources': [ 'src/hello.cc',]}]
}

编译C++

刚才说了要是用node-gyp命令进行编译,注意这个node-gyp要和node版本一致,所以要使用的npm install -g node-gyp进行安装。使用node-gyp configure生成Makefile,再使用node-gyp build进行编译。也可以一步到位node-gyp configure build

node-gyp configure
node-gyp build
或
node-gyp configure build

一切OK的话会生成build\Release\hello.node文件,这个node文件其实就是动态库,linux下是so,windows下是dll。node.js v8引擎会使用dlopen的方式加载这个动态库。工程目录如下所示

HelloWorld
├── binding.gyp
├── build
│   ├── binding.Makefile
│   ├── config.gypi
│   ├── hello.target.mk
│   ├── Makefile
│   └── Release
│       ├── hello.node
│       └── obj.target
│           ├── hello
│           │   └── src
│           │       └── hello.o
│           └── hello.node
├── index.js
└── src└── hello.cc

node.js 调用

最后可以像调用普通js模块一样引用这个库了。

const hello = require('./build/Release/hello.node');console.log(hello.Hello()); // 输出:Hello World!// for Initialize2 第二种导出方式可以这么调用
// console.log(hello()); // Hello World!

到此为止已经对整个工作流程有了个大致的认识,接下来无非就是类型转换等 API 的使用了。

基本类型转换

前面的 HelloWorld 已经介绍了怎么调用 C++ 函数,接下来就是两种语言间不同类型的转换,类型转换分为两种,一种是JS类型转为C++类型,另一种是C++类型转为JS类型,下面通过示例来看看。(主要说明node 8版本,其他版本编译出错的话,自行查阅官方文档,不同版本之间大同小异)

整型和浮点型

整型主要有 int32 uint32 int64,浮点型主要有double。示例都只有一个参数,返回类型和输出类型一致。
For node version 8

void passInt32(const FunctionCallbackInfo<Value> &args){int value = args[0]->Int32Value();  // 输入参数转换为 int32 类型args.GetReturnValue().Set(value);   // 直接调用Set可以返回int类型
}void passUInt32(const FunctionCallbackInfo<Value> &args){uint32_t value = args[0]->Uint32Value();  // 输入参数转换为 uint32 类型args.GetReturnValue().Set(value);
}void passInt64(const FunctionCallbackInfo<Value> &args){int64_t value = args[0]->IntegerValue(); // 输入参数转换为 int64 类型args.GetReturnValue().Set(args[0]);  // 在v8版本里面没找到怎么返回int64类型的函数
}void passDouble(const FunctionCallbackInfo<Value> &args){double value = args[0]->NumberValue();   // 输入参数转换为 double 类型args.GetReturnValue().Set(value);  // 可以直接返回double类型
}

下面是js调用的示例(注册函数省略了),输出数字和输入参数一样。

const mylib = require('./build/Release/mylib.node');
console.log(mylib.passInt32(-1));
console.log(mylib.passUInt32(4294967295));
console.log(mylib.passInt64(-1));
console.log(mylib.passDouble(-1.23));

布尔类型

For node version 8

void passBool(const FunctionCallbackInfo<Value> &args){bool value = args[0]->BooleanValue();  // 获取输入布尔类型args.GetReturnValue().Set(value);  // 可以直接返回bool类型
}

JS调用方法与前面的一样也没啥好说的。

const mylib = require('./build/Release/mylib.node');
console.log(mylib.passBool(false));

字符串类型

字符串类型与数值类型稍有不同。从现在开始会频繁使用到一个Isolate 类型,这个东东可以认为是v8引擎的一个沙盒,不同线程可以有多个Isolate ,一个Isolate同时只能由一个线程访问。
For node version 8

void passString(const FunctionCallbackInfo<Value> &args) {// 获取环境运行的沙盒isolateIsolate *isolate = args.GetIsolate(); // 参数 args[0] 本质上是一个 v8::Value 类型,// 先把这个 Value转换为一个UTF8编码的字符串数组Utf8Value 类型// Utf8Value是一个封装`char* str_; int length_;`的类型,通过星号运算符重载返回str_// 然后就可以把这个类型构造成std::string类型了。std::string value = std::string(*String::Utf8Value(isolate, args[0]));// 从C++字符串转为js字符串用到了String::NewFromUtf8()函数,传入C风格字符args.GetReturnValue().Set(String::NewFromUtf8(isolate, value.c_str()));
}

JS调用同上。

const mylib = require('./build/Release/mylib.node');
console.log(mylib.passString('Hello'));

回调函数和异常处理

目前为止已经知道如何在JS和C++之间传递不同的基本参数类型了。回调函数是JS语言的一大特色,异常处理是现代编程语言都具备的一种语法。下面通过一个计算斐波拉契数列值的函数来看看这两种语法,此函数大概就是这样let f = (n, callback) => { callback(f(n)); }

For node version 8

#include <node.h>using namespace v8;// 这是一个计算斐波拉契数列的C函数
int f(int n) {return (n < 3) ? 1 : f(n - 1) + f(n - 2);
}// 注册这个函数给JS调用
void Fibonacci_Callback(const FunctionCallbackInfo<Value> &args) {Isolate *isolate = args.GetIsolate();//检查参数个数if (args.Length() != 2) {// 使用 String::NewFromUtf8() 构造一个JS字符串// 使用 Exception::TypeError() 构造一个异常类型// 使用 isolate->ThrowException 向JS抛出一个异常isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments")));return;}// 保证参数1是个整数,参数2是个回调函数,否则抛出异常if (!args[0]->IsInt32() || !args[1]->IsFunction()) {isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong arguments")));return;}// 获得整数参数int n = args[0]->Int32Value();int res = f(n); // 计算斐波拉契数值// 把参数2转换为一个函数类型Local<Function> cb = Local<Function>::Cast(args[1]);// 构造这个回调函数的参数,参数个数argc为1,参数数组argv中存储的是实际Value参数的值// 如果有多个参数就塞多个值在数组中const unsigned argc = 1;Local<Value> argv[argc] = { Number::New(isolate, res) };// 调用回调函数cb->Call(Null(isolate), argc, argv);
}

下面来看看JS中调用方法

const mylib = require('./build/Release/mylib.node');
// 正常输出 f(10) = 55
mylib.Fibonacci_Callback(10, (result) => {console.log('f(10) =', result); // f(10) = 55
});// 看一下异常
mylib.Fibonacci_Callback((result) => {console.log('f(10) =', result); // f(10) = 55
});

第二个函数参数不正确,运行后会抛出异常

mylib.Fibonacci_Callback((result) => {^
TypeError: Wrong number of argumentsat Object.<anonymous> (/home/xxxx/Node.js-Cpp-Addons/CommonFunctions/index.js:11:7)at Module._compile (module.js:653:30)at Object.Module._extensions..js (module.js:664:10)at Module.load (module.js:566:32)at tryModuleLoad (module.js:506:12)at Function.Module._load (module.js:498:3)at Function.Module.runMain (module.js:694:10)at startup (bootstrap_node.js:204:16)at bootstrap_node.js:625:3

调用 C++ 类方法

下面通过一个例子来展示怎么调用C++类中的方法。例子是这样的,有个C++类Clazz表示一个课堂吧,有个Add方法往里面添加学生,有个AllMembers方法返回这个课堂中有哪些人的字符串,这个例子有点呆,反正就是个类就是个集合。
这个例子目录结构是这样的。

├── binding.gyp
├── index.js
└── src├── addon.cc├── Clazz.cc└── Clazz.h

addon.cc 很简单,主要的代码都在Clazz类里面了。

#include <node.h>
#include "Clazz.h"using namespace v8;void InitAll(v8::Local<v8::Object> exports) {Clazz::Init(exports);
}NODE_MODULE(hello, InitAll)

Clazz.h 里面声明了内部函数和一些包裹的供JS调用的函数。

#ifndef CLAZZ_H_
#define CLAZZ_H_#include <node.h>
#include <node_object_wrap.h>#include <set>
#include <string>class Clazz : public node::ObjectWrap   // 要继承这个类
{public:static void Init(v8::Local<v8::Object> exports);private:static void New(const v8::FunctionCallbackInfo<v8::Value> &args);// 对C++成员函数就行包裹的对外函数static void Add(const v8::FunctionCallbackInfo<v8::Value> &args);static void AllMembers(const v8::FunctionCallbackInfo<v8::Value> &args);explicit Clazz(std::string className);~Clazz();//C++成员函数,添加和显示成员的实际函数void _Add(std::string member);std::string _AllMembers();static v8::Persistent<v8::Function> constructor;std::set<std::string> _members;std::string _className;
};#endif  // CLAZZ_H_

Clazz.cc

#include "Clazz.h"
#include <sstream>v8::Persistent<v8::Function> Clazz::constructor;void Clazz::Init(v8::Local<v8::Object> exports) {v8::Isolate *isolate = exports->GetIsolate();//准备构造函数(New函数里面实现构造)v8::Local<v8::FunctionTemplate> tpl = v8::FunctionTemplate::New(isolate, New);tpl->SetClassName(v8::String::NewFromUtf8(isolate, "Clazz"));tpl->InstanceTemplate()->SetInternalFieldCount(1);//注册类函数NODE_SET_PROTOTYPE_METHOD(tpl, "Add", Add);NODE_SET_PROTOTYPE_METHOD(tpl, "AllMembers", AllMembers);// constructor.Reset(isolate, tpl->GetFunction());exports->Set(v8::String::NewFromUtf8(isolate, "Clazz"), tpl->GetFunction());// An AtExit hook is a function that is invoked after the Node.js event loop has ended// but before the JavaScript VM is terminated and Node.js shuts down.// AtExit hooks are registered using the node::AtExit API.// 这是个Node运行完毕后执行的回调函数,一般在这里进行释放资源的操作。node::AtExit([](void *) { printf("in node::AtExit\n"); }, nullptr);
}// js调用的构造函数实现
void Clazz::New(const v8::FunctionCallbackInfo<v8::Value> &args) {v8::Isolate *isolate = args.GetIsolate();// 使用new操作符进行构造if (args.IsConstructCall()) {// Invoked as constructor: `new MyObject(...)`std::string cName =args[0]->IsUndefined() ? "Undefined" : std::string(*v8::String::Utf8Value(args[0]->ToString()));// new一个Clazz对象,返回给jsClazz *obj = new Clazz(cName);obj->Wrap(args.This());args.GetReturnValue().Set(args.This());  // Return this object} else {// Invoked as plain function `MyObject(...)`, turn into construct call.// js中构造可以不使用new操作符,这样给处理成使用new构造的逻辑const int argc = 1;v8::Local<v8::Value> argv[argc] = { args[0] };v8::Local<v8::Context> context = isolate->GetCurrentContext();v8::Local<v8::Function> cons = v8::Local<v8::Function>::New(isolate, constructor);// 执行完这句就直接进入到上面 if(args.IsConstructCall()) 语句了v8::Local<v8::Object> result =cons->NewInstance(context, argc, argv).ToLocalChecked();args.GetReturnValue().Set(result);}
}void Clazz::Add(const v8::FunctionCallbackInfo<v8::Value> &args) {// 使用Unwrap获得Clazz对象的实际指针Clazz *obj = ObjectWrap::Unwrap<Clazz>(args.Holder());// 转换js字符串成c++字符串std::string mem = std::string(*v8::String::Utf8Value(args[0]->ToString()));// 调用实际工作的函数添加成员obj->_Add(mem);return;
}void Clazz::AllMembers(const v8::FunctionCallbackInfo<v8::Value> &args) {v8::Isolate *isolate = args.GetIsolate();// 使用Unwrap获得Clazz对象的实际指针Clazz *obj = ObjectWrap::Unwrap<Clazz>(args.Holder());// 获取所有成员字符串并返回给js层std::string res = obj->_AllMembers();args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, res.c_str()));
}Clazz::Clazz(std::string className) : _className(className) {}// Node.js 8版本好像有点问题,没有显示这析构里面的代码。
// 14版本测试是可以显示这行log的,不知道是不是使用不当。
Clazz::~Clazz() {printf("~Clazz()\n");
}void Clazz::_Add(std::string member) {_members.insert(member);
}std::string Clazz::_AllMembers() {std::ostringstream os;os << "Class " << _className << " members: ";int i = 1;for (auto m : _members) {os << i++ << '.' << m << ' ';}os << '.';return os.str();
}

binding.gyp也很简单

{'targets': [{'target_name': 'mylib','sources': ['src/addon.cc','src/Clazz.cc']}]
}

JS index.js调用方法

const mylib = require('./build/Release/mylib.node');const clazz = new mylib.Clazz("Chinese");
// const clazz = mylib.Clazz("Chinese");  // 可以使用new也可以不使用new进行构造
clazz.Add('Tom');
clazz.Add('Mary');
clazz.Add('Liming');
console.log(clazz.AllMembers()); // 实际输出: Class Math: Liming Mary Tom .

这个例子是根据官方文档的用法随便写的一个小demo,js实际调用的函数其实是C++类成员函数的包裹函数。官方文档介绍的使用就这些,烦人的一点就是各个node不同版本的API差异实在有些大。

总结

本文主要描述了这几个方面的示例:

  1. 工程框架HelloWorld;
  2. 两种语言间不同类型怎么转换;
  3. 回调函数和异常处理;
  4. 如何包裹C++类函数。

本文主要针对的是node 8版本的API,其他类型可以参考官方文档,下面列出不同版本的官方文档链接。
Node.js v8 Documentation C++ Addons
Node.js v10 Documentation C++ Addons
Node.js v12 Documentation C++ Addons
Node.js v14 Documentation C++ Addons

下面是我整理的示例代码,也包含了部分其他node版本的代码,有需要可以参考。
https://github.com/lmshao/Node.js-Cpp-Addons

Node.js 调用 C++ 方法 / C++ Addons 详解相关推荐

  1. vue在created调用点击方法_vue.js中created方法的使用详解

    这次给大家带来vue.js中created方法的使用详解,使用vue.js中created方法的注意事项有哪些,下面就是实战案例,一起来看一下. 这是它的一个生命周期钩子函数,就是一个vue实例被生成 ...

  2. node.js卸载、安装、配置详解

    node.js卸载.安装.配置详解 一. node.js卸载 二.下载安装 2.1 下载 2.2 安装 2.2.1 选择msi安装 2.2.2 选择zip安装 三.配置 3.1 环境变量配置 3.2 ...

  3. java.servlet js,调用servlet方法

    <深入剖析Tomcat>一2.1 javax.servlet.Servlet接口 2.1 javax.servlet.Servlet接口 Servlet编程需要使用到javax.servl ...

  4. Node.js调用C#代码

    https://github.com/tjanczuk/edge 运行的时候会报 System.DllnotfoundException 无法加载node.dll,要把\packages\Edge.j ...

  5. node.js调用ejs模板,在浏览器上打印出ejs模板内代码的解决方案

    2019独角兽企业重金招聘Python工程师标准>>> 今天遇到一个非常奇葩的问题,node.js调用ejs模板的时候,在浏览器端居然把此模板内的所有代码都打印出来了,当时我和我的小 ...

  6. 虹软人证核验增值版-node.js调用C++SDK

    一.前言 随着5G时代的到来,人脸识别技术越来越贴近我们的生活,对于开发人员来说要面临的挑战也越来越艰巨.虹软作为国内领先的人脸识别算法厂商之一,提供了多平台多语言的人脸识别SDK,使用场景广泛.产品 ...

  7. node.js调用.c文件_在Node.js中分派S3文件

    node.js调用.c文件 Some of our intranet backends use S3 storage and GraphQL APIs. It's a common scenario ...

  8. Node.js与Sails~方法拦截器policies

    policies sails的方法拦截器类似于.net mvc里的Filter,即它可以作用在controller的action上,在服务器响应指定action之前,对这个action进行拦截,先执行 ...

  9. Unity,WebGL, 页面JS调用Unity方法

    与WebPlayer类似,在JS中用SendMessage 比如在Unity场景中有一个GameObject,叫A, A上有C#脚本,里面有个方法 public void F(string str) ...

最新文章

  1. 最小树形图复杂度分析
  2. soundex mysql_MySQL SOUNDEX()用法及代码示例
  3. 循环遍历Java字符串字符的规范方法——类似python for ch in string
  4. Python系列之Collections内置模块(2)
  5. OpenCV_02 图像的基本操作:图像IO+绘制图形+像素点+属性+图像通道+色彩空间的改变
  6. java将一个对象赋值给另一个对象_java一个对象赋值给另一个对象,支持平铺类和层级类间的互转...
  7. [JS][jQuery]清空元素html()、innerHTML= 与 empty()的区别 、remove()区别
  8. js设置了location.href不跳转问题
  9. L1-046 整除光棍 (20 分)—团体程序设计天梯赛
  10. Visio画图,空间太小,画不下
  11. 2021年了,还有人认为视觉导航不如激光导航
  12. 计算机硬盘格式化了如何恢复出厂设置,怎么把电脑格式化?
  13. 这37个自学网站,一年让你省下十几万。钱买辆车他不香嘛
  14. 华为笔记本没有HOME键和END键
  15. 一年时间,拿到了人生中的第一个20万
  16. 短视频如何添加封面图
  17. 2008年9月3号,星期三,晴。日日行,不怕千万里;常常做,不怕千万事。 ——《格言联璧•处事》
  18. [转]Facebook 如何管理150亿张照片
  19. Linux驱动学习--wifi驱动(rtl88xx系列网卡芯片)源码分析
  20. RFID项目中常见问题分析

热门文章

  1. 重启随机游走(RWR)算法
  2. Linux proxy 设置
  3. 数据堂将出席盖世汽车2021第三届汽车智能座舱与用户体验大会
  4. 以post的方式发请求,传参在url中
  5. JQuery中常用的 属性选择器
  6. NS2网络仿真的过程
  7. virtualbox虚拟机网络配置实现内网外网互通
  8. html中文本重复,在网页中去除文本列表中重复行与计算重复次数的代码原理
  9. 应用QQ2440(s3c2440)ARM开发板驱动MMA7455加速度计的linux设备驱动编写
  10. 今天来聊一聊互联网35岁梗,这个行业真的不需要35岁以上从业人员?