Electron9.x +vue+ffi-napi 调用Dll动态链接库

本文主要介绍在 Electron9.x 中,使用ffi-napi,ref-array-napi,ref-napi 加载 Windows 动态链接库,并在Vue 渲染进程中使用。使用过程中会遇到一系列的坑,本文将会一一解决,并解释原因。如有同行兄弟遇到此问题可以借鉴。
这里列出所使用的环境:
- Visual Studio 2017
- NodeJS v12.17.0 (x64)
- node-gyp v7.0.0
- Python 2.7.15
- Electron :9.1.0
- @vue/cli 4.4.6
- vue-cli-plugin-electron-builder : 2.0.0-rc.4
- ffi-napi : 3.0.1
- ref-napi : 2.0.3
- ref-array-napi : 1.2.1
- ref-struct-napi : 1.1.1
1. 先自己开发一个DLL文件备用
非本文重点,熟悉的朋友可以略过。在这个DLL中,分别开发了三种情况的C函数:
- A. 参数为基本数据类型
- B. 参数为指针
- C. 参数为指向数组的指针
A比较简单,而B和C 涉及到 参数为指针的情况,函数内部可以修改指针指向的内存,函数运行完毕之后,外部内存中的值将会被修改。相当于输出参数,使用JS调用的时候涉及到内存共享问题。
使用 Visual Studio 2017 开发DLL步骤如下:
1.1 新建项目

配置编译为 64 位,因为我的 NodeJS为 64 位

1.2 头文件
MyDllDemo.h IDE 自动生成了这个文件,并自动创建了 CMyDllDemo (类), nMyDllDemo(全局变量),fnMyDllDemo (函数), 这些我们都不需要,将它们删除,重新定义:
#include "pch.h"
#include "framework.h"
#include "MyDllDemo.h"
MYDLLDEMO_API int add(int a, int b) {
return a + b;
}
// 使用指针修改函数外部数据作为返回值
MYDLLDEMO_API void addPtr(int a, int b, int* z) {
*z = a + b;
}
// 外部传入数组的首地址,函数负责初始化数组数据
MYDLLDEMO_API void initArray(int* array,int length) {
for (int i = 0; i < length;i++,array++) {
*array = 100 + i; // 假设数组长度为4, 则程序运行完毕后结果为[100,101,102,103]
}
}
1.4 编译生成DLL文件


