在nodejs
/elctron
中,可以通過node-ffi,通過Foreign Function Interface
調用動態鏈接庫,俗稱調DLL,實現調用C/C++代碼,從而實現許多node不好實現的功能,或復用諸多已實現的函數功能。
node-ffi是一個用於使用純JavaScript加載和調用動態庫的Node.js插件。它可以用來在不編寫任何C ++代碼的情況下創建與本地DLL庫的綁定。同時它負責處理跨JavaScript和C的類型轉換。
與Node.js Addons
相比,此方法有如下優點:
1. 不需要源代碼。
2. 不需要每次重編譯`node`,`Node.js Addons`引用的`.node`會有文件鎖,會對`electron應用熱更新造成麻煩。
3. 不要求開發者編寫C代碼,但是仍要求開發者具有一定C的知識。
缺點是:
1. 性能有折損
2. 類似其他語言的FFI調試,此方法近似黑盒調用,差錯比較困難。
安裝
node-ffi
通過Buffer
類,在C代碼和JS代碼之間實現了內存共享,類型轉換則是通過ref、ref-array、ref-struct實現。由於node-ffi
/ref
包含C原生代碼,所以安裝需要配置Node原生插件編譯環境。
// 管理員運行bash/cmd/powershell,否則會提示權限不足 npm install --global --production windows-build-tools npm install -g node-gyp
根據需要安裝對應的庫
npm install ffi
npm install ref
npm install ref-array
npm install ref-struct
如果是electron
項目,則項目可以安裝electron-rebuild插件,能夠方便遍歷node-modules
中所有需要rebuild
的庫進行重編譯。
npm install electron-rebuild
在package.json中配置快捷方式
package.json "scripts": { "rebuild": "cd ./node_modules/.bin && electron-rebuild --force --module-dir=../../" }
之后執行npm run rebuild 操作即可完成electron
的重編譯。
簡單范例
extern "C" int __declspec(dllexport)My_Test(char *a, int b, int c); extern "C" void __declspec(dllexport)My_Hello(char *a, int b, int c); import ffi from 'ffi' // `ffi.Library`用於注冊函數,第一個入參為DLL路徑,最好為文件絕對路徑 const dll = ffi.Library( './test.dll', { // My_Test是dll中定義的函數,兩者名稱需要一致 // [a, [b,c....]] a是函數出參類型,[b,c]是dll函數的入參類型 My_Test: ['int', ['string', 'int', 'int']], // 可以用文本表示類型 My_Hello: [ref.types.void, ['string', ref.types.int, ref.types.int]] // 更推薦用`ref.types.xx`表示類型,方便類型檢查,`char*`的特殊縮寫下文會說明 }) //同步調用 const result = dll.My_Test('hello', 3, 2) //異步調用 dll.My_Test.async('hello', 3, 2, (err, result) => { if(err) { //todo } return result })
變量類型
C語言中有4種基礎數據類型----整型 浮點型 指針 聚合類型
基礎
整型、字符型都有分有符號和無符號兩種。
類型 | 最小范圍
------------ --- | ----------
char | 0 ~ 127
signed char | -127 ~ 127
unsigned char | 0 ~ 256
在不聲明unsigned時 默認為signed型
ref
中unsigned
會縮寫成u
, 如 uchar
對應 usigned char
。
浮點型中有 float
double
long
double
。
ref
庫中已經幫我們准備好了基礎類型的對應關系。
C++類型 | ref對應類型 |
---------- | ------------
void | ref.types.void
int8 | ref.types.int8
uint8 | ref.types.uint8
int16 | ref.types.int16
uint16 | ref.types.uint16
float | ref.types.float
double | ref.types.double
bool | ref.types.bool
char | ref.types.char
uchar | ref.types.uchar
short | ref.types.short
ushort | ref.types.ushort
int | http://ref.types.int
uint | ref.types.uint
long | ref.types.long
ulong | ref.types.ulong
DWORD | ref.types.ulong
DWORD為
winapi
類型,下文會詳細說明
更多拓展可以去ref doc
ffi.Library
中,既可以通過ref.types.xxx的方式申明類型,也可以通過文本(如uint16
)進行申明。
字符型
字符型由char
構成,在GBK
編碼中一個漢字占2個字節,在UTF-8中占用3~4個字節。一個ref.types.char
默認一字節。根據所需字符長度創建足夠長的內存空間。這時候需要使用ref-array
庫。
const ref = require('ref') const refArray = require('ref-array') const CharArray100 = refArray(ref.types.char, 100) // 申明char[100]類型CharArray100 const bufferValue = Buffer.from('Hello World') // Hello World轉換Buffer // 通過Buffer循環復制, 比較啰嗦 const value1 = new CharArray100() for (let i = 0, l = bufferValue.length; i < l; i++) { value1[i] = bufferValue[i] } // 使用ref.alloc初始化類型 const strArray = [...bufferValue] //需要將`Buffer`轉換成`Array` const value2 = ref.alloc(CharArray100, strArray)
在傳遞中文字符型時,必須預先得知DLL
庫的編碼方式。node默認使用UTF-8編碼。若DLL不為UTF-8編碼則需要轉碼,推薦使用iconv-lite
npm install iconv-lite
const iconv = require('iconv-lite') const cstr = iconv.encode(str, 'gbk')
注意!使用encode轉碼后cstr
為Buffer
類,可直接作為當作uchar
類型
iconv.encode(str.'gbk')中gbk默認使用的是unsigned char | 0 ~ 256
儲存。假如C代碼需要的是signed char | -127 ~ 127
,則需要將buffer中的數據使用int8類型轉換。
const Cstring100 = refArray(ref.types.char, 100) const cString = new Cstring100() const uCstr = iconv.encode('農企葯丸', 'gbk') for (let i = 0; i < uCstr.length; i++) { cString[i] = uCstr.readInt8(i) }
C代碼為字符數組char[]
/char *
設置的返回值,通常返回的文本並不是定長,不會完全使用預分配的空間,末尾則會是無用的值。如果是預初始化的值,一般末尾是一大串的0x00
,需要手動做trimEnd
,如果不是預初始化的值,則末尾不定值,需要C代碼明確返回字符串數組的長度returnValueLength
。
內置簡寫
ffi中內置了一些簡寫
ref.types.int => 'int'
ref.refType('int') => 'int*'
char* => 'string'
只建議使用'string'。
字符串雖然在js中被認為是基本類型,但在C語言中是以對象的形式來表示的,所以被認為是引用類型。所以string其實是**char 而不是*char
聚合類型
多維數組
遇到定義為多維數組的基本類型 則需要使用ref-array進行創建
char cName[50][100] // 創建一個cName變量儲存級50個最大長度為100的名字 const ref = require('ref') const refArray = require('ref-array') const CName = refArray(refArray(ref.types.char, 100), 50) const cName = new CName()
結構體
結構體是C中常用的類型,需要用到ref-struct
進行創建
typedef struct { char cTMycher[100]; int iAge[50]; char cName[50][100]; int iNo; } Class; typedef struct { Class class[4]; } Grade; const ref = require('ref') const Struct = require('ref-struct') const refArray = require('ref-array') const Class = Struct({ // 注意返回的`Class`是一個類型 cTMycher: RefArray(ref.types.char, 100), iAge: RefArray(ref.types.int, 50), cName: RefArray(RefArray(ref.types.char, 100), 50) }) const Grade = Struct({ // 注意返回的`Grade`是一個類型 class: RefArray(Class, 4) }) const grade3 = new Grade() // 新建實例
指針
指針是一個變量,其值為實際變量的地址,即內存位置的直接地址,有些類似於JS中的引用對象。
C語言中使用*
來代表指針
例如 int* a 則就是 整數型a變量的指針
, &
用於表示取地址
int a=10, int *p; // 定義一個指向整數型的指針`p` p=&a // 將變量`a`的地址賦予`p`,即`p`指向`a`
node-ffi
實現指針的原理是借助ref
,使用Buffer
類在C代碼和JS代碼之間實現了內存共享,讓Buffer
成為了C語言當中的指針。注意,一旦引用ref
,會修改Buffer
的prototype
,替換和注入一些方法,請參考文檔ref文檔
const buf = new Buffer(4) // 初始化一個無類型的指針 buf.writeInt32LE(12345, 0) // 寫入值12345 console.log(buf.hexAddress()) // 獲取地址hexAddress buf.type = ref.types.int // 設置buf對應的C類型,可以通過修改`type`來實現C的強制類型轉換 console.log(buf.deref()) // deref()獲取值12345 const pointer = buf.ref() // 獲取指針的指針,類型為`int **` console.log(pointer.deref().deref()) // deref()兩次獲取值12345
要明確一下兩個概念 一個是結構類型,一個是指針類型,通過代碼來說明。
// 申明一個類的實例 const grade3 = new Grade() // Grade 是結構類型 // 結構類型對應的指針類型 const GradePointer = ref.refType(Grade) // 結構類型`Grade`對應的指針的類型,即指向Grade // 獲取指向grade3的指針實例 const grade3Pointer = grade3.ref() // deref()獲取指針實例對應的值 console.log(grade3 === grade3Pointer.deref()) // 在JS層並不是同一個對象 console.log(grade3['ref.buffer'].hexAddress() === grade3Pointer.deref()['ref.buffer'].hexAddress()) //但是實際上指向的是同一個內存地址,即所引用值是相同的
可以通過ref.alloc(Object|String type, ? value) → Buffer
直接得到一個引用對象
const iAgePointer = ref.alloc(ref.types.int, 18) // 初始化一個指向`int`類的指針,值為18 const grade3Pointer = ref.alloc(Grade) // 初始化一個指向`Grade`類的指針
回調函數
C的回調函數一般是用作入參傳入。
const ref = require('ref') const ffi = require('ffi') const testDLL = ffi.Library('./testDLL', { setCallback: ['int', [ ffi.Function(ref.types.void, // ffi.Function申明類型, 用`'pointer'`申明類型也可以 [ref.types.int, ref.types.CString])]] }) const uiInfocallback = ffi.Callback(ref.types.void, // ffi.callback返回函數實例 [ref.types.int, ref.types.CString], (resultCount, resultText) => { console.log(resultCount) console.log(resultText) }, ) const result = testDLL.uiInfocallback(uiInfocallback)
注意!如果你的CallBack是在setTimeout中調用,可能存在被GC的BUG
process.on('exit', () => {
/* eslint-disable-next-line */
uiInfocallback // keep reference avoid gc
})
代碼實例
舉個完整引用例子
// 頭文件 #pragma once //#include "../include/MacroDef.h" #define CertMaxNumber 10 typedef struct { int length[CertMaxNumber]; char CertGroundId[CertMaxNumber][2]; char CertDate[CertMaxNumber][2048]; } CertGroud; #define DLL_SAMPLE_API __declspec(dllexport) extern "C"{ //讀取證書 DLL_SAMPLE_API int My_ReadCert(char *pwd, CertGroud *data,int *iCertNumber); } const CertGroud = Struct({ certLen: RefArray(ref.types.int, 10), certId: RefArray(RefArray(ref.types.char, 2), 10), certData: RefArray(RefArray(ref.types.char, 2048), 10), curCrtID: RefArray(RefArray(ref.types.char, 12), 10), }) const dll = ffi.Library(path.join(staticPath, '/key.dll'), { My_ReadCert: ['int', ['string', ref.refType(CertGroud), ref.refType(ref.types.int)]], }) async function readCert({ ukeyPassword, certNum }) { return new Promise(async (resolve) => { // ukeyPassword為string類型, c中指代 char* ukeyPassword = ukeyPassword.toString() // 根據結構體類型 開辟一個新的內存空間 const certInfo = new CertGroud() // 開辟一個int 4字節內存空間 const _certNum = ref.alloc(ref.types.int) // certInfo.ref()作為certInfo的指針傳入 dll.My_ucRMydCert.async(ukeyPassword, certInfo.ref(), _certNum, () => { // 清除無效空字段