nodeJS 调用 C 语言

How to call C function from nodeJS

最近在 node 项目开发中,有个需求是 nodeJS 需要支持调用 C 语言的函数,node-addon-api 可以支持这个需求。

我用的开发环境 docker 起的 code-server 环境,code-server 版本为 code-server:version-v3.11.1 。可以把 code-server 理解成一个在线 vscode 环境,就像 github 的在线 web 编辑器一样。

1
docker pull linuxserver/code-server:version-v3.11.1

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/9a6234c147f34cfe86d67b72902aa1cf.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
code-server

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/1452c3a995af4dd480963de599766c73.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
github web 编辑器

开发环境搭建成功后,可以实现一个小功能,以熟悉 node-addon-api 的使用。

现在实现一个加法器,JS 调用 C 语言的 add 方法,传入 2 个参数,C 语言累加后返回结果。

创建项目并进行 npm init 初始化:

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/f3f108e8c5664b1390766008751decb0.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
创建项目并初始化

安装 node-addon-api:

1
npm i node-addon-api

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/eecca630b11b49bf840351bd5c6dfe06.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
安装 npm 依赖包

新建一个 cal.cc 文件,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <napi.h>

// 定义一个 Add() 方法
Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); // 获取 js 上下文信息

  if (info.Length() < 2) {
    Napi::TypeError::New(env, "Wrong number of arguments")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  if (!info[0].IsNumber() || !info[1].IsNumber()) {
    Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
    return env.Null();
  }

  int arg0 = info[0].As<Napi::Number>().Int32Value();
  int arg1 = info[1].As<Napi::Number>().Int32Value();

  int arg2 = arg0 + arg1;
  
  Napi::Number num = Napi::Number::New(env, arg2);

  return num;
}

// 导出函数,可使用 exports.Set() 导出多个函数
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add));
  return exports;
}

NODE_API_MODULE(addon, Init)

编译带第三方扩展库的 c/c++ 程序,通常需要在编译时指定额外的头文件包含路径和链接第三方库,这些都是在 binding.gyp 文件中指定的,这些指定在 nodeJS 自动编译的时候,会解析并应用在命令行的编译工具中。

新建一个 binding.gyp 文件,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "targets": [
    {
      "target_name": "test",
      "sources": [
        "cal.cc"
      ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "libraries": [
      ],
      "dependencies": [
        "<!(node -p \"require('node-addon-api').gyp\")"
      ],
      "cflags!": [
        "-fno-exceptions"
      ],
      "cflags_cc!": [
        "-fno-exceptions"
      ],
      "defines": [
        "NAPI_CPP_EXCEPTIONS"
      ],
      "xcode_settings": {
        "GCC_ENABLE_CPP_EXCEPTIONS": "YES"
      }
    }
  ]
}
  • target_name 指定了编译之后模块的名称。
  • sources 指明 c/c++ 的源文件,如果有多个文件,需要用逗号隔开,放到同一个数组中。
  • include_dirs 是编译时使用的头文件引入路径,这里使用 node -p 执行 node-addon-api 模块中的预置变量。
  • dependencies 是必须的,一般不要改变。
  • cflags!cflags_cc!defines 三行指定如果c++程序碰到意外错误的时候,由 NAPI 接口来处理,而不是通常的由 c/c++ 程序自己处理。这防止因为 c/c++ 部分程序碰到意外直接就退出了程序,而是由 nodeJS 程序来捕获处理,如果是在Linux中编译使用,有这三行就够了。

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/04a96bb9ac254b30a65ffa2939134d05.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
编译

每次修改代码后都需要执行 npm i 重新编译

1
npm i 

编译后,进入 nodeJS 中可以直接 require 调用。

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/0eac8fccb7334cb5bbde15a5bd25c573.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
调用

这里 require 的 test.node.node 后缀是固定的,test 就是 binding.gyp 文件里 target_name 的值。

1+3=4 从调用结果来看,符合预期。

现在我们 require 编译后的 node 需要这样写:

1
require('./build/Release/nodecamera.node');

可以用 bindings 包简化 require 。

1
npm i bindings --save

通估👆命令安装 bindings 包。

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/518e631de25144c0a7558b36a50c4375.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
bindings 包使用

所以以上示例简化后的 require 为:

1
const addon = require('bindings')('test.node');

JS 与 C 的数据类型有较大差别,比如 C 中没有字符串的概念,只有字节数组等。node-addon-api 可以很好的支持 JS 与 C 数据类型的转换。

