最近开发涉及到了一些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官方文档
,那里示例比较齐全,我也是参考的官方文档整理的。
我们码农都知道 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
接下来看看hello.cc
中的代码
#include <node.h>
using namespace v8;
void sayHello(const FunctionCallbackInfo<Value> &args) {
Isolate *isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World!"));
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "Hello", sayHello);
void Initialize2(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", sayHello);
NODE_MODULE(hello, Initialize)
然后看看编译脚本 binding.gyp
的内容,这是一个JSON结构的文本,我们示例的模块名为hello,sources 后面是C++源码。
'targets': [
'target_name': 'hello',
'sources': [
'src/hello.cc',
刚才说了要是用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
最后可以像调用普通js模块一样引用这个库了。
const hello = require('./build/Release/hello.node');
console.log(hello.Hello());
到此为止已经对整个工作流程有了个大致的认识,接下来无非就是类型转换等 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();
args.GetReturnValue().Set(value);
void passUInt32(const FunctionCallbackInfo<Value> &args){
uint32_t value = args[0]->Uint32Value();
args.GetReturnValue().Set(value);
void passInt64(const FunctionCallbackInfo<Value> &args){
int64_t value = args[0]->IntegerValue();
args.GetReturnValue().Set(args[0]);
void passDouble(const FunctionCallbackInfo<Value> &args){
double value = args[0]->NumberValue();
args.GetReturnValue().Set(value);
下面是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);
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 = args.GetIsolate();
std::string value = std::string(*String::Utf8Value(isolate, args[0]));
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;
int f(int n) {
return (n < 3) ? 1 : f(n - 1) + f(n - 2);
void Fibonacci_Callback(const FunctionCallbackInfo<Value> &args) {
Isolate *isolate = args.GetIsolate();
if (args.Length() != 2) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments")));
return;
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);
Local<Function> cb = Local<Function>::Cast(args[1]);
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');
mylib.Fibonacci_Callback(10, (result) => {
console.log('f(10) =', result);
});
mylib.Fibonacci_Callback((result) => {
console.log('f(10) =', result);
});
第二个函数参数不正确,运行后会抛出异常
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++类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);
static void Add(const v8::FunctionCallbackInfo<v8::Value> &args);
static void AllMembers(const v8::FunctionCallbackInfo<v8::Value> &args);
explicit Clazz(std::string className);
~Clazz();
void _Add(std::string member);
std::string _AllMembers();
static v8::Persistent<v8::Function> constructor;
std::set<std::string> _members;
std::string _className;
#endif
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();
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());
node::AtExit([](void *) { printf("in node::AtExit\n"); }, nullptr);
void Clazz::New(const v8::FunctionCallbackInfo<v8::Value> &args) {
v8::Isolate *isolate = args.GetIsolate();
if (args.IsConstructCall()) {
std::string cName =
args[0]->IsUndefined() ? "Undefined" : std::string(*v8::String::Utf8Value(args[0]->ToString()));
Clazz *obj = new Clazz(cName);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
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);
v8::Local<v8::Object> result =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(result);
void Clazz::Add(const v8::FunctionCallbackInfo<v8::Value> &args) {
Clazz *obj = ObjectWrap::Unwrap<Clazz>(args.Holder());
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();
Clazz *obj = ObjectWrap::Unwrap<Clazz>(args.Holder());
std::string res = obj->_AllMembers();
args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, res.c_str()));
Clazz::Clazz(std::string className) : _className(className) {}
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");
clazz.Add('Tom');
clazz.Add('Mary');
clazz.Add('Liming');
console.log(clazz.AllMembers());
这个例子是根据官方文档的用法随便写的一个小demo,js实际调用的函数其实是C++类成员函数的包裹函数。官方文档介绍的使用就这些,烦人的一点就是各个node不同版本的API差异实在有些大。
本文主要描述了这几个方面的示例:
- 工程框架HelloWorld;
- 两种语言间不同类型怎么转换;
- 回调函数和异常处理;
- 如何包裹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