node 是由 c 编写的,核心的 node 模块也都是由 c 代码来实现,所以同样 node 也开放了让使用者编写 c 扩展来实现一些操作的窗口。
如果大家对于 require 函数的描述还有印象的话,就会记得如果不写文件后缀,它是有一个特定的匹配规则的:
LOAD_AS_FILE(X)1. If X is a file, load X as its file extension format. STOP2. If X.js is a file, load X.js as javascript text. STOP3. If X.json is a file, parse X.json to a javascript object. STOP4. If X.node is a file, load X.node as binary addon. STOP
可以看到,最后会匹配一个 .node,而后边的描述也表示该后缀的文件为一个二进制的资源。
而这个 .node 文件一般就会是我们所编译好的 c 扩展了。
为什么要写 c 扩展
可以简单理解为,如果想基于 node 写一些代码,做一些事情,那么有这么几种选择:
1. 写一段 JS 代码,然后 require 执行
2. 写一段 c 代码,编译后 require 执行
3. 打开 node 源码,把你想要的代码写进去,然后重新编译
日常的开发其实只用第一项就够了,我们用自己熟悉的语言,写一段熟悉的代码,然后发布在 NPM 之类的平台上,其他有相同需求的人就可以下载我们上传的包,然后在TA的项目中使用。
但有的时候可能纯粹写 JS 满足不了我们的需求,也许是工期赶不上,也许是执行效率不让人满意,也有可能是语言限制。
所以我们会采用直接编写一些 c 代码,来创建一个 c 扩展让 node 来加载并执行。
况且如果已经有了 c 版本的轮子,我们通过扩展的方式来调用执行而不是自己从头实现一套,也是避免重复造轮子的方法。
一个简单的例子,如果大家接触过 webpack 并且用过 sass 的话,那么在安装的过程中很可能会遇到各种各样的报错问题,也许会看到 gyp 的关键字,其实原因就是 sass 内部有使用一些 c 扩展来辅助完成一些操作,而 gyp 就是用来编译 c 扩展的一种工具。
当然,上边也提到了还有第三种操作方法,我们可以直接魔改 node 源码,但是如果你只是想要写一些原生 JS 实现起来没有那么美好的模块,那么是没有必要去魔改源码的,毕竟改完了以后还要编译,如果其他人需要用你的逻辑,还需要安装你所编译好的特殊版本。
这样的操作时很不易于传播的,大家不会想使用 sass 就需要安装一个 sass 版本的 node 吧。
就像为了看星战还要专门下载一个优酷- -。
简单总结一下,写 c 的扩展大概有这么几个好处:
1. 可以复用 node 的模块管理机制
2. 有比 JS 更高效的执行效率
3. 有更多的 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" 来看效果 "
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": [ "
然后我们在项目根目录创建 package.json 文件,并安装 bindings 和 node-addon-api 两个依赖。
接下来就是去编写我们的 c 代码了:
#include
// 定义 Add 函数Napi::Value Add(const Napi::CallbackInfo& info) { Napi::Env env = info.Env();// 接收第一个参数 double arg0 = info[0].As<:number>().DoubleValue(); // 接收第二个参数 double arg1 = info[1].As<: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 所设置的值了。