NodeJS Addon(NodeJS C++插件/扩展)开发与项目搭建(从入门到转行)

NodeJS Addon(NodeJS C++插件/扩展)开发与项目搭建(从入门到转行)

前言
由于没有找到专业的Nodejs Addon开发工具或者IDE,全程都是手工创建项目。但是创建一次之后,每次都可以复用工程文件,只需要修改相关参数即可
本文首次发布于”博客园“:https://home.cnblogs.com/u/MCMonkey/
注意:本文里涉及到的所有C++源代码一定要保存为utf8格式,将来不会产生编码上的问题

本文里的源代码可以在这里下载:https://files.cnblogs.com/files/blogs/668717/NodejsAddonTest.zip?t=1783574461&download=true

前期工作

  • ①下载vscode并安装扩展(C/C++ DevTools、C/C++ Extension Pack、C/C++ Themes)【用于vscode语法解析和格式美化】
  • ②下载Visual Studio安装,并安装桌面应用和移动应用下的“C++的桌面开发”【构建MSVC工程】
  • ③下载和安装Nodejs(推荐通过nvm安装Nodejs,方便版本管理)【Addon执行程序】
  • ④下载Python3.10.11(版本是一个坑,我记得好像要3.10及以上才行)【构建MSVC工程必要工具】

使用npm创建一个项目

