本文主要介绍在 Electron13.x 中,使用ffi-napi,ref-array-napi,ref-napi 加载 Windows 动态链接库,并在Vue 渲染进程中使用。

这里列出所使用的环境:

  • Visual Studio 2017
  • NodeJS v14.16.1(x64)
  • node-gyp v9.1.0
  • Python 3.8.6
  • Electron :13.0.0
  • @vue/cli : 4.5.0
  • vue-cli-plugin-electron-builder : 2.1.1
  • ffi-napi : 4.0.3
  • ref-napi : 3.0.3
  • ref-array-napi : 1.2.2
  • 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 (函数), 这些我们都不需要,将它们删除,重新定义:

 MyDllDemo.h删除 生成的代码后,实现代码如下:

#声明一个函数
extern "C"
{
    __declspec(dllexport) int add(int a, int b);
}

extern "C"意味着:

被 extern "C" 修饰的变量和函数是按照 C 语言方式编译和链接的

__declspec(dllexport)意味着:

__declspec(dllexport)用于Windows中的动态库中,声明导出函数、类、对象等供外面调用,省略给出.def文件。即将函数、类等声明为导出函数,供其它程序调用,作为动态库的对外接口函数、类等。

  1.3 源文件

MyDllDemo.cpp 删除 生成的代码后,实现代码如下:

 1.4 编译生成DLL文件

 这个 MYDLLDEMO.dll 文件就是我们要在 Node JS中调用的DLL文件。 

注意这里编译出来的dll是64位的,NodeJS也应该是64位的。

 2. 在 Electron 13.x 中使用

这个方法使用的是vue-cli 框架

2、创建好项目,使用vue ui 添加插件 vue-cli-plugin-electron-builder,项目自动生成为electron项目
3、使用npm run electron:serve 就可以启动项目

2.1 使用vue cli 创建一个vue 项目 vue create vue-electron-demo

npm i -g @vue/cli@4.5.0
cd E:
vue creat vue-electron-demo
#选择默认选项
? Please pick a preset: (Use arrow keys)
> Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

#等待安装

2.2 安装 electron-builder 插件

cd vue-electron-demo
vue add electron-builder

#  默认选择这个版本 Electron 13.0.0 版本
✔  Successfully installed plugin: vue-cli-plugin-electron-builder

? Choose Electron Version (Use arrow keys)
  ^11.0.0
  ^12.0.0
> ^13.0.0

命令npm run serve 运行 vue,命令npm run electron:serve 运行 electron

 2.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\
#python 版本大于等于3.6
node-gyp configure
node-gyp build
 
cd E:\vue-electron-demo
 

2.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 发现很快启动

2.5 允许渲染进程集成NodeJS

background.js 代码中默认nodeIntegration 是为false,通过查看  vue-cli-plugin-electron-builder 插件文档 得知,可以通过在 vue.config.js 配置文件中进行配置:

module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true
    }
  }
}

2.6 DLL文件

将上面的DLL文件拷贝到项目中。首先在 项目根目录下创建一个 resources文件,这个文件中把 DLL文件作为资源文件放入到项目中。 这里我将DLL编译出了32位和64 位两个文件,都放到了resources目录中。实际运行的时候,可以根据Nodes 是 32位还是 64 位来加载对应的DLL文件。

 

2.7 编写MyDLL JS模块

在 src 目录下编写 MyDll.js 文件,在这个文件中 加载 DLL文件,并导出为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
  }
}

ref库中已经帮我们准备好了基础类型的对应关系。

C++类型ref对应类型
voidref.types.void
int8ref.types.int8
uint8ref.types.uint8
int16ref.types.int16
uint16ref.types.uint16
floatref.types.float
doubleref.types.double
boolref.types.bool
charref.types.char
ucharref.types.uchar
shortref.types.short
ushortref.types.ushort
intref.types.int
uintref.types.uint
longref.types.long
ulongref.types.ulong
DWORDref.types.ulong

2.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=89 uv=1 libc=glibc node=14.16.0 electron=13.6.9 webpack=true
    loaded from: E:\vue-electron-demo
    at Function.load.path (webpack:///./node_modules/node-gyp-build/index.js?:60:9)
    at load (webpack:///./node_modules/node-gyp-build/index.js?:22:30)
    at eval (webpack:///./node_modules/ref-napi/lib/ref.js?:8:111)
    at Object../node_modules/ref-napi/lib/ref.js (E:\vue-electron-demo\dist_electron\index.js:1693:1)
    at __webpack_require__ (E:\vue-electron-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 (E:\vue-electron-demo\dist_electron\index.js:635:1)
    at __webpack_require__ (E:\vue-electron-demo\dist_electron\index.js:20:30)
    at eval (webpack:///./src/MyDll.js?:1:13)
    at Object../src/MyDll.js (E:\vue-electron-demo\dist_electron\index.js:2008:1)

发现是因为 找不到本地编译模块导致。查询  vue-cli-plugin-electron-builder 插件文档https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#table-of-contents, 发现这样一句话:

上文中说要将 本地的包配置到 webpack的 externals(外部扩展)中指定。引用 webpack官方文档中的话:

防止将某些 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

2.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>

现在执行正常。

2.10 打包

执行打包脚本:

npm run electron:build

执行exe文件后:

这个问题是因为找不到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文件了

 执行exe文件成功运行。

3. 常见错误

  • Dynamic Linking Error: Win32 error 126
  1. 通常是传入的DLL路径错误,找不到Dll文件,推荐使用绝对路径。
  2. 如果是在x64的node/electron下引用32位的DLL,也会报这个错,反之亦然。要确保DLL要求的CPU架构和你的运行环境相同。
  3. DLL还有引用其他DLL文件,但是找不到引用的DLL文件,可能是VC依赖库或者多个DLL之间存在依赖关系。
  •  Dynamic Linking Error: Win32 error 127

1.DLL中没有找到对应名称的函数,需要检查头文件定义的函数名是否与DLL调用时写的函数名是否相同。

3.1Path设置

如果你的DLL是多个而且存在相互调用问题,会出现Dynamic Linking Error: Win32 error 126错误3。这是由于默认的进程Path是二进制文件所在目录,即node.exe/electron.exe目录而不是DLL所在目录,导致找不到DLL同目录下的其他引用。可以通过如下方法解决: 

// 调用winapi SetDllDirectoryA设置目录
const kernel32Api = ffi.Library("kernel32", {
'SetDllDirectoryA': ["bool", ["string"]],
})
// DLL文件存放的目录:path.resolve('resources')
//console.log('kernel32',path.resolve('resources'))
kernel32Api.SetDllDirectoryA(path.resolve('resources'));

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