- 蘇格團隊
- 作者:Tomey
一、為什麼需要用node.js呼叫dll?
公司專案採用Electron( electronjs.org/ )開發pc應用,會涉及到與底層硬體裝置的通訊,而sdk封裝 基本上都是通過 C++ 動態連結庫dll實現的。
有兩種方案可供選擇:
- 方案一: 使用node-ffi
- 方案二: 使用C++編寫一個node addon,通過LoadLibrary呼叫dll
以上兩種方案都可以解決dll呼叫問題,方案選型要個人對C++ 的掌握程度,如果熟悉C++開發,可以直接選擇方案二最方便。如果完全不瞭解C++,那麼只能採用方案一。
由於筆主不太懂C++,最終選擇第一種方案。
二、什麼是node-ffi?
node-ffi是使用純JavaScript載入和呼叫動態庫的node addon,它可以用來在不寫任何C++程式碼的情況下呼叫動態連結庫的API 介面。
ffi究竟幹了什麼?其實它本質上還是一個編譯後的Node addon,node_modules/ffi/build/Release/ffi_bindings.node, ffi_bindings.node就是一個addon ffi充當了nodejs和dll之間的橋樑。
下面是一個簡單的載入dll的demo例項:
var ffi = require('ffi');
var libpath = path.join(_dirname, '/test.dll');
var testLib = ffi.Library(libpath, {
'start': ['bool', ['bool']]
});
testLib.start(true); // true
複製程式碼
三、安裝node-ffi
npm install ffi
複製程式碼
如果本地沒有安裝編譯node addon的環境會報錯,如下圖所示
無論是使用ffi,還是直接寫node addon,都缺少不了編譯node Addon這個步驟,要編譯node addon,有兩種方法:
1、node-gyp(www.npmjs.com/package/nod…)。
npm install node-gyp
複製程式碼
具體安裝參考:github.com/nodejs/node…
總結來說需要以下四點:
- python 2.7-3.0版本之間 (推薦裝v2.7,v3.x.x是不支援的)
- NET Framework 4.5.1
- Visual C++編譯工具 (在windows中是不需要安裝VS,如果自己安裝例如VS2015,導致編譯報錯error MSB4132: The tools version "2.0" is unrecognized. Available tools versions are "4.0".這個問題,說明沒有裝好編譯器,又或者編譯器沒有被正確地識別, node-gyp的文件建議使用npm config set msvs_version 2015, 但是有些機器即使這樣設定了也無效,需要手動設定msvs_version, 應該這樣寫: node-gyp rebuild --msvs_version=2015。如果因為安裝了VS2015導致無法正常編譯,可直接恢復到安裝VS之前的還原點)
- 環境變數配置。(注:python安裝位置需要新增到環境變數)
2、electron-rebuild(www.npmjs.com/package/ele… )
如果採用electron開發應用程式,electron同樣也支援node原生模組,但由於和官方的node 相比使用了不同的 V8 引擎,如果你想編譯原生模組,則需要手動設定electron的headers的位置。
electron-rebuild為多個版本的node和electron提供了一種簡單釋出預編譯二進位制原生模組的方法。 它可以重建electron模組,識別當前electron版本,幫你自動完成了下載 headers、編譯原生模組等步驟。 一個下載 electron-rebuild 並重新編譯的例子:
npm install --save-dev electron-rebuild
# 每次執行"npm install"時,也執行這條命令
./node_modules/.bin/electron-rebuild
# 在windows下如果上述命令遇到了問題,嘗試這個:
.\node_modules\.bin\electron-rebuild.cmd
複製程式碼
詳情請看 electronjs.org/docs/tutori…
這裡需要注意nodejs版本問題,nodejs平臺必須跟dll保持一致,同樣是32位或者64位,如果兩者不一致,會導致呼叫dll失敗。
成功安裝ffi模組之後,就可以開始我們下面的ffi呼叫dll的例項應用。
四、應用舉例
在開發需求中,需要呼叫基於C++編寫的TCP資料轉發服務的SDK。
首先我們來看一下dll標頭檔案介面宣告的程式碼如下:
#ifndef JS_CONNECTION_SDK
#define JS_CONNECTION_SDK
#ifdef JS_SDK
#define C_EXPORT __declspec(dllexport)
#else
#define C_EXPORT __declspec(dllimport)
#endif
extern "C"
{
typedef void(*ReceiveCallback) (int cmd, int seq, const char *data);
/*設定讀取資料回撥*/
C_EXPORT void _cdecl SetReceiveCallback(ReceiveCallback callback);
/*
*設定option
*/
C_EXPORT void _cdecl SetOption(
const char* appKey,
const char* tk,
int lc,
int rm
);
/*
*建立連線
*/
C_EXPORT bool _cdecl CreateConnection();
/*傳送資料*/
C_EXPORT bool _cdecl SendData(int cmd, int seq, const char *data, unsigned int len);
/*釋放連線*/
C_EXPORT void _cdecl ReleaseConnection();
}
#endif
複製程式碼
ffi呼叫dll模組封裝,程式碼如下:
try {
const ffi = require('ffi');
const path = require('path');
const Buffer = require('buffer').Buffer;
const libpath = path.join(APP_PATH, '..', '..', '/testSDK.dll');
const sdkLib = ffi.Library(libpath, {
'CreateConnection': ['bool', []],
'SendData': ['bool', ['int', 'int', 'string', 'int']],
'ReleaseConnection': ['void', []],
'SetOption': ['void', ['string', 'string', 'int', 'int']],
'SetReceiveCallback': ['void', ['pointer']]
});
module.exports = {
createConnection: function(){
sdkLib.CreateConnection();
},
setReceiveCallback(cb) {
global.setReceiveCallback = ffi.Callback('void', ['int', 'int', 'string'], function(cmd, seq, data){
cb && cb(cmd, seq, data && JSON.parse(data));
});
sdkLib.SetReceiveCallback(global.setReceiveCallback);
},
sendData: function(cmd, seq, data){
data = JSON.stringify(data);
sdkLib.SendData(cmd, seq, data, data.replace(/[^\x00-\xff]/g, '000').length, 0);
},
releaseConnection: function(){
sdkLib.ReleaseConnection();
},
setOption: function (option) {
sdkLib.SetOption(
option.appKey,
option.tk,
option.lc,
option.rm
);
}
}
} catch (error) {
log.info(error);
}
複製程式碼
第一步:通過ffi註冊dll介面
const sdkLib = ffi.Library(libpath, {
'CreateConnection': ['bool', []],
'SendData': ['bool', ['int', 'int', 'string', 'int']],
'ReleaseConnection': ['void', []],
'SetOption': ['void', ['string', 'string', 'int', 'int']],
'SetReceiveCallback': ['void', ['pointer']]
});
複製程式碼
ffi.Library方法,第一個引數傳入dll路徑,第二引數JSON物件配置相關介面。
key對應dll標頭檔案中輸出的介面,例如C_EXPORT bool _cdecl CreateConnection();
value array配置引數型別,array[0]註冊介面函式返回值型別,array[1]註冊介面函式傳入形參型別。
1、基礎引數型別bool, char, short, int, long等。
2、指標型別,需要引入ref模組,如下:
var ref = require('ref');
var intPointer = ref.refType('char');
var doublePointer = ref.refType('short');
var charPointer = ref.refType('int');
var stringPointer = ref.refType('long');
var boolPointer = ref.refType('bool');
複製程式碼
3、回撥函式指標pointer,可以通過ffi.Callback建立,如下:
global.setReceiveCallback = ffi.Callback('void', ['int', 'int', 'string'], function(cmd, seq, data){
cb && cb(cmd, seq, data && JSON.parse(data));
});
sdkLib.SetReceiveCallback(global.setReceiveCallback);
複製程式碼
回撥函式引數型別配置與dll介面引數型別配置相同,這裡就不多說。
這裡需要注意一點,回撥函式可能會被JavaScript垃圾自動回收機制回收,所以我這裡是把回撥函式掛載到全域性物件global上。
第二步:介面呼叫
通過ffi.Library(libpath, {...})註冊介面,可以直通過返回的sdkLib物件呼叫對接的介面。例如:
var bool = sdkLib.CreateConnection();
console.log(bool); // true or false;
var cmd = 0, seq = 0, data = {...};
var dataStr = JSON.stringify(data);
// JavaScript中文字元長度在C++中長度計算要*3
sdkLib.SendData(cmd, seq, data, data.replace(/[^\x00-\xff]/g, '000').length);
global.setReceiveCallback = ffi.Callback('void', ['int', 'int', 'string'], function(cmd, seq, data){
cb(cmd, seq, data && JSON.parse(data));
});
sdkLib.SetReceiveCallback(global.setReceiveCallback);
複製程式碼
文章到此結束,寫這篇文章目標主要是記錄自己通過node呼叫dll從無到有以的過程以及採坑記錄,文章有誤的地方,歡迎各位大佬指正~