这个 MYDLLDEMO.dll 文件就是我们要在 Node JS中调用的DLL文件。
注意这里编译出来的dll是64位的,NodeJS也应该是64位的。
2 新建NodeJS项目
假设项目目录在 G:/node_ffi_napi_demo
# 添加配置,被保存到了 /.npmrc 配置文件中
npm set registry https://registry.npm.taobao.org/
npm set ELECTRON_MIRROR https://npm.taobao.org/mirrors/electron/
npm set SASS_BINARY_SITE http://npm.taobao.org/mirrors/node-sass
npm set PYTHON_MIRROR http://npm.taobao.org/mirrors/python
# 非必须,备以后使用
npm i chromedriver -g --chromedriver_cdnurl=http://npm.taobao.org/mirrors/chromedriver
# 使用Vue Cli创建vue项目的时候会用到
npm i -g node-sass
# NodeJS 编译 C/C++ 依赖用到
npm i -g node-gyp
#windows 编译工具,需要用管理员身份运行 PowerShell,如果 报错 Could not install Visual Studio Build Tools. 则到 C:\Users\wuqing\.windows-build-tools 目录下 手工进行安装,安装成功后在执行上面的命令
npm i -g --production windows-build-tools
# 安装Python,注意必须是 2.7 版本,安装后并设置环境变量
解决网络下载问题:以管理员身份打开 windows host文件,( C:\Windows\System32\drivers\etc\hosts ),加入如下映射:
cd g:\node_ffi_napi_demo
# https://www.npmjs.com/package/ffi-napi
# 安装这个依赖的时候,会自动使用 node-gyp 进行编译
npm i -S ffi-napi
...其它输出省略
> ffi-napi@3.0.1 install G:\node_ffi_napi_demo\node_modules\ffi-napi
> node-gyp-build
...
+ ffi-napi@3.0.1
added 10 packages from 58 contributors in 39.928s
安装 ref-napi 的时候,只从仓库中下载了源代码,并没有自动执行编译,需要手工执行编译,先执行 node-gyp configure
node-gyp build
# 以下是输出内容
gyp info it worked if it ends with ok
gyp info using node-gyp@7.0.0
gyp info using node@12.17.0 | win32 | x64
gyp info spawn C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe
gyp info spawn args [
gyp info spawn args 'build/binding.sln',
gyp info spawn args '/clp:Verbosity=minimal',
gyp info spawn args '/nologo',
gyp info spawn args '/p:Configuration=Release;Platform=x64'
gyp info spawn args ]
在此解决方案中一次生成一个项目。若要启用并行生成,请添加“/m”开关。
nothing.c
win_delay_load_hook.cc
nothing.vcxproj -> G:\node_ffi_napi_demo\node_modules\ref-napi\build\Release\\nothing.lib
binding.cc
win_delay_load_hook.cc
正在创建库 G:\node_ffi_napi_demo\node_modules\ref-napi\build\Release\binding.lib 和对象 G:\node_ffi_napi_demo\node_modules\ref-napi\build\Release\binding.exp
正在生成代码
All 571 functions were compiled because no usable IPDB/IOBJ from previous compilation was found.
已完成代码的生成
binding.vcxproj -> G:\node_ffi_napi_demo\node_modules\ref-napi\build\Release\\binding.node
gyp info ok
安装 ref-array-napi 和 ref-struct-napi ,因为它们只是纯JS包,并没有本地 C代码,所以无需 node-gyp 编译
const ffi = require('ffi-napi')
var ref = require('ref-napi')
var ArrayType = require('ref-array-napi')
const path = require('path')
// 映射到C语言 int数组类型
var IntArray = ArrayType(ref.types.int)
// 加载 DLL文件,无需写扩展名,将DLL中的函数映射成JS方法
const MyDellDemo = new ffi.Library(path.resolve('MYDLLDEMO'), {
// 方法名必须与C函数名一致
add: [
'int', // 对应 C函数返回类型
['int', 'int'] // C函数参数列表
],
// 使用 ffi中内置类型的简写类型
addPtr: ['void', ['int', 'int', 'int*']],
// IntArray 是上面通过 ArrayType 构建出来的类型
initArray: ['void', [IntArray, 'int']]
})
// 调用add 方法
const result = MyDellDemo.add(1, 2)
console.log(`add method result of 1 + 2 is: ` + result)
// 调用addPtr 方法
// 使用Buffer类在C代码和JS代码之间实现了内存共享,让Buffer成为了C语言当中的指针。
// C函数使用指针操作函数外部的内存,所以首先需要 分配一个int类型的内存空间 第一个参数为 C语言数据类型,第二个参数为 默认值
var intBuf = ref.alloc(ref.types.int, 100)
console.log('addPtr 调用前数据>>', ref.deref(intBuf)) //获取指向的内容
MyDellDemo.addPtr(2, 2, intBuf) // 调用函数,传递指针
console.log('addPtr 调用后数据>>', ref.deref(intBuf))
// 调用initArray 方法
// IntArray 是前面使用ref-napi 和 ref-array-napi 库创建的数据类型,数组的长度为 8
// 这里一定要分配内存空间,否则 函数内的指针无法操作内存
let myArray = new IntArray(8)
MyDellDemo.initArray(myArray, 8)
console.log('初始化数组执行结果:')
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i])
}
要点:
- Js方法名一定要与DLL中的 方法名一致
- C语言数据类型是通过 ref-napi 库来映射的,详细映射可以查看以下文档:
- 官方文档1 官方文档2 官方文档3
- 参考资料: node-ffi使用指南 Node.js 调用C++库
package.json 加入启动脚本
npm start
# 下面是输出
> node_ffi_napi_demo@1.0.0 start G:\node_ffi_napi_demo
> node index.js
add method result of 1 + 2 is: 3
addPtr 调用前数据>> 100
addPtr 调用后数据>> 4
初始化数组执行结果:
100
101
102
103
104
105
106
107
4. 在 Electron 9.x 中使用
以上代码在 NodeJS v12.17.0 (x64) 环境下能够执行成功。下面尝试在 Electron9.1.0 中能够执行成功
4.1 安装Electron 9
const { app, BrowserWindow } = require('electron')
app.on('ready', function createWindow() {
// 创建窗口
let win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true // Node 中的API可以在渲染进程中使用
}
})
// 渲染进程中的web页面可以加载本地文件
win.loadFile('index.html')
// 记得在页面被关闭后清除该变量,防止内存泄漏
win.on('closed', function () {
win = null
})
})
// 页面全部关闭后关闭主进程,这里在不同平台可能有不同的处理方式,这里不深入研究
app.on('window-all-closed', () => {
app.quit()
})
前面写的 index.js 将会被引入到 index.html中, index.html文件:
"scripts": {
"start": "node index.js",
"electron": "npx electron main.js"
},
上面添加了一个名称为 electron的启动脚本,使用 npx命令启动 node_modules 中的 electron.exe, 并指定 main.js 作为入口文件

