相关文章推荐
考研的绿豆  ·  Libuv 1.18.0 发布,Node ...·  3 天前    · 
高兴的猴子  ·  delphi中VirtualStringTr ...·  1 周前    · 
坏坏的铁板烧  ·  TVirtualStringTreeView ...·  1 周前    · 
玩命的牛肉面  ·  .net - How to create ...·  1 年前    · 
英俊的针织衫  ·  python - ...·  1 年前    · 

最近开发涉及到了一些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) {
    // 获取环境运行的沙盒isolate
    Isolate *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 arguments
    at 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对象,返回给js
        Clazz *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