nodeJS 调用 C 语言
How to call C function from nodeJS
最近在 node 项目开发中,有个需求是 nodeJS 需要支持调用 C 语言的函数,node-addon-api 可以支持这个需求。
1 开发环境
我用的开发环境 docker 起的 code-server 环境,code-server 版本为 code-server:version-v3.11.1 。可以把 code-server 理解成一个在线 vscode 环境,就像 github 的在线 web 编辑器一样。
docker pull linuxserver/code-server:version-v3.11.1
2 加法器示例
开发环境搭建成功后,可以实现一个小功能,以熟悉 node-addon-api 的使用。
现在实现一个加法器,JS 调用 C 语言的 add 方法,传入 2 个参数,C 语言累加后返回结果。
2.1 项目初始化
创建项目并进行 npm init
初始化:
安装 node-addon-api:
npm i node-addon-api
2.2 c 代码
新建一个 cal.cc 文件,内容如下:
#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)
2.3 binding.gyp
编译带第三方扩展库的 c/c++ 程序,通常需要在编译时指定额外的头文件包含路径和链接第三方库,这些都是在 binding.gyp 文件中指定的,这些指定在 nodeJS 自动编译的时候,会解析并应用在命令行的编译工具中。
新建一个 binding.gyp 文件,内容如下:
{
"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中编译使用,有这三行就够了。
2.4 编译调用
每次修改代码后都需要执行
npm i
重新编译
npm i
编译后,进入 nodeJS 中可以直接 require 调用。
这里 require 的 test.node
,.node
后缀是固定的,test
就是 binding.gyp 文件里 target_name
的值。
1+3=4
从调用结果来看,符合预期。
2.5 bindings 包
现在我们 require 编译后的 node 需要这样写:
require('./build/Release/nodecamera.node');
可以用 bindings
包简化 require 。
npm i bindings --save
通估👆命令安装 bindings
包。
所以以上示例简化后的 require 为:
const addon = require('bindings')('test.node');
3 常见数据类型转换
JS 与 C 的数据类型有较大差别,比如 C 中没有字符串的概念,只有字节数组等。node-addon-api 可以很好的支持 JS 与 C 数据类型的转换。
3.1 字符串
std::string temp = info[0].As<Napi::String>().ToString();
3.2 ArrayBuffer
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;
}
3.3 数组
JS 将数组作为 C 函数参数。
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);
}
}
}
编译时可能有 warning,但是不影响。
4 FAQ
4.1 持久化函数
这个功能可以理解成在 C 的内存空间中有一个 JS 的函数对象且在生命周期内不会被 C 垃圾回收,可以直接在 C 中调用这个 JS 函数。
以下示例,C 提供了 debug 函数,但是参数是一个函数,这个函数会持久在 C 的内存中,在 C 的 Str 函数中用 Call 来调用这个函数并传入对应的参数。
js-call-c-demo.js
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
#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)