NativeAddons

https://nodejs.org/docs/latest/api/addons.html

https://nodejs.org/docs/latest/api/n-api.html

introduce

NOTE

附加组件(Addons)是用 C++ 编写的动态链接共享对象。require() 函数可以将附加组件作为普通的 Node.js 模块加载。附加组件提供了 JavaScript 和 C/C++ 库之间的接口。

实现附加组件有三种选择:

  • Node-API
  • nan(Node.js 的原生抽象)
  • 直接使用内部的 V8、libuv 和 Node.js 库

除非需要直接访问 Node-API 未公开的功能,否则请使用 Node-API。

当不使用 Node-API 时,实现附加组件会变得更加复杂,需要了解多个组件和 API:

  • V8:Node.js 用于提供 JavaScript 实现的 C++ 库。它提供了创建对象、调用函数等机制。V8 的 API 主要记录在 v8.h 头文件(Node.js 源代码树中的 deps/v8/include/v8.h)中,也可在网上获取。
  • libuv:实现 Node.js 事件循环、工作线程以及平台所有异步行为的 C 库。它还充当跨平台抽象库,使在所有主要操作系统上都能轻松地、类 POSIX 地访问许多常见系统任务,例如与文件系统、套接字、计时器和系统事件进行交互。libuv 还提供了类似于 POSIX 线程的线程抽象,以便更复杂的异步附加组件可以超越标准事件循环。附加组件作者应避免通过 I/O 或其他耗时任务阻塞事件循环,可通过 libuv 将工作卸载到非阻塞系统操作、工作线程或自定义使用 libuv 线程来实现。
  • 内部 Node.js 库:Node.js 本身导出附加组件可以使用的 C++ API,其中最重要的是 node::ObjectWrap 类。
  • 其他静态链接库(包括 OpenSSL):这些其他库位于 Node.js 源代码树中的 deps/ 目录中。只有 libuv、OpenSSL、V8 和 zlib 符号是 Node.js 特意重新导出的,附加组件可以在不同程度上使用它们。
// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world", NewStringType::kNormal).ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo
  • 注意事项

    TIP

    所有 Node.js 扩展都必须按照以下模式导出一个初始化函数:

    void Initialize(Local<Object> exports);
    NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

    NODE_MODULE 后面没有分号,因为它不是一个函数(参见 node.h)。

    module_name 必须与最终二进制文件的文件名(不包括 .node 后缀)匹配。

    hello.cc 示例中,初始化函数是 Initialize,扩展模块名称是 addon

    当使用 node-gyp 构建扩展时,将宏 NODE_GYP_MODULE_NAME 用作 NODE_MODULE() 的第一个参数,将确保最终二进制文件的名称会被传递给 NODE_MODULE()

    NODE_MODULE() 定义的扩展不能同时在多个上下文或多个线程中加载。

    INFO
    • 首先,关于那段 C 代码的意思: Node.js 允许用 C/C++ 写扩展(相当于给 Node.js 加自定义功能),但必须遵守固定的“格式要求”:
    1. 必须写一个叫 Initialize 的函数(名字可以改,但通常约定用这个),它的作用是把 C/C++ 里的功能(比如函数、变量)暴露给 Node.js 调用(通过 exports 对象,类似 Node.js 里的 module.exports)。
    2. 必须用 NODE_MODULE(...) 这个宏来“注册”这个扩展,告诉 Node.js:“这是我的扩展,入口是 Initialize 函数”。
    • 注意它后面没有分号,因为它不是普通函数调用,而是 Node.js 定义的一个“宏指令”(可以理解为一段预定义的代码片段)。
    • 扩展的名字(比如示例里的 addon)必须和最终编译出来的二进制文件名字一致(比如编译后叫 addon.node,那名字就是 addon)。
    • 然后,node-gyp 是什么? 简单说,node-gyp 是一个“编译工具”,专门用来把 C/C++ 代码编译成 Node.js 能识别的扩展(也就是 .node 后缀的二进制文件)。
    • 为什么需要它?
      因为 Node.js 扩展本质是 C/C++ 程序,但需要适配 Node.js 的内部机制(比如 V8 引擎的接口),直接用普通的 C 编译器(如 gcc、MSVC)太麻烦。node-gyp 会帮你处理这些复杂的适配工作:
    • 自动生成编译所需的配置文件(比如 Makefile 或 Visual Studio 项目)。
    • 确保编译时使用正确的 Node.js 头文件和库文件。
    • 最终输出 .node 文件,让 Node.js 可以直接 require 加载。
    • 总结一下流程:
    1. 你用 C/C++ 写扩展代码,按照规定的格式定义 Initialize 函数和 NODE_MODULE 注册。
    2. node-gyp 配置编译参数,执行编译命令。
    3. node-gyp 生成 .node 二进制文件。
    4. 在 Node.js 代码里用 require('./addon.node') 加载,就能调用 C/C++ 实现的功能了。

    这样做的好处是:让 Node.js 能利用 C/C++ 的高性能(比如处理密集计算),同时保持 JavaScript 的易用性。

