Electron-利用DLL實現不可能

爲何

若是咱們的應用想要實現這樣一個需求:監聽電腦的usb接口,當有新的設備(移動硬盤或者U盤)接入電腦時,可以獲取裏面的移動設備的狀況並更新到應用程序的界面上。javascript

按照 Electron 或者 Node.js 現成的接口,咱們沒法直接實現。java

這時候,咱們就能夠根據咱們本身的狀況,對系統的底層接口封裝成獨立的動態連接庫(.dll),而後把動態連接庫暴露給Electron 或者 Node.js 進行調用,從而實現需求。node

簡介

Node.js 能夠經過對接 .dll,調用系統底層接口從而實現原有接口不提供的功能。同理,Electron 是基於Node.jss 進行封裝的,Electron 也就擁有了對接動態連接庫,可以把不可能變爲可能。c++

方案選擇

截至本文完成時間(2018-12-17)git

現有的主流的方案一共有兩項github

  1. 自定義C++ Addonsnpm

  2. node-ffi 對接方案bash

(暫不討論 .net core 等對接方式)數據結構

原理分析

首先,明確一個觀點,Electron 是基於 Node.js 封裝的框架,Electron 對接 動態連接庫 的底層方案,是基Node.js c++ Addons機制多線程

從 Node.js 官網上能夠了解到,Addons機制涉及多個組件和 API 的知識:

  • V8: Node.js 目前用於提供 JavaScript 實現的 C++ 庫。 V8 提供了用於建立對象、調用函數等的機制。 V8 的 API 文檔主要在v8.h頭文件中(Node.js 源代碼中的deps/v8/include/v8.h),也能夠在查看V8 在線文檔

  • libuv: 實現了 Node.js 的事件循環、工做線程、以及平臺全部的的異步操做的 C 庫。 它也是一個跨平臺的抽象庫,使全部主流操做系統中能夠像 POSIX 同樣訪問經常使用的系統任務,好比與文件系統、socket、定時器、以及系統事件的交互。 libuv 還提供了一個相似 POSIX 多線程的線程抽象,可被用於強化更復雜的須要超越標準事件循環的異步插件。 建議插件開發者多思考如何經過在 libuv 的非阻塞系統操做、工做線程、或自定義的 libuv 線程中下降工做負載來避免在 I/O 或其餘時間密集型任務中阻塞事件循環。

  • 內置的 Node.js 庫:Node.js 自身開放了一些插件可使用的 C++ API。 其中最重要的是node::ObjectWrap類。

  • Node.js 包含一些其餘的靜態連接庫:如 OpenSSL,這些庫位於 Node.js 源代碼中的deps/目錄。 只有 V8 和 OpenSSL 符號是被 Node.js 開放的,而且經過插件被用於不一樣的場景。 更多信息可查看連接到 Node.js 自身的依賴

總結來講,Node.js 能夠理解爲 js 和 c++ dll 相互工做的橋樑,而 Node.js 自身也提供了擴展 c++ dll 調用的插件機制。

同理,Electron 也能夠從這個機制中獲益。

目前,主流的 .dll 調用方案的關係圖以下所示:

5c1758d230410

解釋以下:

  • node-ffi 本質上是 Addons 機制下,進行過抽象封裝的方案

  • addons插件須要針對對應版本的Node.js編譯後,才能被對應版本的Node.js進行調用;換言之,若是addons插件在編譯時的目標版本是 Node.js v8.3.1,那麼它編譯後的代碼就不能被 Node.js v6.0.0的版本進行調用

  • node-gyp 能夠幫助開發者脫離當前全局安裝的Node.js版本,指定任意 Node.js 版本進行模塊的編譯,在編譯前,須要下載對應版本的原生模塊頭文件,頭文件的默認下載地址爲 nodejs.org

  • Electron想要調用 .dll 文件,也須要進行對 addons 插件的編譯,編譯用的頭文件也須要額外下載,和Node.js不一樣,Electron對應的頭文件的默認下載地址爲 atom.io/downloaded

  • 頭文件的版本必須與 調用者 ( Node.js 或者 Electron )版本一致,這樣 addons插件(包括 自定義 addon 和 node-ffi)才能正確運行

執行狀況:

