我的electron教程系列
electron教程(一): electron的安裝和項目的創建
electron教程(番外篇一): 開發環境及插件, VSCode調試, ESLint + Google JavaScript Style Guide代碼規范
electron教程(番外篇二): 使用TypeScript版本的electron, VSCode調試TypeScript, TS版本的ESLint
electron教程(二): http服務器, ws服務器, 子進程管理
electron教程(三): 使用ffi-napi引入C++的dll
electron教程(四): 使用electron-builder或electron-packager將項目打包為可執行桌面程序(.exe)
引言
這一篇將介紹如何在node.js+electron環境中, 使用node-ffi/ffi-napi
調用C/C++編寫的動態鏈接庫(即dll), 實現調用C/C++代碼.
本教程適用於electron 4.x-6.x版本.
如electron 4.2.10版本, electron 5.0.6版本, electron 6.0.10版本.
ffi
實現這個功能, 主要使用的插件是ffi
.
node-ffi是一個用於使用純JavaScript加載和調用動態庫的Node.js插件。它可以用來在不編寫任何C++代碼的情況下創建與本地DLL庫的綁定。同時它負責處理跨JavaScript和C的類型轉換。
node-ffi
連接了C代碼和JS代碼, 通過內存共享來完成調用, 而內部又通過ref
,ref-array
和ref-struct
來實現類型轉換.
安裝 ffi-napi
ffi-napi
是作者(node-ffi-napi)根據node-ffi
修改而發布到npm倉庫
的, 可以直接通過npm安裝, 支持node.js 12和electron高版本.
ffi-napi
詳情見: ffi-napi的github頁面
node-ffi
是ffi的官方版本, 但是不能用在我們的項目中, 如果你對它失敗的原因感興趣, 我寫在了本文的最后一節.
1. 部署node.js+electron環境
按步驟完成electron教程(一): electron的安裝和項目的創建所介紹的內容.
2. 安裝ffi-napi
執行指令:
yarn add ffi-napi
使用ffi-napi
在main.js
中添加如下代碼:
const ffi = require('ffi-napi');
/**
* 先定義一個函數, 用來在窗口中顯示字符
* @param {String} text
* @return {*} none
*/
function showText(text) {
return new Buffer(text, 'ucs2').toString('binary');
};
// 通過ffi加載user32.dll
const myUser32 = new ffi.Library('user32', {
'MessageBoxW': // 聲明這個dll中的一個函數
[
'int32', ['int32', 'string', 'string', 'int32'], // 用json的格式羅列其返回類型和參數類型
],
});
// 調用user32.dll中的MessageBoxW()函數, 彈出一個對話框
const isOk = myUser32.MessageBoxW(
0, showText('I am Node.JS!'), showText('Hello, World!'), 1
);
console.log(isOk);
這段代碼中, 主要調用了windows的user32.dll, 具體的步驟都寫在了代碼的注釋中.
啟動程序:
npm start
彈窗出現, Hello World!
自己生成一個dll
0. 首先要明確一點的就是:
ffi只接受純C函數, 確切的說, 是按照C標准編譯的函數
下面來說說具體的原因:
在通過ffi引入dll的時候, 我們是這么聲明的:
const myUser32 = new ffi.Library('user32', {
'MessageBoxW': // 聲明這個dll中的一個函數
[
'int32', ['int32', 'string', 'string', 'int32'], // 用json的格式羅列其返回類型和參數類型
],
});
在user32.dll
中, 尋找一個名字叫MessageBoxW
的函數.
但是, C和C++的函數命名是不同的, 我指的是編譯后的函數名字
對於C, 函數int func(int n)
會被編譯為類似_func
這樣的名字.
對於C++, 函數int func(int n)
會被編譯為類似?func@@YAHH@Z
這樣的名字.
同樣是C++, 函數int func(int double)
會被編譯為類似?func@@YAHN@Z
這樣的名字(和上一個不同).
名字中包含了較多信息, 比如:
參數的入棧方式
返回值的類型
參數的類型和數量
這是因為C++有函數重載
特性, 雖然函數命名是func
, 但int func(int n)
和int func(int double)
完全是兩個不同的函數, 編譯器通過給它們賦予不同的名字來區分它們.
回到ffi, 它在dll中查找函數名字的時候, 是用C風格來查找的.
所以如果你的函數使用C++編譯的, ffl在這個dll中就找不到這個函數, 錯誤LINK 126
!
1. 創建工程
使用VS創建一個C++空項目即可. 項目名成以myAddDll為例.
當然, 你也可以直接創建動態鏈接庫DLL.
2.修改配置類型為動態庫.dll
如圖:
在項目配置中, 選擇生成動態庫.dll
確保你配置了Debug和Release, 同時確保你在x64環境下生成.
3. 函數聲明
創建一個myAdd.h頭文件
聲明一個函數:
extern "C"
{
__declspec(dllexport) int funAdd(int a, int b);
}
extern "C"
意味着:
被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和鏈接的
__declspec(dllexport)
意味着:
__declspec(dllexport)用於Windows中的動態庫中,聲明導出函數、類、對象等供外面調用,省略給出.def文件。即將函數、類等聲明為導出函數,供其它程序調用,作為動態庫的對外接口函數、類等。
4. 函數定義
創建一個myAdd.cpp文件
定義funAdd
函數:
#include "myAdd.h"
int funAdd(int a, int b)
{
return (a + b);
}
函數的內容很簡單, 接受兩個int類型參數, 返回它們的和.
5. 生成dll
右鍵項目選擇生成即可, 生成的myAddDll.dll
位於項目目錄下的x64/Debug中.
(根據你項目的配置去找, x64或x86, Debug或Release)
6. 測試dll
將myAddDll.dll
拷貝至你的electron項目
的根目錄下的dll
文件夾內
在main.js中添加如下代碼:
const ffi = require('ffi-napi'); // 如果前面已經定義過ffi, 就注釋掉這一行
// 通過ffi加載myAddDll.dll
const myAddDll = new ffi.Library('../dll/myAddDll', {
'funAdd': // 聲明這個dll中的一個函數
[
'int', ['int', 'int'], // 用json的格式羅列其返回類型和參數類型
],
});
// 調用函數, 參數1和2, 將返回值直接打印出來, 預計為3
const result = myAddDll.funAdd(1, 2);
console.log(`the result of 1 + 2 is: `+ result);
這段代碼中, 主要調用了myAddDll.dll
中的int funAdd(int, int)
, 具體的步驟都寫在了代碼的注釋中.
啟動程序:
npm start
查看cmd中的日志:
the result of 1 + 2 is: 3
6.1 可能的錯誤
LINK 126
這個錯誤, 意味者electron無法使用你的dll.
在這行代碼中
const myAddDll = new ffi.Library('../dll/myAddDll', {
ffi.Library
的第一個參數, 不光指定了dll的名字, 還指定了dll的路徑.
出現LINK 126
有兩個常見原因:
1. 沒有這個目錄, 或這個目錄下沒有myAddDll.dll
2. myAddDll.dll
還依賴了其他的一些dll, 但是electron無法找到這個dll.
LINK 127
出現LINK 127的可能原因:
1. electron找到了你的dll, 但是在dll中找不到你聲名的函數(funAdd).這通常是由於函數名字錯誤, 或者是返回值類型/參數的個數及類型不一致導致的.
node-ffi為什么會安裝失敗
如果我們按照node-ffi的github鏈接中介紹的方法來安裝ffi, 即
npm install ffi
然后嘗試在main.js
中加上一句代碼來導入這個模塊,
const ffi = require('ffi');
運行一下
npm start
你會得到, 一個錯誤!
仔細看看提示:
The module xxxxxx was compiled against a different Node.js version using
NODE_MODULE_VERSION 69. This version of Node.js requires
NODE_MODULE_VERSION 73
簡單地說:
這個模塊是用一個NODE_MODULE_VERSION 69的node.js版本進行編譯的,
而當前的版本的Node.js需要NODE_MODULE_VERSION 73(來進行編譯).
那么NODE_MODULE_VERSION
是什么意思? 后面的數字又是什么意思?
我們在這里可以查詢到, 各個NODE_MUODULE_VERSION
對應的node.js版本或electron版本, 也叫做node_abi
.
再翻譯一下錯誤提示:
這個模塊是用一個electron 4.0.4 版本進行編譯的,
而當前的版本需要electron 6(來進行編譯).
目前為止, 我們的問題解決方案:
重新編譯.
重新編譯, 指定electron版本, 執行指令:
npm rebuild --runtime=electron --target=6.0.10 --disturl=https://atom.io/download/atom-shell --abi=73
注: 從https://atom.io/download/atom-shell
下載會比較慢, 請自備梯子, 或者使用淘寶庫來下載.
日志中打印出了多條error, 原因是:
node-ffi里面會調用v8或其他依賴模塊的接口, 而這些接口已經更新了, 有的接口改了名字, 有的接口改了參數數量. 但是node-ffi的調用接口語句並沒有更新, 所以編譯不過.
進一步的問題解決方案:
修改node-ffi的代碼, 以適應新版本的v8, 和其他依賴模塊.
一般而言, 我建議翻閱github
中該項目的的issues
, 在關於編譯和electron的話題中, 你會找到其他人已經修改好的代碼, 通常是一個該項目的fork
版本. 你需要下載這個fork
版本的源碼, 將它拷貝至你的項目node_modules
文件夾中, 使用上面的編譯指令編譯安裝.
而前文介紹的ffi-napi
是一個特殊情況, 作者不光修改了node-ffi
. 還把它修改后的代碼上傳到了npm倉庫
, 所以我們可以通過npm install ffi-napi
來進行安裝, 不必按照下載-拷貝-編譯
的流程來安裝它.