搭建 node-gyp 开发环境

NOTE

首先声明,我只用 pnpm 但是其他的一样的呐,可以自己查阅资料吧

  • pnpm install node-gyp node-addon-api --save-dev

    • node-gyp 是本地的核心编译工具

    • node-addon-api 简化的 C++ 接口吧,方便于寻找 node.h 的接口的呢

    TIP
    • 进行安装后在 node_module 下就会出现一个 node-gyp 的包,然后出现一个可执行文件 .bin/node-gyp

      • 只要是nodejs 中的可执行文件的话都是将可执行文件存储在 .bin 下的呐
    • 准备 C 编译环境和 python3环境进行操作

      • xcode-select --install 验证 xcode-select 是否进行了安装了的

      • brew install python3 对于liunx和macbook自带的python版本都是python2.7的,但是为了动态链接的友好性,所以尽可能使用 python3 版本吧

  • 编写配置文件

    INFO
    • 对于附加组件的编译工具node-gyp的是会进行识别你的配置文件的:binding.gyp
    Usage: node-gyp <command> [options] where <command> is one of: - build - Invokes `make` and builds the module - clean - Removes any generated build files and the "out" dir - configure - Generates a Makefile for the current module - rebuild - Runs "clean", "configure" and "build" all at once - install - Install node development files for the specified node version. - list - Prints a listing of the currently installed node development files - remove - Removes the node development files for the specified version
    • 这里注意下执行顺序

      • 先进行 pnpm exec node-gyp configure ---> 生成build 目录

      • 然后进行 pnpm exec node-gyp build --> 生成release <name>.node文件的呢

Micro NODE_MODULE

NOTE
  • 核心的使用规则是:

    • NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
    void Initialize(Local<Object> exports); NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
    • context-addon: NODE_MODULE_INITIALIZER
    using namespace v8; extern "C" NODE_MODULE_EXPORT void NODE_MODULE_INITIALIZER(Local<Object> exports, Local<Value> module, Local<Context> context) { /* Perform addon initialization steps here. */ }

cpp-encrypt-addon

NOTE
  • 这里就通过一个简单的例子来进行讲解如何进行使用NativeAddons 吧

    • 核心使用的技术栈是:C++ | nodejs | typescript | node-gyp ...

    • 核心实现的是字符串加密的库吧

first steps: env

  • pnpm init -y

  • mkdir cpp-encrypt-addon

  • pnpm add -D typescript @types/node node-gyp

  • pnpm add node-addon-api

pnpm: 与 npm 的区别在于「依赖安装方式」—— 采用硬链接 + 符号链接管理依赖,避免重复安装,适合多项目开发

node-gyp: 编译工具,核心功能是「将 C++ 代码转为 Node.js 可识别的二进制模块(.node)」,底层依赖系统编译器(如 Windows 的 MSVC、macOS 的 Clang)

node-addon-api: 封装了 Node.js 的 N-API(C 语言接口),提供 C++ 风格的 API,避免直接写 C 语言代码

why use cpp??

  • 性能优势:编译型语言,直接生成机器码,比 JS(解释型)快 10-100 倍,适合加密等计算密集型任务。

  • 系统级交互:可直接调用操作系统 API(如加密库),而 JS 受限于 V8 引擎沙箱。

  • STL(标准模板库):内置数据结构(字符串、容器)和算法,简化加密逻辑实现(类似前端的 Lodash,但更底层)。

cpp core concept

  • 命名空间(namespace):避免函数名冲突,类似前端的模块化(如namespace encrypt { ... })

  • 引用(&):传递变量的内存地址,避免拷贝(类似 JS 的对象引用,但类型严格)。

  • STL 容器与算法:

    • std::string:动态字符串处理(比 C 语言的 char * 更安全,自动管理内存)。

    • std::reverse:反转字符串(STL 算法,类似 JS 的split('').reverse().join(''))。

    • std::for_each:遍历容器(类似 JS 的forEach,但性能更高)。