Node.js的原生模塊編譯,經過 node-gyp 能夠比較方便地進行編譯

Electron的原生模塊編譯,因爲頭文件與 Node.js 的頭文件並不一致,直接用 node-gyp 進行編譯的話,還須要進行一些額外的配置(頭文件下載地址、版本映射等),相對沒這麼方便。幸虧,開源社區已經準備好了一個封裝好的工具 Electron-rebuild ,它底層原理也是使用 node-gyp 進行編譯,不過就不須要開發者進行額外的配置了

根據 Electron 版本的不一樣(主要是 v4 和 之前的版本不一樣),須要在應用中執行額外的代碼

編譯環境要提早準備,三大操做系統(Windows、MacOS、Linux)各不相同,看官須要根據 node-gyp 的文檔,提早調整好本身的編譯環境。參考文檔(截至 2018-12-17):node-gyp.readme

案例分享

萬事俱備,咱們把源碼準備好,按照 node-gyp的教程準備好編譯環境,開始操做:

本次的案例方案爲 Node-FFI,想要自定義addon的看管,能夠先了解Electron addon的編寫後,再進行編譯 和 使用

第一步:

在項目路徑下,

安裝好全部依賴

npm install
複製代碼

安裝 node-ffi、ref、ref-array:

npm install node-ffi --save
npm install ref --save
npm install ref-array --save
複製代碼

全局安裝好 Electron-rebuild

npm install -g Electron-rebuild
複製代碼

第二步:

假設咱們的Electron版本爲 v3.0.11,32位應用

在項目路徑下,執行 Electron-rebuild 命令,從新編譯 node-ffi、ref、ref-array 原生模塊:

Electron-rebuild -v 3.0.11 -a ia32
複製代碼

第三步:

如無心外,編譯成功後,咱們就能夠經過 Electron 應用調用 ffi 和 ref 模塊了

var ffi = require('ffi');
var ref = require('ref');
複製代碼

第四步:

使用 ref 定義好數據類型,由於 c++ 的數據類型的內存模型不可能和 js 的是一致的,使用時,須要利用 ref 庫進行轉換

var intPointer         = ref.refType('int');
var doublePointer     = ref.refType('double');
var charPointer      = ref.refType('char');
var stringPointer   = ref.refType(ref.types.CString);
var boolPointer        = ref.refType('bool');
複製代碼

pointer,能夠理解爲對應 c++ 裏面的指針,pointer.ref() 則是獲取指針對應的數據

使用 ffi 鏈接 .dll 文件

var usbLib = ffi.Library(libpath, {
    'InitSDK': ['int', ['pointer']],
    'GetData': ['int', ['char', stringPointer]],
    'ClearData': ['int', [charPointer, charPointer]],
    'GetRemovableDrives': ['int', [stringPointer]]
});
複製代碼

libpath 爲 .dll所在的路徑,相對路徑與絕對路徑都可,考慮到後續的安裝包打包,建議爲相對路徑

第五步:

把鏈接好的dll使用起來

var data = new Buffer(1000);

var result = usbLib.GetData(driveName, data);    
var resultStr = '';

if(result === 0){
    resultStr = wideCharBufferToString(data);
}

return resultStr;
複製代碼

如上述代碼,js 調用 .dll中定義好的 GetData 方法

.dll中的C++源碼以下:

int GetData(string driveName, char *data) 複製代碼

函數調用結果,經過 data 參數返回,調用狀態 經過 int 的數據格式返回到 js 的 result 變量中

js中的data,是一個 ref 生成的 StringPointer(其實是經過Buffer擴展出的數據結構)

當函數調用結束,函數的結果也以指針的形式賦值給了data

接下來,把data這個指針指向的數據解析出來,便可獲取函數的返回數據

問題記錄

截至 2018-12-17

一、NodeJs v10.x 與 Electron v3.x 對應的原生模塊頭文件,都沒法和 Node-FFI latest 版本完成編譯(互相不兼容)

解決方法:

開源社區上已經有開發者提交了 Node-FFI 的 PR 並經過了測試

開發人員能夠先安裝 PR 版本的 Node-FFI ,實測能夠正常編譯與正常使用

npm install node-ffi/node-ffi#169773d
複製代碼
相關文章
相關標籤/搜索