Electron9.x+vue+ffi-napi调用Dll动态链接库
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
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 新建项⽬
image-20200720132632850.png
配置编译为 64 位,因为我的 NodeJS为 64 位
image-20200720133034819
1.2 头⽂件
MyDllDemo.h IDE ⾃动⽣成了这个⽂件,并⾃动创建了 CMyDllDemo (类), nMyDllDemo(全局变量),fnMyDllDemo (函数),这些我们都不需要,将它们删除,重新定义:
// `extern "C"`意味着: 被 extern "C" 修饰的变量和函数是按照 C 语⾔⽅式编译和链接的
extern "C"
{
// MYDLLDEMO_API 是上⾯定义的宏,其实就是 __declspec(dllexport)
// 参数和返回值都是基本数据类型
MYDLLDEMO_API int add(int a, int b);
// 使⽤指针修改函数外部数据作为返回值
MYDLLDEMO_API void addPtr(int a, int b,int* z);
// 外部传⼊数组的⾸地址,函数负责初始化数组数据
// array为数组⾸地址, length 为数组长度
MYDLLDEMO_API void initArray(int* array,int length);
}
1.3 源⽂件
MyDllDemo.cpp 删除⽣成的代码后,实现代码如下:
#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⽂件
image-20200720135211587.png
image-20200720135248810.png
这个 MYDLLDEMO.dll ⽂件就是我们要在 Node JS中调⽤的DLL⽂件。
注意这⾥编译出来的dll是64位的,NodeJS也应该是64位的。
2 新建NodeJS项⽬
假设项⽬⽬录在 G:/node_ffi_napi_demo
cd g:\node_ffi_napi_demo
npm init -y
此时⽣成了⼀个 package.json⽂件
2.1 环境准备
在安装依赖之前,先做些准备⼯作。
因为安装 ffi_napi 依赖的时候,需要有编译环境,否则会因为⽆法编译⽽报错。
# 添加配置,被保存到了 <windows⽤户主⽬录>/.npmrc 配置⽂件中
npm set registry https:///
npm set ELECTRON_MIRROR https:///mirrors/electron/
npm set SASS_BINARY_SITE /mirrors/node-sass
npm set PYTHON_MIRROR /mirrors/python
# ⾮必须,备以后使⽤
npm i chromedriver -g --chromedriver_cdnurl=/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 ),加⼊如下映射:
52.216.164.171
52.216.99.59
54.231.112.144
54.231.88.43
52.216.8.107
更新DNS: ipconfig /flushdns
以上如果⾸次安装,会⽐较慢,需要耐⼼等待
2.2 安装依赖
cd g:\node_ffi_napi_demo
# https:///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
npm i -S ref-napi
cd node_modules\ref-napi\
node-gyp configure //配置
# 下⾯是控制台输出内容
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 find Python using Python version 2.7.15 found at "C:\Users\xxxxx\.windows-build-tools\python27\python.exe"
gyp info find VS using VS2017 (15.9.28307.1216) found at:
... 省略输出
gyp info spawn args '-Dmodule_root_dir=G:\\node_ffi_napi_demo\\node_modules\\ref-napi',
gyp info spawn args '-Dnode_engine=v8',
gyp info spawn args '--depth=.',
gyp info spawn args '--no-parallel',
gyp info spawn args '--generator-output',
gyp info spawn args 'G:\\node_ffi_napi_demo\\node_modules\\ref-napi\\build',
gyp info spawn args '-Goutput_dir=.'
gyp info spawn args ]
gyp info ok
在执⾏编译命令:node-gyp build
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_
nothing.vcxproj -> G:\node_ffi_napi_demo\node_modules\ref-napi\build\Release\\nothing.lib
win_delay_load_
正在创建库 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 编译
npm i -S ref-array-napi ref-struct-napi
3. 使⽤ffi-napi 调⽤Dll
将前⾯⽣成的 DLL⽂件拷贝到NodeJS项⽬根⽬录下,然后新建⼀个 index.js 作为nodejs 程序⼊⼝:
image-20200720143025083.png
index.js
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 库来映射的,详细映射可以查看以下⽂档:
参考资料:
package.json 加⼊启动脚本
{
"name": "node_ffi_napi_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ffi-napi": "^3.0.1",
"ref-array-napi": "^1.2.1",
"ref-napi": "^2.0.3",
"ref-struct-napi": "^1.1.1"
}
}
启动程序执⾏:
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
npm i electron@9.1.0 -D
Electron9 被安装到了 node_modules⽬录中了,node 提供了 npx命令来⽅便执⾏ node_modules下的可执⾏脚本,稍后在 package.json中添加启动脚本。
4.2 编写main.js 来启动 Electron
main.js
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⽂件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
请点击菜单,打开开发者⼯具,查看控制台输出
<script src="index.js"></script>
</body>
</html>
在package.json中添加启动脚本
"scripts": {
"start": "node index.js",
"electron": "npx electron main.js"
},
上⾯添加了⼀个名称为 electron的启动脚本,使⽤ npx命令启动 node_modules 中的 electron.exe, 并指定 main.js 作为⼊⼝⽂件
image-20200720154256860.png
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
npm i -g @vue/cli@4.4.6
cd g:
vue create electron_vue_ffi_demo
# 选择默认选项
Vue CLI v4.4.6
Please pick a preset:
.preset (node-sass, babel, router, eslint)
element (node-sass, babel, router, eslint)
> default (babel, eslint)
Manually select features
# 等待安装
5.2 安装 electron-builder 插件
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 uninstall electron
npm i electron@9.1.0 -D
5.3 安装ffi-napi,ref-napi ,ref-array-napi,ref-struct-napi 依赖
这⾥使⽤⼀条命令进⾏安装
npm i ffi-napi ref-napi ref-array-napi ref-struct-napi -S
ffi-napi 会⾃动调⽤windows编译⼯具进⾏编译,但是 ref-napi 不会,还需要⼿动执⾏ node-gyp 命令进⾏编译
cd node_modules\ref-napi\
node-gyp configure
node-gyp build
cd g:\electron_vue_ffi_demo
5.4 去掉 electron-devtools-installer 的安装
项⽬ package.json⽂件中已经添加了启动脚本:
"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窗⼝,发现启动⾮常慢,最后输出:
Failed to fetch extension, trying 4 more times
Failed to fetch extension, trying 3 more times
Failed to fetch extension, trying 2 more times
Failed to fetch extension, trying 1 more times
这是因为默认添加的 background.js ⽂件中,做了 electron-devtools-installer 插件安装,因为⽹络原因我们⽆法在google应⽤商店下载到插件,所以这⾥直接在代码中去掉这部分的安装。
在 background.js 中注释掉:
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
将 app.on⽅法中的if 语句块注释掉
app.on('ready', async () => {
// if (isDevelopment && !process.env.IS_TEST) {
// // Install Vue Devtools
// try {
// await installExtension(VUEJS_DEVTOOLS)
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
// }
createWindow()
})
再次执⾏ npm run electron:serve 发现很快启动
5.5 允许渲染进程集成NodeJS
background.js 代码中默认nodeIntegration 是为false,通过查看得知,可以通过在 vue.config.js 配置⽂件中进⾏配置:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true
}
}
}
5.6 DLL⽂件
将上⾯的DLL⽂件拷贝到项⽬中。
⾸先在项⽬根⽬录下创建⼀个 resources⽂件,这个⽂件中把 DLL⽂件作为资源⽂件放⼊到项⽬中。
这⾥我将DLL编译出了32位和64 位两个⽂件,都放到了resources⽬录中。
实际运⾏的时候,可以根据Nodes 是 32位还是 64 位来加载对应的DLL⽂件。
image-20200720162228250.png
5.7 编写MyDLL JS模块
在 src ⽬录下编写 MyDll.js ⽂件,在这个⽂件中加载 DLL⽂件,并导出为JS 对象⽅法。
src/MyDll.js 。
这⾥直接拿上个项⽬中的 index.js 稍作改动,添加了 32 ,64 架构判断,并将DLL调⽤⽤JS进⾏了封装后导出
const ffi = require('ffi-napi')
var ref = require('ref-napi')
var ArrayType = require('ref-array-napi')
const path = require('path')
let { arch } = process // x64
//默认加载 32位 DLL
let dllFilePath = path.resolve('resources/MYDLLDEMO_x32')
if (arch === 'x64') {
dllFilePath = path.resolve('resources/MYDLLDEMO_x64')
}
// 映射到C语⾔ int数组类型,并导出
const IntArray = ArrayType(ref.types.int)
// 加载 DLL⽂件,⽆需写扩展名,将DLL中的函数映射成JS⽅法
// 导出为JS⽅法
const MyDellDemo = new ffi.Library(dllFilePath, {
// ⽅法名必须与C函数名⼀致
add: [
'int', // 对应 C函数返回类型
['int', 'int'] // C函数参数列表
],
addPtr: ['void', ['int', 'int', 'int*']],
initArray: ['void', [IntArray, 'int']]
})
module.exports = {
add(x, y) {
return MyDellDemo.add(x, y)
},
addPtr(x, y) {
var intBuf = ref.alloc(ref.types.int, 100)
MyDellDemo.addPtr(x, y, intBuf)
return ref.deref(intBuf)
},
initArray(len) {
let myArray = new IntArray(len)
MyDellDemo.initArray(myArray, len)
let result = []
for (var i = 0; i < len; i++) {
result.push(myArray[i])
}
return result
}
}
5.8 尝试在主进程中调⽤
在 background.js ⽂件中,加载 MyDLL 模块并调⽤它. 在⽂件末尾处加⼊代码:
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, 发现报告错误:
App threw an error during load
Error: No native build was found for platform=win32 arch=x64 runtime=electron abi=80 uv=1 libc=glibc
at Function.load.path (webpack:///./node_modules/node-gyp-build/index.js?:56:9)
at load (webpack:///./node_modules/node-gyp-build/index.js?:21:30)
at eval (webpack:///./node_modules/ref-napi/lib/ref.js?:8:111)
at Object../node_modules/ref-napi/lib/ref.js (G:\electron_vue_ffi_demo\dist_electron\index.js:1764:1)
at __webpack_require__ (G:\electron_vue_ffi_demo\dist_electron\index.js:20:30)
at eval (webpack:///./node_modules/ffi-napi/lib/ffi.js?:7:13)
at Object../node_modules/ffi-napi/lib/ffi.js (G:\electron_vue_ffi_demo\dist_electron\index.js:635:1)
at __webpack_require__ (G:\electron_vue_ffi_demo\dist_electron\index.js:20:30)
at eval (webpack:///./src/MyDll.js?:1:13)
at Object../src/MyDll.js (G:\electron_vue_ffi_demo\dist_electron\index.js:2080:1)
发现是因为找不到本地编译模块导致。
查询,, 发现这样⼀句话:
image-20200720171034640.png
上⽂中说要将本地的包配置到 webpack的 externals(外部扩展)中指定。
引⽤中的话:
防⽌将某些 import 的包(package)打包到 bundle 中,⽽是在运⾏时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
所以在 vue.config.js⽂件中做如下配置:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
//因为这两个模块中包含原⽣ C代码,所以要在运⾏的时候再获取,⽽不是被webpack打包到bundle中
externals: ['ffi-napi', 'ref-napi']
}
}
}
再次执⾏后,发现控制台输出正常:
INFO Launching Electron...
add method result of 1 + 2 is: 3
addPtr 调⽤后数据>> 4
初始化数组执⾏结果:
100
101
102
103
5.9 在渲染进程中使⽤
App.vue
<template>
<div id="app">
<button @click="exeAdd">执⾏add⽅法</button> {{ addResult }}
<hr />
<button @click="exeAddPtr">执⾏addPtr⽅法</button> {{ addPtrResult }}
<hr />
<button @click="exeInitArray">执⾏initArray⽅法,初始化数组</button>
{{ initArrayResult }}
<hr />
</div>
</template>
<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>
image-20200720172702596.png
现在执⾏正常。
5.10 打包
执⾏打包脚本:
npm run electron:build
image-20200720172940444.png
执⾏exe⽂件后:
image-20200720173032905.png
这个问题是因为找不到DLL⽂件。
原因是打包的时候,没有将项⽬中的dll⽂件拷贝到最终⽣成的dist_electron\win-unpacked ⽂件夹中。
这同样需要在 vue.config.js ⽂件中做配置:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
//因为这两个模块中包含原⽣ C代码,所以要在运⾏的时候再获取,⽽不是被webpack打包到bundle中
externals: ['ffi-napi', 'ref-napi'],
builderOptions: {
extraResources: {
// 拷贝静态⽂件到指定位置,否则打包之后出现找不到资源的问题.将整个resources⽬录拷贝到发布的根⽬录下
from: 'resources/',
to: './'
}
}
}
}
}
再次打包后. 在 win-unpacked\resources 中就能找到 dll⽂件了
image-20200720173609351.png。