configure binding.gyp

{ "targets": [ { "target_name": "encrypt_addon", // 生成的.node文件名(最终为encrypt_addon.node) "sources": ["./src/encrypt.cc"], // C++源文件路径 // 头文件搜索路径:必须包含node-addon-api的头文件 "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" // 动态获取路径(跨平台关键) ], // 依赖配置:引入node-addon-api的编译规则(处理跨平台兼容) "dependencies": [ "<!(node -p \"require('node-addon-api').gyp\")" ], // 编译选项:指定C++标准(支持STL特性) "cflags_cc": ["-std=c++17"], // Linux/macOS:启用C++17 "defines": ["NAPI_VERSION=8"], // N-API版本(8是稳定版,兼容Node.js 14+) // 跨平台条件编译(核心!处理不同系统差异) "conditions": [ ["OS == 'win'", { // Windows系统 "msvs_settings": { // Visual Studio编译器配置 "VCCLCompilerTool": { "AdditionalOptions": ["/std:c++17"], // 启用C++17 "ExceptionHandling": 1 // 允许异常处理 } } }], ["OS == 'mac'", { // macOS系统 "xcode_settings": { // Xcode编译器配置 "CLANG_CXX_LANGUAGE_STANDARD": "c++17" // 同步C++标准 } }] ] } ] }
  • 最后的编译命令是:pnpm exec node-gyp configure build

  • Napi 主要是用来进行的是我们的进行 js 的数据类型和 cpp的数据类型进行转换的呐

    • configure:根据当前系统生成编译配置(如 Windows 生成 VS 项目,Linux 生成 Makefile)。

    • build:调用系统编译器(MSVC/Clang/GCC)编译 C++ 代码,生成build/Release/encrypt_addon.node。

base dir

  • 一般在我们的`src下进行书写源代码,以及进行后续的操作吧

    • 一般我们的 C++ 的编程包含的有:

      • 源文件为: .c .cpp

      • 头文件为: .h .hpp

      • 代码的核心的编排是这些吧

  • 定义配置文件 binding.gyp

  • 注意事项

    • 由于在书写 NativeAddon 的时候,我们的核心的步骤是:

        1. 获取得到 js 的输入
        1. 将 js 数据转化为 cpp 中的数据,此时依赖于我们的 napi
        1. 在 cpp 中进行对对应的数据进行处理
        1. 将 cpp 数据转化为 js 供前端使用
        1. 最后设置 exports 进行将处理的方法到处
    • 上面的步骤就说明了这样的使用的话依赖于我们的 commonjs 规范

      • 所以说使用的话需要使用我们的 require 函数来实现导入自定义的 addon

      • 对于 esmodule 的话此时就需要进行对应的自定义构造得到我们的 require ,这个就是核心的 esmodule 工程化的知识了,哈哈

      import { createRequire } from 'module';
      
      const require = createRequire(import.meta.url);
      
      const encryptModule = require('../build/Release/encrypt_addon.node');
      • 在 typescript 中进行使用的时候就需要进行自定义声明 dts
      declare module 'encrypt_addon.node' {
          export function encrypt(input: string): string;
          export function decrypt(input: string): string;
      }
  • 核心的依赖包有

    • 开发环境依赖:

      • @types/node nodejs的类型包

      • typescript ts的核心包,内部包含有对应的 tsc 的打包工具吧

      • node-gyp 实现的编译NativaAddon 为 .node 文件呐

    • 生产环境依赖

      • node-addon-api 核心提供的是 napi.h 这样的头文件吧 js <---> cpp
  • demo 示例:https://github.com/juwenzhang/cpp_encrypt_addon

官网还有很多很多的工具可以借鉴使用,自行选择最合适的搭配即可,这里呐也没有硬性要求,简单来说 NativeAddons 核心解决的还是跨平台以及性能问题的一个利器吧

  1. 实现突破 Javascript 的性能瓶颈
  1. 实现 JS 和底层系统/硬件的交互吧
  1. 实现复用底层的 C/C++ 生态资源

相关开源库阅读

  1. https://github.com/nodejs/nan
  1. https://github.com/damianociarla/node-ffmpeg
  1. https://github.com/cmake-js/cmake-js
  1. https://github.com/nodejs/node-gyp
  1. https://github.com/nodejs/node-addon-api