注:我的环境是2022年安装的node18.20.2,以及Visual Studio2022,可能在命令执行上会有差别。例如node18.20.2我在2026年测试之后发现和最新的node-gyp有冲突,只能调回旧版本node-gyp@9.3.1
  • 新建一个文件夹,取名任意(推荐不要含中文,全路径都不要含中文)

  • 分别在目录下创建addons.vscode目录。随后在addons下新建environments,在environments下新建gypCachepython

  • 然后把下载的Python拷贝到python目录下

  • 并使用vscode打开,然后快捷键`ctrl + ``打开内置命令行

  • 创建一个项目 npm init

  • 添加必要的依赖

    • npm install yarn -D # 安装 yarn (个人习惯)
      yarn add node-gyp node-addon-api gulp @types/fs-extra @types/node --dev # 安装开发时依赖
      yarn add fs-extra # 安装运行时依赖
      # node-gyp和node-addon-api是编译Addon的必要库
      # gulp是一个成熟稳定的批任务工具(可以不安装)
      # @types/fs-extra和@types/node是Nodejs常用类型定义库
      # 安装的时候可能安装不上,要么通过科学方式,要么修改npm源
      
  • 在根目录下创建gpy配置文件binding.gyp,内容在下面会详细说明

配置文件与参数

vscode环境配置

①c_cpp_properties.json环境配置
  • .vscode目录下下新建文件c_cpp_properties.json,用来配置项目环境的语法索引,如果不配置也可以使用也能成功构建MSVC工程,但是会缺少语法解析到处报错,以及无法通过ctrl点击进入函数内部。里面的//注释要删除,否则不能使用

    • {
          "configurations": [
              {
                  "name": "Win32", //环境名称
                  "includePath": [ //项目所需所有的include路径,如果没有,源代码能通过编译,但是不能通过语法检查
                      "${workspaceFolder}/node_modules/node-addon-api",
                  	//这个目前还没有,启动编译之后会联网下载到cache,后续都可以一直使用    
                      "${workspaceFolder}/addons/environments/gypCache/18.20.2/include/node/**",
                      "${workspaceFolder}/addons/SayHello/include/**",
                  ],
                  "defines": [
                      "_DEBUG",
                      "UNICODE",
                      "_UNICODE"
                  ],
                  "intelliSenseMode": "windows-msvc-x86",
                  //编译所使用的cli.exe,使用的x86目录下
                  "compilerPath": "D:/myprogram/vs2022/ide/VC/Tools/MSVC/14.38.33130/bin/Hostx86/x86/cl.exe"
              },
              {
                  "name": "Win64",
                  "includePath": [
                      "${workspaceFolder}/node_modules/node-addon-api",
                      "${workspaceFolder}/addons/environments/gypCache/18.20.2/include/node/**",
                      "${workspaceFolder}/addons/SayHello/include/**",
                  ],
                  "defines": [
                      "_DEBUG",
                      "UNICODE",
                      "_UNICODE"
                  ],
                  "windowsSdkVersion": "10.0.22621.0",
                  "intelliSenseMode": "windows-msvc-x64",
                  //编译所使用的cli.exe,使用的x64目录下
                  "compilerPath": "D:/myprogram/vs2022/ide/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe"
              }
          ],
          "version": 4
      }
      
②launch.json调试器配置
  • .vscode目录下新建launch.json调试器配置,如果没有调试器配置,是无法启用vscode对C++代码运行时调试的,但也可以构建和编译MSVC工程。里面的//注释要删除,否则不能使用

    • {
          "version": "0.2.0",
          "configurations": [
              {
                  "name": "Release Temporaldenoiser x86", //启动名称
                  "type": "cppvsdbg",
                  "request": "launch",
                  "program": "node.exe",//全局安装的Nodejs
                  "args": [
                      //启动参数
                      "${workspaceFolder}/build/Release/SayHello.node"
                  ],
                  "stopAtEntry": false,
                  "cwd": "${workspaceFolder}/build/Release/", //命令执行目录
                  "environment": [],
                  "console": "externalTerminal",
                  "preLaunchTask": "npm: SayHello:compile-win32-release" //执行前命令(编译命令)
              },
              {
                  "name": "Release Temporaldenoiser x64", //启动名称
                  "type": "cppvsdbg",
                  "request": "launch",
                  "program": "node.exe",//全局安装的Nodejs
                  "args": [
                      //启动参数
                      "${workspaceFolder}/build/Release/SayHello.node"
                  ],
                  "stopAtEntry": false,
                  "cwd": "${workspaceFolder}/build/Release/", //命令执行目录
                  "environment": [],
                  "console": "externalTerminal",
                  "preLaunchTask": "npm: SayHello:compile-win64-release" //执行前命令(编译命令)
              }
          ]
      }
      
③.clang-format代码美化配置

在根目录下创建.clang-format文件,模板如下。(这个不是必须,只是vscode的C++扩展的代码格式化和平常JS格式美化不一致,因此需要定制化,个人习惯)

# 基于JavaScript风格的C++格式化配置(4空格缩进)
Language: Cpp  # 指定格式化语言为C++

# 访问说明符(public、private等)的偏移量(相对于类定义的缩进)
AccessModifierOffset: -4

# 开括号(圆括号/尖括号/方括号)后的对齐方式
AlignAfterOpenBracket: Align  # 对齐开括号后的内容

# 是否对齐连续赋值语句
AlignConsecutiveAssignments: false

# 是否对齐连续声明语句
AlignConsecutiveDeclarations: false

# 反斜杠换行时的对齐方式
AlignEscapedNewlines: Right  # 右对齐反斜杠

# 是否对齐操作数
AlignOperands: true  # 对齐二元和三元表达式的操作数

# 是否对齐尾部注释
AlignTrailingComments: true

# 是否允许函数所有参数放在下一行
AllowAllParametersOfDeclarationOnNextLine: false

# 是否允许短代码块放在单行
AllowShortBlocksOnASingleLine: true

# 是否允许短case标签放在单行
AllowShortCaseLabelsOnASingleLine: true

# 短函数的处理方式
AllowShortFunctionsOnASingleLine: All  # 允许所有短函数单行显示(类似JS风格)

# 是否允许短if语句放在单行
AllowShortIfStatementsOnASingleLine: true

# 是否允许短循环放在单行
AllowShortLoopsOnASingleLine: true

# 返回类型后的换行方式
AlwaysBreakAfterReturnType: None  # 不强制在返回类型后换行

# 是否在多行字符串字面量前换行
AlwaysBreakBeforeMultilineStrings: false

# 是否在template声明后换行
AlwaysBreakTemplateDeclarations: true

# 函数调用参数是否打包
BinPackArguments: true  # 允许参数自动换行

# 函数声明参数是否打包
BinPackParameters: true  # 允许参数自动换行

# 大括号换行设置(使用Attach风格,类似JS)
BraceWrapping:
  AfterClass: false      # 类后不换行
  AfterControlStatement: false  # 控制语句后不换行
  AfterEnum: false       # 枚举后不换行
  AfterFunction: false   # 函数后不换行
  AfterNamespace: false  # 命名空间后不换行
  AfterStruct: false     # 结构体后不换行
  AfterUnion: false      # 联合体后不换行
  AfterExternBlock: false  # extern块后不换行
  BeforeCatch: false     # catch前不换行
  BeforeElse: false      # else前不换行
  IndentBraces: false    # 不缩进大括号
  SplitEmptyFunction: false  # 不分割空函数
  SplitEmptyRecord: false  # 不分割空记录
  SplitEmptyNamespace: false  # 不分割空命名空间

# 二元运算符前换行方式
BreakBeforeBinaryOperators: None  # 在运算符后换行(JS风格)

# 大括号前换行风格
BreakBeforeBraces: Attach  # 大括号不单独换行(JS风格)

# 是否在三元运算符前换行
BreakBeforeTernaryOperators: false

# 构造函数初始化列表换行位置
BreakConstructorInitializers: AfterColon  # 在冒号后换行

# 是否分割字符串字面量
BreakStringLiterals: false

# 每行字符限制(0表示无限制)
ColumnLimit: 0

# 是否压缩命名空间声明
CompactNamespaces: true

# 构造函数初始化列表格式
ConstructorInitializerAllOnOneLineOrOnePerLine: false

# 构造函数初始化列表缩进宽度
ConstructorInitializerIndentWidth: 4

# 续行缩进宽度
ContinuationIndentWidth: 4

# C++11大括号初始化风格
Cpp11BracedListStyle: true

# 是否从表达式推导指针对齐
DerivePointerAlignment: false

# 是否修复命名空间注释
FixNamespaceComments: true

# 是否缩进case标签(解决switch缩进问题)
IndentCaseLabels: true  # case标签相对于switch缩进

# 预处理指令缩进方式
IndentPPDirectives: None

# 基本缩进宽度
IndentWidth: 4

# 是否缩进包裹的函数名
IndentWrappedFunctionNames: false

# 是否保留块起始处的空行
KeepEmptyLinesAtTheStartOfBlocks: false

# 保留的最大连续空行数
MaxEmptyLinesToKeep: 1

# 命名空间内容缩进方式
NamespaceIndentation: All  # 缩进命名空间内的所有内容

# 指针和引用对齐方式
PointerAlignment: Left  # 星号/引用符靠近类型(类似JS的点号访问)

# 是否重新排版注释
ReflowComments: true

# 是否排序#include
SortIncludes: false

# 是否排序using声明
SortUsingDeclarations: false

# 是否在C风格类型转换后添加空格
SpaceAfterCStyleCast: false

# 是否在template关键字后添加空格
SpaceAfterTemplateKeyword: true

# 是否在赋值运算符前添加空格
SpaceBeforeAssignmentOperators: true

# 是否在圆括号前添加空格
SpaceBeforeParens: ControlStatements  # 控制语句关键字(if/for等)后加空格

# 是否在空圆括号内添加空格
SpaceInEmptyParentheses: false

# 尾随注释前的空格数
SpacesBeforeTrailingComments: 1

# 是否在尖括号内添加空格
SpacesInAngles: false

# 是否在C风格类型转换的括号内添加空格
SpacesInCStyleCastParentheses: false

# 是否在容器字面量中添加空格
SpacesInContainerLiterals: true

# 是否在圆括号内添加空格
SpacesInParentheses: false

# 是否在方括号内添加空格
SpacesInSquareBrackets: false

# C++标准版本
Standard: Cpp11

# Tab宽度(即使使用空格缩进也需设置)
TabWidth: 4

# 使用Tab的方式
UseTab: Never  # 始终使用空格缩进

编译配置

①binding.gyp配置

上面提到的binding.gyp配置文件,模板如下,它的目的是配置Visual Studio工具构建MSVC工程。里面的//注释要删除,否则不能使用

{
    "targets": [
        {
            "target_name": "SayHello", //编译之后Addon的名称
            "cflags!": [ "-fno-exceptions" ],
            "cflags_cc!": [ "-fno-exceptions" ],
            "sources": [
                //编译的当前Addon所用到的项目的源码相对路径
                "addons/SayHello/src/main.cpp"
            ],
            "conditions": [
                ["OS=='win'", {
                    "msvs_settings": {
                        "VCCLCompilerTool": {
                            "AdditionalOptions": ["/utf-8"] //强制MSVC编译文件编码为utf8
                        }
                    }
                }]
            ],
            "include_dirs": [
                "<!@(node -p \"require('node-addon-api').include\")",
                "addons/SayHello/include" //当前Addon所需的所有include目录
            ],
            "library_dirs": [
                //"addons/SayHello/lib/OpenCV/lib" //外部第三方C++库目录
            ],
            "libraries": [
                //"opencv_world480.lib" //外部第三方C++库目录所需的lib文件
            ],
            "dependencies": [
                "<!(node -p \"require('node-addon-api').gyp\")" //依赖的第三方或者子gyp工程文件,在执行编译时会顺带一起编译
            ],
            "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS'] //编译前自定义宏定义(NAPI_DISABLE_CPP_EXCEPTIONS为默认)
        }
    ]
}
②package.json配置

这些命令是用于gyp构建MSVC的控制命令。//注释要删除,否则不能使用

{
  "name": "nodejsaddontest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    // 清理gyp编译文件
    "gpy:clean": "node-gyp clean",
	// 执行MSVC工程配置生成
    "gyp:config": "node-gyp configure --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --devdir=./addons/environments/gypCache/ --msvs_version=2019 --openssl_fips=''",
     //编译32位Addon
    "SayHello:compile-win32-release": "node-gyp build SayHello --release --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --arch=ia32",
     //编译64位Addon【注意,个人测试下发现64位Nodejs只能编译64位Addon,编译32位需要nvm安装一个对应版本的Nodejs32位才行】
    "SayHello:compile-win64-release": "node-gyp build SayHello --release --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --arch=x64",
     //执行gulp任务复制必要运行时dll(通常是第三方dll)
    "SayHello:copyDll": "yarn gulp SayHello:copyDll"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/fs-extra": "^11.0.4",
    "@types/node": "^26.1.1",
    "gulp": "^5.0.1",
    "node-addon-api": "^8.9.0",
    "node-gyp": "9.3.1",
    "yarn": "^1.22.22"
  },
  "dependencies": {
    "fs-extra": "^11.3.6"
  },
  "engines": {"node": ">=18.20.2"}
}

③gulpfile.js配置

在根目录下新建gulpfile.js文件,模板如下。gulp主要用于第三方dll构建时辅助工具,例如将指定dll复制到指定目录。

const gulp = require('gulp');
const fs_extra = require("fs-extra");

gulp.task('default', async function (cb) {
    // 默认任务
    console.log("Gulp start...");
    cb();
});
gulp.task('SayHello:copyDll', async function (cb) {
    // 自定义任务
    cb();
});

C++代码模板

addons目录下新建SayHello文件夹;在SayHello文件夹下创建srcinclude目录;src目录下创建main.cppinclude文件夹下创建SayHello.hpp,代码模板如下

#pragma once
#include <iostream>

void SayHello(){
    std::cout<<"Hello"<<std::endl;
}
#include <iostream>
#include <napi.h>
#include "SayHello.hpp"

Napi::Value JsAddonFunction_SayHello(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    // 执行C++函数
    SayHello();
    return env.Undefined();
}

Napi::Value JsAddonFunctionExample(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (info.Length() < 2) {
        env.RunScript("throw new Error('JsAddonFunctionExample(arg1:string, arg2:string)')")
    }
    Napi::String arg1 = info[0].As<Napi::String>();
    Napi::String arg2 = info[1].As<Napi::String>();

    std::cout << arg1.Utf8Value() << " " << arg2.Utf8Value() << std::endl;
    return env.Undefined();
}

// Addon初始化入口,固定格式
Napi::Object Initialize(Napi::Env env, Napi::Object exports) {
    // 直接执行Addon函数
    Napi::Function::New(env, JsAddonFunction_SayHello).Call({});
    // 导出到JS层给js使用
    exports.Set("JsAddonFunction_SayHello",Napi::Function::New(env, JsAddonFunction_SayHello));
    exports.Set("JsAddonFunctionExample",Napi::Function::New(env, JsAddonFunctionExample));
    return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize)

启动程序

  • 1、先执行yarn gyp:config生成MSVC工程。执行之后会在根目录下出现build目录,并且自动下载cache到该命令内的指定位置--devdir=./addons/environments/gypCache/

  • 2、然后执行yarn SayHello:compile-win64-release,即可在build/Release目录下得到SayHello.node文件,这个文件就是我们的Nodejs Addon,可以被js直接require使用。require之后我们就能得到exports导出的函数,可以直接调用,同时会发现会打印Hello,因为我们在初始化入口处执行了Call

    • //导出
      exports.Set("JsAddonFunction_SayHello",Napi::Function::New(env, JsAddonFunction_SayHello));
      exports.Set("JsAddonFunctionExample",Napi::Function::New(env, JsAddonFunctionExample));
      
    • PS E:\test\NodejsAddonTest\build\Release> node
      Welcome to Node.js v18.20.2.
      Type ".help" for more information.
      > const SayHello = require("./SayHello.node")
      Hello
      undefined
      > console.log(SayHello)
      {
        JsAddonFunction_SayHello: [Function (anonymous)],
        JsAddonFunctionExample: [Function (anonymous)]
      }
      undefined
      > SayHello.JsAddonFunction_SayHello()
      Hello
      undefined
      > SayHello.JsAddonFunctionExample("string1","string2")
      string1 string2
      undefined
      
  • 3、当然,第2条是需要手动用js导入使用。上面配置了launch调试器配置,可以直接使用vscode的调试器启动、选中x64,然后点击那个绿色的三角运行即可。

  • 最后到目前为止,完成了NodeJS Addon工程的搭建,奉上结构目录:

进阶:调用其他第三方dll(以OpenCV为例)

下载OpenCV

  • OpenCV官网下载OpenCV并安装

  • 在SayHello目录下新建一个lib目录,然后再在这个lib目录下新建一个OpenCV目录,然后在OpenCV目录下新建一个lib

  • 从安装后的OpenCV拷贝出如下文件放到指定目录内

    • 把安装的OpenCV下的build/include复制到刚刚新建的OpenCV目录下
    • 把安装的OpenCV下的build/x64/vc16/bin下的opencv_world480.dll复制到刚刚新建的OpenCV/lib目录下
    • 把安装的OpenCV下的build/x64/vc16/lib下的opencv_world480.lib复制到刚刚新建的OpenCV/lib目录下

目录如下:

配置OpenCV到项目内

配置binding.gpy

把OpenCV配置到binding.gpy内,表示要把OpenCV加入MSVC工程

{
    "targets": [
        {
            "target_name": "SayHello",
            "cflags!": [ "-fno-exceptions" ],
            "cflags_cc!": [ "-fno-exceptions" ],
            "sources": [
                "addons/SayHello/src/main.cpp"
            ],
            "conditions": [
                ["OS=='win'", {
                    "msvs_settings": {
                        "VCCLCompilerTool": {
                            "AdditionalOptions": ["/utf-8"]
                        }
                    }
                }]
            ],
            "include_dirs": [
                "<!@(node -p \"require('node-addon-api').include\")",
                "addons/SayHello/include",
                "addons/SayHello/lib/OpenCV/include" //把OpenCVinclude添加到include目录内,使得MSVC工程能够找到OpenCV的头文件
            ],
            "library_dirs": [
                "addons/SayHello/lib/OpenCV/lib" //把OpenCV的lib目录添加到lib搜索目录内,使得MSVC工程能够找到编译时所需的lib文件
            ],
            "libraries": [
                "opencv_world480.lib" //lib目录内目标lib文件,要一一列出
            ],
            "dependencies": [
                "<!(node -p \"require('node-addon-api').gyp\")"
            ],
            "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS']
        }
    ]
}

配置c_cpp_properties.json

配置c_cpp_properties.json目的是让vscode的语法解析能够读取OpenCV的include头文件,使得代码不会出错(指的是源代码语法解析,即便不配置也能够通过MSVC工程构建与编译)

{
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "${workspaceFolder}/node_modules/node-addon-api",
                "${workspaceFolder}/addons/environments/gypCache/18.20.2/include/node/**",
                "${workspaceFolder}/addons/SayHello/include/**",
                "${workspaceFolder}/addons/SayHello/lib/OpenCV/include" //添加到语法解析includepath,防止源码全是错误曲线提示
            ],
            "defines": [
                "_DEBUG",
                "UNICODE",
                "_UNICODE"
            ],
            "intelliSenseMode": "windows-msvc-x86",
            "compilerPath": "D:/myprogram/vs2022/ide/VC/Tools/MSVC/14.38.33130/bin/Hostx86/x86/cl.exe"
        },
        {
            "name": "Win64",
            "includePath": [
                "${workspaceFolder}/node_modules/node-addon-api",
                "${workspaceFolder}/addons/environments/gypCache/18.20.2/include/node/**",
                "${workspaceFolder}/addons/SayHello/include/**",
                "${workspaceFolder}/addons/SayHello/lib/OpenCV/include" //添加到语法解析includepath,防止源码全是错误曲线提示
            ],
            "defines": [
                "_DEBUG",
                "UNICODE",
                "_UNICODE"
            ],
            "windowsSdkVersion": "10.0.22621.0",
            "intelliSenseMode": "windows-msvc-x64",
            "compilerPath": "D:/myprogram/vs2022/ide/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe"
        }
    ],
    "version": 4
}

编写Addon调用OpenCV

修改main.cpp代码简单测试是否配置成功

#include <iostream>
#include <napi.h>
#include "SayHello.hpp"
#include <opencv2/opencv.hpp>

// OpenCV测试代码
int OpenCVTest()
{
    // 1. 创建一幅 600x400 的彩色图像,初始为蓝色 (BGR格式)
    cv::Mat img(400, 600, CV_8UC3, cv::Scalar(255, 0, 0));

    // 2. 绘制一个实心圆 (绿色)
    cv::circle(img, cv::Point(300, 200), 60, cv::Scalar(0, 255, 0), -1);

    // 3. 绘制一个矩形边框 (红色)
    cv::rectangle(img, cv::Point(50, 50), cv::Point(550, 350), cv::Scalar(0, 0, 255), 3);

    // 4. 写上文字 "OpenCV is working!"
    cv::putText(img, "OpenCV is working!", cv::Point(120, 80),
                cv::FONT_HERSHEY_SIMPLEX, 0.9, cv::Scalar(255, 255, 255), 2);

    // 5. 显示图像
    cv::imshow("OpenCV Test", img);
    std::cout << "Press any key in the image window to exit..." << std::endl;
    cv::waitKey(0);  // 等待按键

    return 0;
}
//必须通过Napi进行一次包装,以便于导出给JS层使用
Napi::Value JsAddonFunction_OpenCVTest(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    OpenCVTest();
    return env.Undefined();
}



Napi::Value JsAddonFunction_SayHello(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    // 执行C++函数
    SayHello();
    return env.Undefined();
}

Napi::Value JsAddonFunctionExample(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (info.Length() < 2) {
        env.RunScript("throw new Error('JsAddonFunctionExample(arg1:string, arg2:string)')");
    }
    Napi::String arg1 = info[0].As<Napi::String>();
    Napi::String arg2 = info[1].As<Napi::String>();

    std::cout << arg1.Utf8Value() << " " << arg2.Utf8Value() << std::endl;
    return env.Undefined();
}

Napi::Object Initialize(Napi::Env env, Napi::Object exports) {
    // 直接执行Addon函数
    // Napi::Function::New(env, JsAddonFunction_SayHello).Call({});
    Napi::Function::New(env, JsAddonFunction_OpenCVTest).Call({}); //直接调用OpenCV,方便测试
    // 导出到JS层给js使用
    exports.Set("JsAddonFunction_SayHello",Napi::Function::New(env, JsAddonFunction_SayHello));
    exports.Set("JsAddonFunctionExample",Napi::Function::New(env, JsAddonFunctionExample));
    // 导出OpenCV测试函数接口
    exports.Set("JsAddonFunction_OpenCVTest",Napi::Function::New(env, JsAddonFunction_OpenCVTest));
    return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize)

编译项目与dll拷贝

添加一个gulp任务

编写gulpfile.js添加一个任务,将OpenCV所依赖的dll在编译时复制到编译目标目录内,以供运行时使用

const gulp = require('gulp');
const fs_extra = require("fs-extra");

gulp.task('default', async function (cb) {
    // 默认任务
    console.log("Gulp start...");
    cb();
});
gulp.task('SayHello:copyDll', async function (cb) {
    // 自定义任务,复制OpenCV的运行时dll
    await fs_extra.copyFile(`./addons/SayHello/lib/OpenCV/lib/opencv_world480.dll`,`./build/Release/opencv_world480.dll`);
    cb();
});

添加package.json方便命令

{
  "name": "nodejsaddontest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "gyp:clean": "node-gyp clean",
    "gyp:config": "node-gyp configure --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --devdir=./addons/environments/gypCache/ --msvs_version=2019 --openssl_fips=''",
    "SayHello:compile-win32-release": "node-gyp build SayHello --release --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --arch=ia32",
    "SayHello:compile-win64-release": "node-gyp build SayHello --release --python=./addons/environments/python/python-3.10.11-embed-amd64/python.exe --arch=x64",
    "SayHello:copyDll": "yarn gulp SayHello:copyDll",
      
    // 方便命令,给调试器一键执行
    "SayHello:recompile-win32": "yarn gyp:clean && yarn gyp:config && yarn SayHello:compile-win32-release && yarn SayHello:copyDll",
    "SayHello:recompile-win64": "yarn gyp:clean && yarn gyp:config && yarn SayHello:compile-win64-release && yarn SayHello:copyDll"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/fs-extra": "^11.0.4",
    "@types/node": "^26.1.1",
    "gulp": "^5.0.1",
    "node-addon-api": "^8.9.0",
    "node-gyp": "9.3.1",
    "yarn": "^1.22.22"
  },
  "dependencies": {
    "fs-extra": "^11.3.6"
  },
  "engines": {"node": ">=18.20.2"}
}

修改调试器配置

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Release Temporaldenoiser x86",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "node.exe",
            "args": [
                "${workspaceFolder}/build/Release/SayHello.node"
            ],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}/build/Release/",
            "environment": [],
            "console": "externalTerminal",
            "preLaunchTask": "npm: SayHello:recompile-win32" //把一键命令放到执行前,先编译再执行
        },
        {
            "name": "Release Temporaldenoiser x64",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "node.exe",
            "args": [
                "${workspaceFolder}/build/Release/SayHello.node"
            ],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}/build/Release/",
            "environment": [],
            "console": "externalTerminal",
            "preLaunchTask": "npm: SayHello:recompile-win64" //把一键命令放到执行前,先编译再执行
        }
    ]
}

启动与测试

  • 1、测试通过调试器能否执行成功

    • 通过调试器运行:
    • 如果出现Error: EBUSY: resource busy or locked, rmdir 'E:\test\NodejsAddonTest\build\Release',需要查看是否该目录被命令行或者其他程序占用,先关闭占用程序在执行
    • 执行结果:
  • 2、测试js是否能够通过导入调用成功

    • build/Release/下创建一个测试文件test.js

    • // 调用的时候会触发一次
      const SayHello = require("./SayHello.node"); // 一定要用路径,不能直接使用"SayHello.node",因为不带路径会被nodejs认为是js模块,进而导致nodejs不回去索引.node文件
      
      // 手动调用一次
      SayHello.JsAddonFunction_OpenCVTest();
      
      //总共会弹出两次OpenCV弹窗
      
    • 将当前目录切换到build/Release/下并执行node test.js

Addon内的多线程与异步编程

该章节教程麻烦啰嗦,附带我已经封装的代码,放到项目的include下调用即可

// AsyncThreadCaller.hpp
#include <chrono>
#include <iostream>
#include <napi.h>
#include <thread>

#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <vector>

#include "MyThreadPool.hpp"

#ifndef _ASYNC_THREAD_
#define _ASYNC_THREAD_

// 异步任务执行结果
struct AsyncTaskResult {
    bool result;         // 执行结果
    std::string msg;          // 执行结果消息
    std::string data; // 返回参数
};

// 自定义线程池
CppHelper::MyThreadPool myThreadPool(5);
using Context = Napi::Reference<Napi::Value>;

void cppCallback(Napi::Env env, Napi::Function jsCallback, Context *context, AsyncTaskResult *taskResult);
using TSFN = Napi::TypedThreadSafeFunction<Context, AsyncTaskResult, cppCallback>;
using FinalizerDataType = void;

// c++层面的回调,参数:(当前环境env、由js传递过来的回调函数对象、nodejs上下文指针、C++层面执行BlockingCall/NonBlockingCall传递过来的参数,类型随意但必须是指针)
void cppCallback(Napi::Env env, Napi::Function jsCallback, Context *context, AsyncTaskResult *taskResult) {
    if (taskResult != NULL && taskResult != nullptr) {
        // 手动释放传参
        jsCallback.Call({Napi::Boolean::New(env, taskResult->result), Napi::String::New(env, taskResult->msg), Napi::String::New(env, taskResult->data)});
        delete taskResult;
    } else {
        jsCallback.Call({Napi::Boolean::New(env, false), Napi::String::New(env, "task return nothing"), Napi::String::New(env, "{\"type\":\"null\",\"data\":\"null\"}")});
    }
}

// 调用线程池执行任务,结束之后调用回调返回到js
Napi::Value AsyncWork(const Napi::CallbackInfo &info, Napi::Function callback, std::function<AsyncTaskResult *()> *task) {
    Napi::Env env = info.Env();
    // 当前回调上下文
    Context *context = new Napi::Reference<Napi::Value>(Napi::Persistent(info.This()));
    // 创建一个TypedThreadSafeFunction
    TSFN tsfn = TSFN::New(env,
                          callback,                  // JavaScript function called asynchronously
                          "jsAsyncCallbackFunction", // Name
                          0,                         // Unlimited queue
                          1,                         // Only one thread will use this initially
                          context,
                          [](Napi::Env env, FinalizerDataType *finalizerDataType, Context *ctx) { // Finalizer used to clean threads up
                              delete ctx;                                                         // 必须手动释放
                          });

    // 将任务提交到线程池,切记不要把任何属于node作用域相关的数据传入线程,可能会导致数据被nodejs回收,如果需要,则通过动态申请空间传入
    // 如果是动态申请的值,切记在C++回调内手动释放
    myThreadPool.submitTask([tsfn, task] {
        AsyncTaskResult *result = (*task)();
        delete task;
        // 执行js调用
        napi_status status = tsfn.BlockingCall(result);
        if (status != napi_ok) {
            std::cout << "BlockingCall err occurred in AsyncWork:" + status << std::endl;
        }

        // 释放线程安全函数
        tsfn.Release();
    });
    return Napi::Boolean::New(env, true);
}

namespace AsyncThread {
    /**
    // 多线程非阻塞异步执行task,将执行结果通过js回调返回
    Napi::Value AddonTest_AsyncTest(const Napi::CallbackInfo &info) {
        Napi::Env env = info.Env();
        if (info.Length() < 1) {
            env.RunScript("throw new Error('\"C_Scan(callback:(bool,string)=>void)\" is required: missing args')");
            return env.Null();
        } else if (!info[0].IsFunction()) {
            env.RunScript("throw new Error('\"C_Scan(callback:(bool,string)=>void)\" is required: wrong type of args')");
            return env.Null();
        }

        // 创建C++层面需要多线程执行的逻辑
        std::function<AsyncTaskResult *()> *task = new std::function<AsyncTaskResult *()>([]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(5000));
            // 特殊类型数据通过构造json实现即可
            return new AsyncTaskResult{true, "", "This is test data"};
        });

        AsyncThread::CallAsyncWorker(info, info[0].As<Napi::Function>(), task);
        return Napi::Boolean::New(env, env.Null());
    }


    js层面:
    require("addon.node").AddonTest_AsyncTest((res,msg,data)=>{
        console.log(res); //true '' "This is test data"
    })
     */
    Napi::Value CallAsyncWorker(const Napi::CallbackInfo &info, Napi::Function callback, std::function<AsyncTaskResult *()> *task) {
        AsyncWork(info, callback, task);
        return Napi::Boolean::New(info.Env(), true);
    }
    /**
     * 关闭线程池所有线程(非强制关闭)
     */
    Napi::Value CleanThreadPool(const Napi::CallbackInfo &info) {
        myThreadPool.killAll();
        return Napi::Boolean::New(info.Env(), true);
    }
} // namespace AsyncThread

#endif
// MyThreadPool.hpp

#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>

#ifndef _CPP_HELPPER_
#define _CPP_HELPPER_

namespace CppHelper
{

    /**
     * 定义可复用线程的固定线程池
     */
    class MyThreadPool
    {
    protected:
        std::queue<std::function<void()>> taskQueue; // 任务队列
        std::mutex queueMutex;                       // 互斥锁
        std::condition_variable cv;                  // 条件变量
        std::vector<std::thread> threadPool;         // 线程池
        bool stopThreads = false;

        int opFlag = 0; // 0空状态,1join,2detach,3kill

        // 工作线程函数
        void workerThread()
        {
            while (true)
            {
                std::function<void()> task;
                {
                    std::unique_lock<std::mutex> lock(this->queueMutex);
                    cv.wait(lock, [this]
                            { return !taskQueue.empty() || this->stopThreads; });
                    if (this->stopThreads && taskQueue.empty())
                    {
                        // 如果需要停止线程并且任务队列为空,则退出
                        return;
                    }
                    task = this->taskQueue.front();
                    this->taskQueue.pop();
                }
                task();
            }
        }

    public:
        // 创建线程池
        MyThreadPool(int numThreads)
        {
            if (numThreads <= 0)
                numThreads = 5;
            for (int i = 0; i < numThreads; ++i)
            {
                this->threadPool.emplace_back([this]()
                                              { this->workerThread(); });
            }
        }

        ~MyThreadPool()
        {
            this->killAll();
        }

        // 通过回调提交任务
        void submitTask(std::function<void()> task)
        {
            {
                // 使用括号为了限制lock_guard作用域,能够自动释放相关资源
                std::lock_guard<std::mutex> lock(this->queueMutex);
                this->taskQueue.push(task);
            }
            this->cv.notify_one(); // 唤醒一个线程
        }

        // 将所有当前线程全部join
        void joinAll()
        {
            if (threadPool.size() <= 0 || this->opFlag != 0)
            {
                return;
            }
            this->opFlag = 1;
            for (auto &thread : threadPool)
            {
                thread.join();
            }
        }

        // 将所有当前线程池全部detach
        void detachAll()
        {
            if (threadPool.size() <= 0 || this->opFlag != 0)
            {
                return;
            }
            this->opFlag = 2;
            for (auto &thread : threadPool)
            {
                thread.detach();
            }
        }

        // 将所有当前线程池全部kill
        void killAll()
        {
            if (threadPool.size() <= 0 || this->opFlag == 3)
            {
                return;
            }
            this->opFlag = 3;
            {
                std::unique_lock<std::mutex> lock(queueMutex);
                stopThreads = true; // 设置停止标记
                cv.notify_all();    // 唤醒所有线程
            }

            for (auto &thread : threadPool)
            {
                if (thread.joinable())
                {
                    thread.join(); // 等待线程退出
                }
            }
        }
    };

}

#endif
posted @ 2026-07-09 12:14  麦块程序猿  阅读(4)  评论(0)    收藏  举报