1
std::string temp = info[0].As<Napi::String>().ToString();

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/bd714b564556421b83480d47b7c00c9e.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
字符串转换示例

1
2
3
4
5
6
Napi::ArrayBuffer ABuffer(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  int8_t num[4] = {14,25,45,88};
  Napi::ArrayBuffer x = Napi::ArrayBuffer::New(env,num,4);
  return x;
}

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/d6d3e0a559f14e7db556152c7e165a33.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
ArrayBuffer 示例

JS 将数组作为 C 函数参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Napi::Value ArrayArg(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  Napi::Array b = info[0].As<Napi::Array>();
  for (int i = 0; i < b.Length(); i++)
  {
      Napi::Value v = b[i];
      if (v.IsString()){
          std::string value = (std::string)v.As<Napi::String>();
          return Napi::String::New(env,value);
      }
  }
}

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/a5e1c3db4491444394f51cbee383df45.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
编译可能有 warning

编译时可能有 warning,但是不影响。

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/3a4b15094092464b8608c0d3f8bdda85.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
数组参数

这个功能可以理解成在 C 的内存空间中有一个 JS 的函数对象且在生命周期内不会被 C 垃圾回收,可以直接在 C 中调用这个 JS 函数。

以下示例,C 提供了 debug 函数,但是参数是一个函数,这个函数会持久在 C 的内存中,在 C 的 Str 函数中用 Call 来调用这个函数并传入对应的参数。

js-call-c-demo.js

1
2
3
4
5
6
7
8
9
const addon = require('bindings')('test.node');

// 调用 c 中的 debug 函数,将函数注入到 c 中
addon.debug(msg => {
    console.log("debug console, c 中传入的 msg 需要打印的参数值为:", msg)
})

// 调用 c 的 str 函数,在 str 函数中会调用 debug 函数中的 console.log()
console.log("str 函数的返回值为: ", addon.str("xiaobinqt"))

cal.cc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <napi.h>


Napi::FunctionReference Debug;
napi_env DebugEnv;


Napi::Value DebugFun(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  Debug = Napi::Persistent(info[0].As<Napi::Function>());
  DebugEnv = env; 
  return Napi::String::New(env,"OK");
}


Napi::Value Str(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  std::string temp = info[0].As<Napi::String>().ToString();
  Napi::String s = Napi::String::New(env, temp);
  // 调用 Debug 函数
  Debug.Call({Napi::String::New(DebugEnv,"我是一个测试 debug")});
  return s;
}


// 定义一个 Add() 方法
Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); // 获取 js 上下文信息

  if (info.Length() < 2) {
    Napi::TypeError::New(env, "Wrong number of arguments")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  if (!info[0].IsNumber() || !info[1].IsNumber()) {
    Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
    return env.Null();
  }

  int arg0 = info[0].As<Napi::Number>().Int32Value();
  int arg1 = info[1].As<Napi::Number>().Int32Value();

  int arg2 = arg0 + arg1;
  
  Napi::Number num = Napi::Number::New(env, arg2);

  return num;
}



Napi::ArrayBuffer ABuffer(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  int8_t num[4] = {14,25,45,88};
  Napi::ArrayBuffer x = Napi::ArrayBuffer::New(env,num,4);
  return x;
}


Napi::Value ArrayArg(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env(); 
  Napi::Array b = info[0].As<Napi::Array>();
  for (int i = 0; i < b.Length(); i++)
  {
      Napi::Value v = b[i];
      if (v.IsString()){
          std::string value = (std::string)v.As<Napi::String>();
          return Napi::String::New(env,value);
      }
  }
}



// 导出函数,可使用 exports.Set() 导出多个函数
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add));
  exports.Set(Napi::String::New(env, "str"), Napi::Function::New(env, Str));
  exports.Set(Napi::String::New(env, "ab"), Napi::Function::New(env, ABuffer));
  exports.Set(Napi::String::New(env, "arr"), Napi::Function::New(env, ArrayArg));
  exports.Set(Napi::String::New(env, "debug"), Napi::Function::New(env, DebugFun));
  return exports;
}

NODE_API_MODULE(addon, Init)

https://cdn.xiaobinqt.cn/xiaobinqt.io/20220412/0329f648225d4aac9b0201a4fe8a1589.png?imageView2/0/q/75|watermark/2/text/eGlhb2JpbnF0/font/dmlqYXlh/fontsize/1000/fill/IzVDNUI1Qg==/dissolve/52/gravity/SouthEast/dx/15/dy/15
测试结果