view > Toggle Developer Tools 可以打开开发者工具,Dll调用的结果在控制台上输出。
5. 在Vue Electron builder 项目中调用DLL
在实际的 Vue Electron项目中调用 Dll 的时候,会遇到一些问题,通过配置可以解决这些问题。我在实际使用的过程中,刚开始遇到了很多问题,一度以为 NodeJS 12.X 和 Electron 9.x 与 ffi-napi 不兼容。有了前面的实验,可以可定的是不存在兼容性问题,通过在 vue.config.js文件中配置,这些问题都可以解决。
5.1 安装@vue/cli
cd electron_vue_ffi_demo
vue add electron-builder
# 我在写这篇文章的时候,electron-builder 只提示到 Electron 9.0.0 版本,先选择这个版本,然后重新安装 9.1.0
^7.0.0
^8.0.0
> ^9.0.0
我们的目标是实验 Electron 9.1.0 ,所以先卸载 9.0.0,然后再安装 9.1.0
npm i ffi-napi ref-napi ref-array-napi ref-struct-napi -S
ffi-napi 会自动调用windows编译工具进行编译,但是 ref-napi 不会,还需要手动执行 node-gyp 命令进行编译
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
}
使用命令 npm run electron:serve
来启动 Electron窗口,发现启动非常慢,最后输出:
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
将 app.on方法中的if 语句块注释掉
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true
}
}
}
5.6 DLL文件
将上面的DLL文件拷贝到项目中。首先在 项目根目录下创建一个 resources文件,这个文件中把 DLL文件作为资源文件放入到项目中。 这里我将DLL编译出了32位和64 位两个文件,都放到了resources目录中。实际运行的时候,可以根据Nodes 是 32位还是 64 位来加载对应的DLL文件。

5.7 编写MyDLL JS模块
在 src 目录下编写 MyDll.js 文件,在这个文件中 加载 DLL文件,并导出为JS 对象方法。
src/MyDll.js 。 这里直接拿上个项目中的 index.js 稍作改动,添加了 32 ,64 架构判断,并将DLL调用用JS进行了封装后导出
import { add, addPtr, initArray } from './MyDll'
// 调用add 方法
const result = add(1, 2)
console.log(`add method result of 1 + 2 is: ` + result)
// 调用addPtr
console.log('addPtr 调用后数据>>', addPtr(2, 2)) // 调用函数,传递指针
// 调用initArray 方法
let myArray = initArray(4)
console.log('初始化数组执行结果:')
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i])
}
启动 npm run electron:serve
, 发现报告错误:

module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
//因为这两个模块中包含原生 C代码,所以要在运行的时候再获取,而不是被webpack打包到bundle中
externals: ['ffi-napi', 'ref-napi']
}
}
}
再次执行后,发现控制台输出正常:
{{ addResult }}
{{ addPtrResult }}
{{ initArrayResult }}
<script>
import { add, addPtr, initArray } from './MyDll'
export default {
data() {
return {
addResult: null,
addPtrResult: null,
initArrayResult: null
}
},
methods: {
exeAdd() {
this.addResult = add(100, 200)
},
exeAddPtr() {
this.addPtrResult = addPtr(2, 2)
},
exeInitArray() {
let len = 4
this.initArrayResult = initArray(len)
console.log('初始化数组执行结果:', this.initArrayResult)
}
}
}
</script>

现在执行正常。
5.10 打包
执行打包脚本:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
//因为这两个模块中包含原生 C代码,所以要在运行的时候再获取,而不是被webpack打包到bundle中
externals: ['ffi-napi', 'ref-napi'],
builderOptions: {
extraResources: {
// 拷贝静态文件到指定位置,否则打包之后出现找不到资源的问题.将整个resources目录拷贝到 发布的根目录下
from: 'resources/',
to: './'
}
}
}
}
}
再次打包后. 在 win-unpacked\resources 中就能找到 dll文件了
