node 是由 c++ 编写的,外围的 node 模块也都是由 c++ 代码来实现,所以同样 node 也凋谢了让使用者编写 c++ 扩大来实现一些操作的窗口。
如果大家对于 require 函数的形容还有印象的话,就会记得如果不写文件后缀,它是有一个特定的匹配规定的:
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
能够看到,最初会匹配一个 .node
,而后边的形容也示意该后缀的文件为一个二进制的资源。
而这个 .node
文件个别就会是咱们所编译好的 c++ 扩大了。
为什么要写 c++ 扩大
能够简略了解为,如果想基于 node 写一些代码,做一些事件,那么有这么几种抉择:
- 写一段 JS 代码,而后 require 执行
- 写一段 c++ 代码,编译后 require 执行
- 关上 node 源码,把你想要的代码写进去,而后从新编译
日常的开发其实只用第一项就够了,咱们用本人相熟的语言,写一段相熟的代码,而后公布在 NPM 之类的平台上,其余有雷同需要的人就能够下载咱们上传的包,而后在 TA 的我的项目中应用。
但有的时候可能纯正写 JS 满足不了咱们的需要,兴许是工期赶不上,兴许是执行效率不让人称心,也有可能是语言限度。
所以咱们会采纳间接编写一些 c++ 代码,来创立一个 c++ 扩大让 node 来加载并执行。
况且如果曾经有了 c++ 版本的轮子,咱们通过扩大的形式来调用执行而不是本人从头实现一套,也是防止反复造轮子的办法。
一个简略的例子,如果大家接触过 webpack 并且用过 sass 的话,那么在装置的过程中很可能会遇到各种各样的报错问题,兴许会看到 gyp 的关键字,其实起因就是 sass 外部有应用一些 c++ 扩大来辅助实现一些操作,而 gyp 就是用来编译 c++ 扩大的一种工具。
https://github.com/sass/node-sass
当然,上边也提到了还有第三种操作方法,咱们能够间接魔改 node 源码,然而如果你只是想要写一些原生 JS 实现起来没有那么美妙的模块,那么是没有必要去魔改源码的,毕竟改完了当前还要编译,如果其他人须要用你的逻辑,还须要装置你所编译好的非凡版本。
这样的操作时很不易于流传的,大家不会想应用 sass 就须要装置一个 sass 版本的 node 吧。
就像为了看星战还要专门下载一个优酷 - -。
简略总结一下,写 c++ 的扩大大略有这么几个益处:
- 能够复用 node 的模块管理机制
- 有比 JS 更高效的执行效率
- 有更多的 c++ 版本的轮子能够拿来用
怎么去写一个简略的扩大
node 从问世到当初曾经走过了 11 年,通过晚期的材料、博客等各种信息渠道能够看到之前开发一个 c++ 扩大并不是很容易,但通过了这么些年迭代,各种大佬们的致力,咱们再去编写一个 c++ 扩大曾经是比拟轻松的事件了。
这里直入正题,放出明天比拟要害的一个工具:node-addon-api module
以及这里是官网提供的各种简略 demo 来让大家相熟这是一个什么样的工具:node-addon-examples
须要留神的一点是,demo 目录下会分为三个子目录,在 readme 中也有写,别离是三种不同的 c++ 扩大的写法(基于不同的工具)。
咱们本次介绍的是在 node-addon-api
目录下的,算是三种里边最为易用的一种了。
首先是咱们比拟相熟的 package.json
文件,咱们须要依赖两个组件来实现开发,别离是 bindings 和 node-addon-api。
而后咱们还须要简略理解一下 gyp 的用法,因为编译一个 c++ 扩大须要用到它。
就像 helloworld 示例中的 binding.gyp 文件示例:
{
"targets": [
{
// 导出的文件名
"target_name": "hello",
// 编译标识的定义 禁用异样机制(留神感叹号示意排除过滤)"cflags!": ["-fno-exceptions"],
// c++ 编译标识的定义 禁用异样机制(留神感叹号示意排除过滤,也就是 c++ 编译器会去除该标识)"cflags_cc!": ["-fno-exceptions"],
// 源码入口文件
"sources": ["hello.cc"],
// 源码蕴含的目录
"include_dirs": [// 这里示意一段 shell 的运行,用来获取 node-addon-api 的一些参数,有趣味的老铁能够自行 node -p "require('node-addon-api').include" 来看成果
"<!@(node -p \"require('node-addon-api').include\")"
],
// 环境变量的定义
'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
}
]
}
gyp 的语法挺多的,这次并不是独自针对 gyp 的一次记录,所以就不过多的介绍。
从最简略的数字相加来实现
而后咱们来实现一个简略的创立一个函数,让两个参数相加,并返回后果。
源码地位:https://github.com/Jiasm/node…
咱们须要这样的一个 binding.gyp 文件:
{
"targets": [
{
"target_name": "add",
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"sources": ["add.cc"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"
],
'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
}
]
}
而后咱们在我的项目根目录创立 package.json 文件,并装置 bindings 和 node-addon-api 两个依赖。
接下来就是去编写咱们的 c++ 代码了:
#include <napi.h>
// 定义 Add 函数
Napi::Value Add(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();
// 接管第一个参数
double arg0 = info[0].As<Napi::Number>().DoubleValue();
// 接管第二个参数
double arg1 = info[1].As<Napi::Number>().DoubleValue();
// 将两个参数相加并返回
Napi::Number num = Napi::Number::New(env, arg0 + arg1);
return num;
}
// 入口函数,用于注册咱们的函数、对象等等
Napi::Object Init(Napi::Env env, Napi::Object exports) {
// 将一个名为 add 的函数挂载到 exports 上
exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add));
return exports;
}
// 固定的宏应用
NODE_API_MODULE(addon, Init)
在 c++ 代码实现当前就是须要用到 node-gyp
的时候了,倡议全局装置 node-gyp
,防止一个我的项目中呈现多个 node_modules 目录的时候应用 npx
会呈现一些不可意料的问题:
> npm i -g node-gyp
# 生成构建文件
> node-gyp configure
# 构建
> node-gyp build
这时候你会发现我的项目目录下曾经生成了一个名为 add.node 的文件,就是咱们在 binding.gyp 里边的 target_name 所设置的值了。
最初咱们就是要写一段 JS 代码来调用所生成的 .node 文件了:
const {add} = require('bindings')('add.node')
console.log(add(1, 2)) // 3
console.log(add(0.1, 0.2)) // 相熟的 0.3XXXXX
实现一个函数柯里化
接下来咱们来整点好玩的,实现一个前端的高频考题,如何实现一个函数柯里化,定义如下:
add(1)(2)(3) // => 6
add(1, 2, 3) // => 6
源码地位:https://github.com/Jiasm/node…
咱们会用到的一些技术点:
- 如何在 c++ 函数中返回一个函数供 JS 调用
- 如何让返回值既反对函数调用又反对取值操作
- 如何解决非固定数量的参数(其实这个很简略了,从上边也能看进去,自身就是一个数组)
不再赘述 binding.gyp 与 package.json 的配置,咱们间接上 c++ 代码:
#include <napi.h>
// 用来笼罩 valueOf 实现的函数
Napi::Value GetValue(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();
// 获取咱们在创立 valueOf 函数的时候传入的 result
double* storageData = reinterpret_cast<double*>(info.Data());
// 防止空指针状况
if (storageData == NULL) {return Napi::Number::New(env, 0);
} else {return Napi::Number::New(env, *storageData);
}
}
Napi::Function CurryAdd(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();
// 获取咱们下边在创立 curryAdd 函数的时候传入的 result
double* storageData = reinterpret_cast<double*>(info.Data());
double* result = new double;
// 遍历传入的所有参数
long len, index;
for (len = info.Length(), index = 0; index < len; index++) {double arg = info[index].As<Napi::Number>().DoubleValue();
*result += arg;
}
// 用于屡次的计算
if (storageData != NULL) {*result += *storageData;}
// 创立一个新的函数用于函数的返回值
Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd", result);
// 篡改 valueOf 办法,用于输入后果
fn.Set("valueOf", Napi::Function::New(env, GetValue, "valueOf", result));
return fn;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd");
exports.Set(Napi::String::New(env, "curryAdd"), fn);
return exports;
}
NODE_API_MODULE(curryadd, Init)
编译实现当前,再写一段简略的 JS 代码来调用验证后果即可:
const {curryAdd} = require('bindings')('curry-add');
const fn = curryAdd(1, 2, 3);
const fn2 = fn(4);
console.log(fn.valueOf()) // => 6
console.log(fn2.valueOf()) // => 10
console.log(fn2(5).valueOf()) // => 15
而后能够讲一下上边列出来的三个技术点是如何解决的:
-
如何在 c++ 函数中返回一个函数供 JS 调用
- 通过
Napi::Function::New
创立新的函数,并将计算结果存入函数能够获取到的中央供下次应用
- 通过
-
如何让返回值既反对函数调用又反对取值操作
- 通过
fn.Set
篡改valueOf
函数并返回后果
- 通过
-
如何解决非固定数量的参数(其实这个很简略了,从上边也能看进去,自身就是一个数组)
- 通过拿到
info
的Length
来遍历获取
- 通过拿到
与 JS 进行比照
当然,就例如柯里化之类的函数,拿 JS 来实现的话会非常简单,配合 reduce 函数基本上五行以内就能够写进去。
那咱们折腾这么多到底是为了什么呢?
这就要回到结尾所说的劣势了:执行效率
采纳冒泡排序来比照
为了证实效率的差别,咱们抉择用一个排序算法来验证,采纳了最简略易懂的冒泡排序来做,首先是 JS 版本的:
源码地位:https://github.com/Jiasm/node…
function bubble (arr) {for (let i = 0, len = arr.length; i < len; i++) {for (let j = i + 1; j < len; j++) {if (arr[i] < arr[j]) {[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
}
return arr
}
bubble([7, 2, 1, 5, 3, 4])
而后是咱们的 c++ 版本,因为是一个 JS 的扩大,所以会波及到数据类型转换的问题,大抵代码如下:
#include <napi.h>
void bubbleSort(double* arr, int len) {
double temp;
int i, j;
for (i = 0; i < len; i++) {for (j = i + 1; j < len; j++) {if (*(arr + i) < *(arr + j)) {temp = *(arr + i);
*(arr + i) = *(arr + j);
*(arr + j) = temp;
}
}
}
}
Napi::Value Add(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();
Napi::Array array = info[0].As<Napi::Array>();
int len = array.Length(), i;
// 返回值
Napi::Array arr = Napi::Array::New(env, len);
double* list = new double[len];
// 将 Array 转换为 c++ 可方便使用的 double 数组
for (i = 0; i < len; i++) {Napi::Value i_v = array[i];
list[i] = i_v.ToNumber().DoubleValue();
}
// 执行排序
bubbleSort(list, len);
// 将 double 数组转换为要传递给 JS 的数据类型
for (i = 0; i < len; i++) {arr[i] = Napi::Number::New(env, list[i]);
}
return arr;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {exports.Set(Napi::String::New(env, "bubble"), Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(bubble, Init)
而后咱们通过一个随机生成的数组来比照耗时:
const {bubble} = require('bindings')('bubble.node')
const arr = Array.from(new Array(1e3), () => Math.random() * 1e6 | 0)
console.time('c++')
const a = bubble(arr)
console.timeEnd('c++')
function bubbleJS (arr) {for (let i = 0, len = arr.length; i < len; i++) {for (let j = i + 1; j < len; j++) {if (arr[i] < arr[j]) {[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
}
return arr
}
console.time('js')
bubbleJS(arr)
console.timeEnd('js')
在 1,000
数据量的时候耗时差距大略在 6
倍左右,在 10,000
数据量的时候耗时差距大略在 3
倍左右。
也是简略的证实了在雷同算法状况下 c++ 效率的确是会比 JS 高一些。
当然了,也通过上边的 bubble sort 能够来证实另一个观点:有更多的 c++ 版本的轮子能够拿来用
就比方上边的 bubbleSort
函数,可能就是一个其余的加密算法实现、SDK 封装,如果没有 node 版本,而咱们要应用就须要参考它的逻辑从新实现一遍,但如果采纳 c++ 扩大的形式,齐全能够基于原有的 c++ 函数进行一次简略的封装就领有了一个 node 版本的 函数 /SDK。
后记
上边的一些内容就是如何应用 node-addon-api
来疾速开发一个 c++ 扩大,以及如何应用 node-gyp
进行编译,还有最初的如何应用 JS 调用 c++ 扩大。
在开发 node 程序的过程中,如果可能适当的利用 c++ 的能力是会对我的项目有很大的帮忙的,在一些比拟要害的中央,亦或者 node 弱项的中央,应用更尖锐的 c++ 来帮忙咱们解决问题。
不要让编程语言限度了你的想象力
参考资料
- node-gyp
- node-addon-api | Addon
- node-addon-api | CallbackInfo
- node-addon-api | Function
- node-addon-api | Object
- node-addon-api | Array
- node-addon-api | Number