node-ffi使用指南

nodejs/elctron中,能夠經過node-ffi,經過Foreign Function Interface調用動態連接庫,俗稱調DLL,實現調用C/C++代碼,從而實現許多node很差實現的功能,或複用諸多已實現的函數功能。node

node-ffi是一個用於使用純JavaScript加載和調用動態庫的Node.js插件。它能夠用來在不編寫任何C ++代碼的狀況下建立與本地DLL庫的綁定。同時它負責處理跨JavaScript和C的類型轉換。git

Node.js Addons相比,此方法有以下優勢:github

1. 不須要源代碼。
2. 不須要每次重編譯`node`,`Node.js Addons`引用的`.node`會有文件鎖,會對`electron應用熱更新形成麻煩。
3. 不要求開發者編寫C代碼,可是仍要求開發者具備必定C的知識。
複製代碼

缺點是:shell

1. 性能有折損
2. 相似其餘語言的FFI調試,此方法近似黑盒調用,差錯比較困難。
複製代碼

安裝

node-ffi經過Buffer類,在C代碼和JS代碼之間實現了內存共享,類型轉換則是經過refref-arrayref-struct實現。因爲node-ffi/ref包含C原生代碼,因此安裝須要配置Node原生插件編譯環境。npm

// 管理員運行bash/cmd/powershell,不然會提示權限不足
npm install --global --production windows-build-tools
npm install -g node-gyp
複製代碼

根據須要安裝對應的庫json

npm install ffi
npm install ref
npm install ref-array
npm install ref-struct
複製代碼

若是是electron項目,則項目能夠安裝electron-rebuild插件,可以方便遍歷node-modules中全部須要rebuild的庫進行重編譯。windows

npm install electron-rebuild
複製代碼

在package.json中配置快捷方式api

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種基礎數據類型----整型 浮點型 指針 聚合類型bash

基礎

整型、字符型都有分有符號和無符號兩種。

類型 最小範圍
char 0 ~ 127
signed char -127 ~ 127
unsigned char 0 ~ 256

在不聲明unsigned時 默認爲signed型

refunsigned會縮寫成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 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轉碼後cstrBuffer類,可直接做爲看成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,會修改Bufferprototype,替換和注入一些方法,請參考文檔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, () => {
            // 清除無效空字段
            let cert = bufferTrim.trimEnd(new Buffer(certInfo.certData[certNum]))
            cert = cert.toString('binary')
            resolve(cert)
        })
    })
}
複製代碼

常見錯誤

  • Dynamic Linking Error: Win32 error 126

這個錯誤有三種緣由

  1. 一般是傳入的DLL路徑錯誤,找不到Dll文件,推薦使用絕對路徑。
  2. 若是是在x64的node/electron下引用32位的DLL,也會報這個錯,反之亦然。要確保DLL要求的CPU架構和你的運行環境相同。
  3. DLL還有引用其餘DLL文件,可是找不到引用的DLL文件,多是VC依賴庫或者多個DLL之間存在依賴關係。
  • Dynamic Linking Error: Win32 error 127:DLL中沒有找到對應名稱的函數,須要檢查頭文件定義的函數名是否與DLL調用時寫的函數名是否相同。

Path設置

若是你的DLL是多個並且存在相互調用問題,會出現Dynamic Linking Error: Win32 error 126錯誤3。這是因爲默認的進程Path是二進制文件所在目錄,即node.exe/electron.exe目錄而不是DLL所在目錄,致使找不到DLL同目錄下的其餘引用。能夠經過以下方法解決:

//方法一, 調用winapi SetDllDirectoryA設置目錄
const ffi = require('ffi')

const kernel32 = ffi.Library("kernel32", {
'SetDllDirectoryA': ["bool", ["string"]]
})
kernel32.SetDllDirectoryA("pathToAdd")

//方法二(推薦),設置Path環境環境
process.env.PATH = `${process.env.PATH}${path.delimiter}${pathToAdd}`
複製代碼

DLL分析工具

能夠查看DLL連接庫的全部信息、以及DLL依賴關係的工具,可是很遺憾不支持WIN10。若是你不是WIN10用戶,那麼你只須要這一個工具便可,下面工具能夠跳過。

能夠查看進程執行時候的各類操做,如IO、註冊表訪問等。這裏用它來監聽node/electron進程的IO操做,用於排查Dynamic Linking Error: Win32 error錯誤緣由3,能夠查看ffi.Libary時的全部IO請求和對應結果,查看缺乏了什麼DLL

dumpbin.exe爲Microsoft COFF二進制文件轉換器,它顯示有關通用對象文件格式(COFF)二進制文件的信息。可用使用dumpbin檢查COFF對象文件、標準COFF對象庫、可執行文件和動態連接庫等。 經過開始菜單 -> Visual Studio 20XX -> Visual Studio Tools -> VS20XX x86 Native Command Prompt啓動。

dumpbin /headers [dll路徑] // 返回DLL頭部信息,會說明是32 bit word Machine/64 bit word Machine
dumpbin /exports [dll路徑] // 返回DLL導出信息,name列表爲導出的函數名
複製代碼

閃崩問題

實際node-ffi調試的時候,很容易出現內存錯誤閃崩,甚至會出現斷點致使崩潰的狀況。這個是每每是由於非法內存訪問形成,能夠經過Windows日誌看到錯誤信息,可是相信我,那並無什麼用。C的內存差錯是否是一件簡單的事情。

附錄

自動轉換工具

tjfontaine大神提供了一個node-ffi-generate,能夠根據頭文件,自動生成node-ffi函數申明,注意這個須要Linux環境,簡單用KOA包了一層改爲了在線模式ffi-online,能夠丟到VPS中運行。

WINAPI

輪子

winapi存在大量的自定義的變量類型,waitingsong大俠的輪子 node-win32-api中完整翻譯了全套windef.h中的類型,並且這個項目採用TS來規定FFI的返回Interface,很值得借鑑。

注意!裏面的類型不必定都是對的,相信做者也沒有完整的測試過全部變量,實際使用中也遇到過裏面類型錯誤的坑。

GetLastError

簡單說node-ffi經過winapi來調用DLL,這致使GetLastError永遠返回0。最簡單方法就是本身寫個C++ addon來繞開這個問題。

參考Issue GetLastError() always 0 when using Win32 API 參考PR github.com/node-ffi/no…

PVOID返回空,即內存地址FFFFFFFF閃崩

winapi中,常常經過判斷返回的pvoid指針是否存在來判斷是否成功,可是在node-ffi中,對FFFFFFFF的內存地址deref()會形成程序閃崩。必須迂迴採用指針的指針類型進行特判

HDEVNOTIFY WINAPI RegisterDeviceNotificationA( _In_ HANDLE hRecipient, _In_ LPVOID NotificationFilter, _In_ DWORD Flags);

HDEVNOTIFY hDevNotify = RegisterDeviceNotificationA(hwnd, &notifyFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
if (!hDevNotify) {
	DWORD le = GetLastError();
	printf("RegisterDeviceNotificationA() failed [Error: %x]\r\n", le);
	return 1;
}
複製代碼
const apiDef = SetupDiGetClassDevsW: [W.PVOID_REF, [W.PVOID, W.PCTSTR, W.HWND, W.DWORD]] // 注意返回類型`W.PVOID_REF`必須設置成pointer,就是不設置type,則node-ffi不會嘗試`deref()`
const hDEVINFOPTR = this.setupapi.SetupDiGetClassDevsW(null, typeBuffer, null,
    setupapiConst.DIGCF_PRESENT | setupapiConst.DIGCF_ALLCLASSES
)
const hDEVINFO = winapi.utils.getPtrValue(hDEVINFOPTR, W.PVOID) // getPtrValue特判,若是地址爲全`FF`則返回空
if (!hDEVINFO) {
    throw new ErrorWithCode(ErrorType.DEVICE_LIST_ERROR, ErrorCode.GET_HDEVINFO_FAIL)
}
複製代碼
相關文章
相關標籤/搜索