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下新建gypCache与python -
然后把下载的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文件夹下创建src与include目录;src目录下创建main.cpp,include文件夹下创建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下的
目录如下:
配置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


浙公网安备 33010602011771号