研究了大半天 Nodejs的内置库,越深入感觉自己跟不上进度。这篇我们稍微放轻松一些,暂时不讨论管道、调用、进程这些偏系统底层的东西,我们今天研究一下Node中如何调用C/C++语言程序。毕竟单纯用脚本语言想要实现高速的计算还是比较困难的,使用C语言插件可以直接使用很多已有的线性代数库,避免进入重复造轮的境地。
注意:这个话题包含很多概述性质的内容,由于本人很懒,因此很多后续用不到的内容只做提纲挈领的是串烧,不展开叙述,具体细节可以去网上找相关内容。
一、概述
目前Node.js环境下调用主要有以下三种方法:
1. Chrome V8 API
在Node.js起步阶段,实现C++库函数调用,需要直接调用V8内核API。
其代码典型特点如下:
#include <node.h>
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
....
这种方法与V8的版本紧密联系,由于V8引擎底层代码更新太快,很多代码随着V8版本升级直接无法编译,因此除了确实需要进行那些深入骨髓的操作,大概率不推荐这种方法。
2. nan
NAN是 Native Abstractions for Node.js的缩写,是Node.js 为了解决版本更新代码复用而给出的一个接口定义,用于只需要按照给定接口写代码,至于如何转化为版本对应的语句,由NAN来完成。这样用户就告别了直接对V8底层API的直接调用,避免新老版本代码不通用的问题。其代码基本都引用了nan.h这个头文件。
示例程序如下:
#include <nan.h>
void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
info.GetReturnValue().Set(Nan::New("world").ToLocalChecked());
}
void Init(v8::Local<v8::Object> exports) {
v8::Local<v8::Context> context =
exports->GetCreationContext().ToLocalChecked();
exports->Set(context,
Nan::New("hello").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Method)
->GetFunction(context)
.ToLocalChecked());
}
NODE_MODULE(hello, Init)
3.node-api
Node-API是Node.js 推出的用于开发 C++ 原生模块的接口,它把所有底层数据结构全部黑盒化,抽象成接口,不同版本的 Node.js 使用同样的接口,只要 ABI 的版本号一致,编译好的 C++ 扩展就可以直接使用,而不需要重新编译。node-api程序有以下特点:
-
提供头文件 node_api.h;
-
任何 N-API 调用都返回一个
napi_status
枚举,来表示这次调用成功与否; -
N-API 的返回值由于被
napi_status
占坑了,所以真实返回值由传入的参数来继承,如传入一个指针让函数操作; -
所有 JavaScript 数据类型都被黑盒类型
napi_value
封装,不再是类似于v8::Object
、v8::Number
等类型; -
如果函数调用不成功,可以通过
napi_get_last_error_info
函数来获取最后一次出错的信息。
示例程序如下:
#include <assert.h>
#include <node_api.h>
static napi_value Method(napi_env env, napi_callback_info info) {
napi_status status;
napi_value world;
status = napi_create_string_utf8(env, "world", 5, &world);
assert(status == napi_ok);
return world;
}
#define DECLARE_NAPI_METHOD(name, func) \
{ name, 0, func, 0, 0, 0, napi_default, 0 }
static napi_value Init(napi_env env, napi_value exports) {
napi_status status;
napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
status = napi_define_properties(env, exports, 1, &desc);
assert(status == napi_ok);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
4.node-addon-api
node-addon-api 是构建在ndoe-api 之上,提供了更加简单的 API,使得扩展开发者可以更加容易地编写跨版本、跨平台的扩展。
示例程序如下:
#include <napi.h>
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Method));
return exports;
}
啰嗦这么多,Ctrl+C 手都酸了,总的来说就一句话,目前主流是Node-Api和Node-Addon-Api,后者出现更晚一些。本着学技术新不学旧的观点,我们直接跳过选项 1 2 3 进入node-addon-api的学习。
二、node-gyp
node-gyp是node环境下专门用于编译C/C++文件的程序,是Node环境下构建C++插件的构建工具,和Cmake作用类似,node-gyp读取项目文件下面的名为binding.gyp 的文件,检查构建所需的工具是否齐全、构建环境设置是否到位,自动生成当前环境构建程序所需Makefile等文件。
1.安装
开始之前确保你已经具备了一定node.js基础知识,弄清了node npm npx 这些命令是什么,知道怎么用这些命令基本用法,能够实现前面几篇文章涉及的内容。否则请重新巩固一下基础知识。
此外,由于本人十分厌恶windows下的C开发环境(zhu),后面的实验均是在Linux环境下实现,为此你可能还需要掌握一些linux系统下C语言编程的知识。
使用以下命令
npm -g install node-gyp
其中 -g 代表安装到 node执行文件所在的环境(全局环境)中
2.测试
mkdir js2
cd js2
npm init
然后一路回车
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (js2)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /home/fan/work/js2/package.json:
{
"name": "js2",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
Is this OK? (yes) yes
然后可以看到文件夹下面多了一个 package.json文件
然后安装 node-addon-api
npm install node-addon-api
可以看到文件夹下面多了一个 node_modules 文件夹,node-addon-js就装在这下面
package.json 也多了一项
# package.json
"dependencies": {
"node-addon-api": "^8.3.0"
}
编写一个最简单的测试文件 hello.cc ,这些文件也可以从Github或 Gitee下载
/** hello.cc **/
#include <napi.h>
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Method));
return exports;
}
NODE_API_MODULE(hello, Init)
和binding.gyp文件
# binding.gyp
{
"targets": [
{
"target_name": "hello_addon", # name of the module
"sources": [ "hello.cc" ], # source files
"include_dirs": [ # include directories
"<!@(node -p \"require('node-addon-api').include\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], # disable C++ exceptions
'cflags!': [ '-fno-exceptions' ],# disable C++ exceptions
'cflags_cc!': [ '-fno-exceptions' ] # disable C++ exceptions
}
]
}
项目的含义我已经在备注里标明了,其中<!@ () 这是 node-gyp的特有的标识符,表明展开括号里语句的执行结果。感兴趣的也可以把括号里的内容复制到shell环境执行以下,看一下结果。
然后执行 配置构建程序
node-gyp configure
大概结果如下:
fan@fan-virtual-machine:~/work/js2$ node-gyp configure
gyp info it worked if it ends with ok
gyp info using node-gyp@11.0.0
gyp info using node@22.11.0 | linux | x64
gyp info find Python using Python version 3.10.12 found at "/usr/bin/python3"
gyp info spawn /usr/bin/python3
gyp info spawn args [
gyp info spawn args '/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args 'binding.gyp',
gyp info spawn args '-f',
gyp info spawn args 'make',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/work/js2/build/config.gypi',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp/addon.gypi',
gyp info spawn args '-I',
gyp info spawn args '/home/fan/.cache/node-gyp/22.11.0/include/node/common.gypi',
gyp info spawn args '-Dlibrary=shared_library',
gyp info spawn args '-Dvisibility=default',
gyp info spawn args '-Dnode_root_dir=/home/fan/.cache/node-gyp/22.11.0',
gyp info spawn args '-Dnode_gyp_dir=/home/fan/apps/node-v22.11.0-linux-x64/lib/node_modules/node-gyp',
gyp info spawn args '-Dnode_lib_file=/home/fan/.cache/node-gyp/22.11.0/<(target_arch)/node.lib',
gyp info spawn args '-Dmodule_root_dir=/home/fan/work/js2',
gyp info spawn args '-Dnode_engine=v8',
gyp info spawn args '--depth=.',
gyp info spawn args '--no-parallel',
gyp info spawn args '--generator-output',
gyp info spawn args 'build',
gyp info spawn args '-Goutput_dir=.'
gyp info spawn args ]
gyp info ok
这么一大坨,我不想看更看不懂,但是我认识 ok ,既然ok 那就可以进行下一步了。
需要注意的是文件夹这时又会多出一个 build 文件,我们构建所需要的文件都在里面。
fan@fan-virtual-machine:~/work/js2/build$ ls -la
total 52
drwxrwxr-x 2 fan fan 4096 12月 17 21:34 .
drwxrwxr-x 4 fan fan 4096 12月 17 21:34 ..
-rw-rw-r-- 1 fan fan 119 12月 17 21:34 binding.Makefile
-rw-rw-r-- 1 fan fan 16296 12月 17 21:34 config.gypi
-rw-rw-r-- 1 fan fan 4248 12月 17 21:34 hello_addon.target.mk
-rw-rw-r-- 1 fan fan 13736 12月 17 21:34 Makefile
那就构建吧, 在项目文件下 运行
fan@fan-virtual-machine:~/work/js2$ node-gyp build
gyp info it worked if it ends with ok
gyp info using node-gyp@11.0.0
gyp info using node@22.11.0 | linux | x64
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
make: Entering directory '/home/fan/work/js2/build'
CXX(target) Release/obj.target/hello_addon/hello.o
SOLINK_MODULE(target) Release/obj.target/hello_addon.node
COPY Release/hello_addon.node
make: Leaving directory '/home/fan/work/js2/build'
gyp info ok
又是一坨,还好最后还是ok 那就说明构建成功了。仔细可看信息提示,最后构建的模块再 build/Release这个文件夹里面,名称就是我们再binding.gyp文件里设置的那个名字。
我们建一个test.js文件来调用一下:
/** test.js **/
let hello_module = require('./build/Release/hello_addon.node');
// 使用import 会报ERR_UNKNOWN_FILE_EXTENSION 错误
console.log(hello_module.hello());
在shell运行一下:
fan@fan-virtual-machine:~/work/js2$ node test.js
world
可以看到输出了期望的字符串,说明模块调用成功。
注意这里直接使用 import 方法会报错。然而对于一些代码书写必须用最新的标准强迫症来说,怎么解决?
安装bingdings 模块
npm isntall bindings
test.js文件中引入该模块
/** test.js **/
//let hello_module = require('./build/Release/hello_addon.node');
// 使用import 会报ERR_UNKNOWN_FILE_EXTENSION 错误
import bindings from 'bindings'
let hello_module = bindings('hello_addon.node') //bingdings 会自动查找该模块可能存在的位置
console.log(hello_module.hello());
运行一下:
fan@fan-virtual-machine:~/work/js2$ node --trace-warnings test.js
world
结果一样
3.总结
写了那么多我们大概梳理一下 node-addon-api 封包使用的过程
首先是按照规范编写C程序,程序里面要标注导出项有哪些(具体实现后面再说)。
其次是编写binding.gyp文件,给出编译的选项
然后是通过node-gyp编译这些文件
具体使用时 需要通过 require 导入( 或着通过 bindings 模块辅助),使用时与普通函数没有差别。
后面我们将详细探讨 node-addon-api的一些使用。