无需 node-gyp 的 Node.js 原生插件

I18N: English | 简体中文

目录

为什么

显然,我们需要为一些计算密集型的任务绑定原生代码。当前,NAPI (node-api) + node-gyp 这个组合是为 Node.js 构建原生插件的最流行方式。

但由于历史原因,使用 node-gyp 会让人感到沮丧。node-gyp 的一些缺点包括:它很容易导致 难以解决的问题。而且,node-gyp 所基于的 gyp 较为不透明,很少被普通 C / C++ 开发人员使用。因此,为什么不使用我们常用的通用构建工具来替代 node-gyp?

手动构建一个插件

git clone https://github.com/kkocdko/hinapi

NAPI 是一组 ABI stable 的函数,导入 node_api.h 是最重要的步骤。这里我们还要使用 node-addon-api (c++),当然,你可以只使用 C。

最简单的途径是,假设你已经安装了 CMake(别跑!我们很快就会提到没有 cmake 的方法):

# 在目录 ./hinapi
cmake -B build
cmake --build build
node src/main.js
du third_party -sh

输出:

[kkocdko@klf hinapi]$ cmake -B build
-- The C compiler identification is GNU 12.2.1
...
added 2 packages in 624ms
...
-- Build files have been written to: /home/kkocdko/misc/code/hinapi/build

[kkocdko@klf hinapi]$ cmake --build build
[ 50%] Building CXX object CMakeFiles/hinapi.dir/src/hinapi.cc.o
[100%] Linking CXX shared library hinapi.node
[100%] Built target hinapi

[kkocdko@klf hinapi]$ node src/main.js
calc 1 + 2 = 3
created object = { name: 'tom', age: 'tom' }
callback argument = hello world
promise resolved value = 1.2

[kkocdko@klf hinapi]$ du third_party -sh
508K    third_party

模板 repo 中的 CMakeLists.txt 是“刚好够用”的,支持 MSVC、GCC、Clang,在 Linux、Windows(MSVC 和 MinGW)、macOS 上运行。参见 该 repo 的 GitHub Actions

如果你不想要 CMake,我可以告诉你 CMakeLists.txt 具体做了什么:

  1. 下载 node-api-headers(当前它提取自 Node.js 19)和 node-addon-api./third_party 目录。

  2. 我们的目标是编译出一个名为 hinapi.node 的动态库。在 Linux 上,运行 g++ src/hinapi.cc -o build/hinapi.node -I third_party/node-addon-api -I third_party/node-api-headers/include -shared -fPIC

  3. 然而,Windows 要求 我们提供一个 DLL 来确定链接时的所有符号。所以我们需要 一个 .def 文件,它定义了导出的 NAPI 函数,并使用 lib (MSVC) / dlltool (MinGW) 来创建 libnode.lib(MSVC)/ libnode.a(MinGW),然后链接静态库。值得庆幸的是,这在 macOS 上要容易得多,只需添加 -undefined dynamic_lookup 就可以了。

优点:1. 轻量级,依赖体积小于 0.6 MiB。2. 透明,可以完全控制的编译过程。

完成啦!现在,试试运行 node src/main.js

你真的需要 NAPI 吗?

让我们回到开头:

显然,我们需要为一些计算密集型的任务绑定原生代码。

真的是这样吗?并非总是如此。对于某些工作负载,你可以尝试以下方法,为你的 Node.js 应用带来原生速度。

C FFI

不需要 async、Promise 和复杂对象这样的高级功能?尝试调用 dlopen(),并直接执行动态库的导出函数。

这个函数在 BunDeno 中也有实现,这意味着你将能够在这些运行时上运行你的应用程序,为适配问题少操点心。

通过 Stdio

Effective Go 中有一句 格言

不要通过共享内存来通信;相反,通过通信来共享内存。

ESBuild 是一个用于 Web 技术栈的打包器,它为我们完美地解释了这一点!它 使用 stdio 在 ESBuild 进程和 Node.js 进程之间通信。不仅如此,许多著名的项目,例如 LSP,都在使用这个方法。

你可能对它的性能有所疑虑,然而,在很多情况下,stdio 的瓶颈是终端模拟器,而不是 shell 和程序本身。在我的机器上传输相同数据时,stdio 使用了 3 倍于 memcpy 的时间。

优点是更简单的概念和更好的兼容性(更多语言的调用支持),是的,stdio 是最常见和最原始的 IPC 方法之一。

但缺点也是不容忽视的。你需要对两端的数据进行处理和包装,额外的数据处理,导致更大的延迟和更多的内存占用。

结束

希望这篇文章能让你在 Node.js 应用中需要原生性能时有更多选择。感谢上面提到的每个项目 / 链接,谢谢~