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++ 扩展了。javascript
能够简单理解为,若是想基于 node 写一些代码,作一些事情,那么有这么几种选择:前端
平常的开发其实只用第一项就够了,咱们用本身熟悉的语言,写一段熟悉的代码,而后发布在 NPM 之类的平台上,其余有相同需求的人就能够下载咱们上传的包,而后在TA的项目中使用。
但有的时候可能纯粹写 JS 知足不了咱们的需求,也许是工期赶不上,也许是执行效率不让人满意,也有多是语言限制。
因此咱们会采用直接编写一些 c++ 代码,来建立一个 c++ 扩展让 node 来加载并执行。
何况若是已经有了 c++ 版本的轮子,咱们经过扩展的方式来调用执行而不是本身从头实现一套,也是避免重复造轮子的方法。 java
一个简单的例子,若是你们接触过 webpack 而且用过 sass 的话,那么在安装的过程当中极可能会遇到各类各样的报错问题,也许会看到 gyp 的关键字,其实缘由就是 sass 内部有使用一些 c++ 扩展来辅助完成一些操做,而 gyp 就是用来编译 c++ 扩展的一种工具。 node
https://github.com/sass/node-sasswebpack
固然,上边也提到了还有第三种操做方法,咱们能够直接魔改 node 源码,可是若是你只是想要写一些原生 JS 实现起来没有那么美好的模块,那么是没有必要去魔改源码的,毕竟改完了之后还要编译,若是其余人须要用你的逻辑,还须要安装你所编译好的特殊版本。
这样的操做时很不易于传播的,你们不会想使用 sass 就须要安装一个 sass 版本的 node 吧。
就像为了看星战还要专门下载一个优酷- -。 c++
简单总结一下,写 c++ 的扩展大概有这么几个好处:git
node 从问世到如今已经走过了 11 年,经过早期的资料、博客等各类信息渠道能够看到以前开发一个 c++ 扩展并非很容易,但通过了这么些年迭代,各类大佬们的努力,咱们再去编写一个 c++ 扩展已是比较轻松的事情了。
这里直入正题,放出今天比较关键的一个工具:node-addon-api module
以及这里是官方提供的各类简单 demo 来让你们熟悉这是一个什么样的工具: node-addon-examples github
须要注意的一点是, demo 目录下会分为三个子目录,在 readme 中也有写,分别是三种不一样的 c++ 扩展的写法(基于不一样的工具)。
咱们本次介绍的是在 node-addon-api
目录下的,算是三种里边最为易用的一种了。 web
首先是咱们比较熟悉的 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...
咱们会用到的一些技术点:
再也不赘述 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来实现的话会很是简单,配合 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++ 来帮助咱们解决问题。
不要让编程语言限制了你的想象力