1. 程式人生 > 其它 >Electron9.x +vue+ffi-napi 呼叫Dll動態連結庫

Electron9.x +vue+ffi-napi 呼叫Dll動態連結庫

kaiwil

本文主要介紹在 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://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 ),加入如下對映:

52.216.164.171 github-production-release-asset-2e65be.s3.amazonaws.com
52.216.99.59 github-production-release-asset-2e65be.s3.amazonaws.com
54.231.112.144 github-production-release-asset-2e65be.s3.amazonaws.com
54.231.88.43 github-production-release-asset-2e65be.s3.amazonaws.com
52.216.8.107 github-production-release-asset-2e65be.s3.amazonaws.com

更新DNS:ipconfig /flushdns

以上如果首次安裝,會比較慢,需要耐心等待

2.2 安裝依賴

cd g:\node_ffi_napi_demo
# https://www.npmjs.com/package/ffi-napi
# 安裝這個依賴的時候,會自動使用 node-gyp 進行編譯
npm i -S ffi-napi
...其它輸出省略
> [email protected] install G:\node_ffi_napi_demo\node_modules\ffi-napi
> node-gyp-build
...
+ [email protected]
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 [email protected]
gyp info using [email protected] | 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 [email protected]
gyp info using [email protected] | 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 編譯

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])
}

要點:

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
# 下面是輸出
> [email protected] 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 [email protected] -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/[email protected]
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 [email protected] -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-cli-plugin-electron-builder 外掛文件得知,可以通過在 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, 發現報告錯誤:

image-20200720170744299.png
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)

發現是因為 找不到本地編譯模組導致。查詢vue-cli-plugin-electron-builder 外掛文件https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#table-of-contents, 發現這樣一句話:

image-20200720171034640.png

上文中說要將 本地的包配置到 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

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