Nodejs如何調用Dll模塊

  • 蘇格團隊
  • 做者:Tomey

1、爲何須要用node.js調用dll?

公司項目採用Electron( electronjs.org/ )開發pc應用,會涉及到與底層硬件設備的通訊,而sdk封裝 基本上都是經過 C++ 動態連接庫dll實現的。node

有兩種方案可供選擇:

  • 方案一: 使用node-ffi
  • 方案二: 使用C++編寫一個node addon,經過LoadLibrary調用dll

以上兩種方案均可以解決dll調用問題,方案選型要我的對C++ 的掌握程度,若是熟悉C++開發,能夠直接選擇方案二最方便。若是徹底不瞭解C++,那麼只能採用方案一。python

因爲筆主不太懂C++,最終選擇第一種方案。git

2、什麼是node-ffi?

www.npmjs.com/package/ffi…github

node-ffi是使用純JavaScript加載和調用動態庫的node addon,它能夠用來在不寫任何C++代碼的狀況下調用動態連接庫的API 接口。npm

ffi究竟幹了什麼?其實它本質上仍是一個編譯後的Node addon,node_modules/ffi/build/Release/ffi_bindings.node, ffi_bindings.node就是一個addon ffi充當了nodejs和dll之間的橋樑。windows

下面是一個簡單的加載dll的demo實例:bash

var ffi = require('ffi');
var libpath = path.join(_dirname, '/test.dll');
var testLib = ffi.Library(libpath, {
	'start': ['bool', ['bool']]
});

testLib.start(true); // true
複製代碼

3、安裝node-ffi

npm install ffi
複製代碼

若是本地沒有安裝編譯node addon的環境會報錯,以下圖所示 app

image

不管是使用ffi,仍是直接寫node addon,都缺乏不了編譯node Addon這個步驟,要編譯node addon,有兩種方法:electron

一、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安裝位置須要添加到環境變量)

二、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的實例應用。

4、應用舉例

在開發需求中,須要調用基於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]註冊接口函數傳入形參類型。

一、基礎參數類型bool, char, short, int, long等。

二、指針類型,須要引入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');
複製代碼

三、回調函數指針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從無到有以的過程以及採坑記錄,文章有誤的地方,歡迎各位大佬指正~

相關文章
相關標籤/搜索