Nodejs如何呼叫Dll模組

蘇格團隊發表於2018-09-17
  • 蘇格團隊
  • 作者: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?

www.npmjs.com/package/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的環境會報錯,如下圖所示

image

無論是使用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從無到有以的過程以及採坑記錄,文章有誤的地方,歡迎各位大佬指正~

相關文章