node-ffi使用指南


 

nodejs/elctron中,可以通過node-ffi,通過Foreign Function Interface調用動態鏈接庫,俗稱調DLL,實現調用C/C++代碼,從而實現許多node不好實現的功能,或復用諸多已實現的函數功能。

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

Node.js Addons相比,此方法有如下優點:

  1.  
    1. 不需要源代碼。
  2.  
    2. 不需要每次重編譯`node`,`Node.js Addons`引用的`.node`會有文件鎖,會對`electron應用熱更新造成麻煩。
  3.  
    3. 不要求開發者編寫C代碼,但是仍要求開發者具有一定C的知識。
  4.  
    復制代碼

缺點是:

  1.  
    1. 性能有折損
  2.  
    2. 類似其他語言的FFI調試,此方法近似黑盒調用,差錯比較困難。
  3.  
    復制代碼

安裝

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

  1.  
    // 管理員運行bash/cmd/powershell,否則會提示權限不足
  2.  
    npm install -- global --production windows-build-tools
  3.  
    npm install -g node-gyp
  4.  
    復制代碼

根據需要安裝對應的庫

  1.  
    npm install ffi
  2.  
    npm install ref
  3.  
    npm install ref-array
  4.  
    npm install ref-struct
  5.  
    復制代碼

如果是electron項目,則項目可以安裝electron-rebuild插件,能夠方便遍歷node-modules中所有需要rebuild的庫進行重編譯。

  1.  
    npm install electron-rebuild
  2.  
    復制代碼

在package.json中配置快捷方式

  1.  
    package.json
  2.  
    "scripts": {
  3.  
    "rebuild": "cd ./node_modules/.bin && electron-rebuild --force --module-dir=../../"
  4.  
    }
  5.  
    復制代碼

之后執行npm run rebuild 操作即可完成electron的重編譯。

簡單范例

  1.  
    extern "C" int __declspec(dllexport)My_Test(char *a, int b, int c);
  2.  
    extern "C" void __declspec(dllexport)My_Hello(char *a, int b, int c);
  3.  
    復制代碼
  1.  
    import ffi from 'ffi'
  2.  
    // `ffi.Library`用於注冊函數,第一個入參為DLL路徑,最好為文件絕對路徑
  3.  
    const dll = ffi.Library( './test.dll', {
  4.  
    // My_Test是dll中定義的函數,兩者名稱需要一致
  5.  
    // [a, [b,c....]] a是函數出參類型,[b,c]是dll函數的入參類型
  6.  
    My_Test: ['int', ['string', 'int', 'int']], // 可以用文本表示類型
  7.  
    My_Hello: [ref.types.void, ['string', ref.types.int, ref.types.int]] // 更推薦用`ref.types.xx`表示類型,方便類型檢查,`char*`的特殊縮寫下文會說明
  8.  
    })
  9.  
     
  10.  
    //同步調用
  11.  
    const result = dll.My_Test('hello', 3, 2)
  12.  
     
  13.  
    //異步調用
  14.  
    dll. My_Test.async('hello', 3, 2, (err, result) => {
  15.  
    if(err) {
  16.  
    //todo
  17.  
    }
  18.  
    return result
  19.  
    })
  20.  
    復制代碼

變量類型

C語言中有4種基礎數據類型----整型 浮點型 指針 聚合類型

基礎

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

類型 最小范圍
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庫。

  1.  
    const ref = require('ref')
  2.  
    const refArray = require('ref-array')
  3.  
     
  4.  
    const CharArray100 = refArray(ref.types.char, 100) // 申明char[100]類型CharArray100
  5.  
    const bufferValue = Buffer.from('Hello World') // Hello World轉換Buffer
  6.  
    // 通過Buffer循環復制, 比較啰嗦
  7.  
    const value1 = new CharArray100()
  8.  
    for (let i = 0, l = bufferValue.length; i < l; i++) {
  9.  
    value1[i] = bufferValue[i]
  10.  
    }
  11.  
    // 使用ref.alloc初始化類型
  12.  
    const strArray = [...bufferValue] //需要將`Buffer`轉換成`Array`
  13.  
    const value2 = ref.alloc(CharArray100, strArray)
  14.  
    復制代碼

在傳遞中文字符型時,必須預先得知DLL庫的編碼方式。node默認使用UTF-8編碼。若DLL不為UTF-8編碼則需要轉碼,推薦使用iconv-lite

  1.  
    npm install iconv-lite
  2.  
    復制代碼
  1.  
    const iconv = require('iconv-lite')
  2.  
    const cstr = iconv.encode(str, 'gbk')
  3.  
    復制代碼

注意!使用encode轉碼后cstrBuffer類,可直接作為當作uchar類型

iconv.encode(str.'gbk')中gbk默認使用的是unsigned char | 0 ~ 256儲存。假如C代碼需要的是signed char | -127 ~ 127,則需要將buffer中的數據使用int8類型轉換。

  1.  
    const Cstring100 = refArray(ref.types.char, 100)
  2.  
    const cString = new Cstring100()
  3.  
    const uCstr = iconv.encode('農企葯丸', 'gbk')
  4.  
    for (let i = 0; i < uCstr.length; i++) {
  5.  
    cString[i] = uCstr. readInt8(i)
  6.  
    }
  7.  
    復制代碼

C代碼為字符數組char[]/char *設置的返回值,通常返回的文本並不是定長,不會完全使用預分配的空間,末尾則會是無用的值。如果是預初始化的值,一般末尾是一大串的0x00,需要手動做trimEnd,如果不是預初始化的值,則末尾不定值,需要C代碼明確返回字符串數組的長度returnValueLength

內置簡寫

ffi中內置了一些簡寫

  1.  
    ref.types.int => 'int'
  2.  
    ref.refType( 'int') => 'int*'
  3.  
    char* => 'string'
  4.  
    復制代碼

只建議使用'string'。

字符串雖然在js中被認為是基本類型,但在C語言中是以對象的形式來表示的,所以被認為是引用類型。所以string其實是char* 而不是char

聚合類型

多維數組

遇到定義為多維數組的基本類型 則需要使用ref-array進行創建

  1.  
    char cName[50][100] // 創建一個cName變量儲存級50個最大長度為100的名字
  2.  
    復制代碼
  1.  
    const ref = require('ref')
  2.  
    const refArray = require('ref-array')
  3.  
     
  4.  
    const CName = refArray(refArray(ref.types.char, 100), 50)
  5.  
    const cName = new CName()
  6.  
    復制代碼

結構體

結構體是C中常用的類型,需要用到ref-struct進行創建

  1.  
    typedef struct {
  2.  
    char cTMycher[100];
  3.  
    int iAge[50];
  4.  
    char cName[50][100];
  5.  
    int iNo;
  6.  
    } Class;
  7.  
     
  8.  
    typedef struct {
  9.  
    Class class[4];
  10.  
    } Grade;
  11.  
    復制代碼
  1.  
    const ref = require('ref')
  2.  
    const Struct = require('ref-struct')
  3.  
    const refArray = require('ref-array')
  4.  
     
  5.  
    const Class = Struct({ // 注意返回的`Class`是一個類型
  6.  
    cTMycher: RefArray(ref.types.char, 100),
  7.  
    iAge: RefArray(ref.types.int, 50),
  8.  
    cName: RefArray(RefArray(ref.types.char, 100), 50)
  9.  
    })
  10.  
    const Grade = Struct({ // 注意返回的`Grade`是一個類型
  11.  
    class: RefArray(Class, 4)
  12.  
    })
  13.  
    const grade3 = new Grade() // 新建實例
  14.  
    復制代碼

指針

指針是一個變量,其值為實際變量的地址,即內存位置的直接地址,有些類似於JS中的引用對象。

C語言中使用*來代表指針

例如 int a* 則就是 整數型a變量的指針 , &用於表示取地址

  1.  
    int a=10,
  2.  
    int *p; // 定義一個指向整數型的指針`p`
  3.  
    p=&a // 將變量`a`的地址賦予`p`,即`p`指向`a`
  4.  
    復制代碼

node-ffi實現指針的原理是借助ref,使用Buffer類在C代碼和JS代碼之間實現了內存共享,讓Buffer成為了C語言當中的指針。注意,一旦引用ref,會修改Bufferprototype,替換和注入一些方法,請參考文檔ref文檔

  1.  
    const buf = new Buffer(4) // 初始化一個無類型的指針
  2.  
    buf. writeInt32LE(12345, 0) // 寫入值12345
  3.  
     
  4.  
    console.log(buf.hexAddress()) // 獲取地址hexAddress
  5.  
     
  6.  
    buf. type = ref.types.int // 設置buf對應的C類型,可以通過修改`type`來實現C的強制類型轉換
  7.  
    console.log(buf.deref()) // deref()獲取值12345
  8.  
     
  9.  
    const pointer = buf.ref() // 獲取指針的指針,類型為`int **`
  10.  
     
  11.  
    console.log(pointer.deref().deref()) // deref()兩次獲取值12345
  12.  
    復制代碼

要明確一下兩個概念 一個是結構類型,一個是指針類型,通過代碼來說明。

  1.  
    // 申明一個類的實例
  2.  
    const grade3 = new Grade() // Grade 是結構類型
  3.  
    // 結構類型對應的指針類型
  4.  
    const GradePointer = ref.refType(Grade) // 結構類型`Grade`對應的指針的類型,即指向Grade
  5.  
    // 獲取指向grade3的指針實例
  6.  
    const grade3Pointer = grade3.ref()
  7.  
    // deref()獲取指針實例對應的值
  8.  
    console.log(grade3 === grade3Pointer.deref()) // 在JS層並不是同一個對象
  9.  
    console.log(grade3['ref.buffer'].hexAddress() === grade3Pointer.deref()['ref.buffer'].hexAddress()) //但是實際上指向的是同一個內存地址,即所引用值是相同的
  10.  
    復制代碼

可以通過ref.alloc(Object|String type, ? value) → Buffer直接得到一個引用對象

  1.  
    const iAgePointer = ref.alloc(ref.types.int, 18) // 初始化一個指向`int`類的指針,值為18
  2.  
    const grade3Pointer = ref.alloc(Grade) // 初始化一個指向`Grade`類的指針
  3.  
    復制代碼

回調函數

C的回調函數一般是用作入參傳入。

  1.  
    const ref = require('ref')
  2.  
    const ffi = require('ffi')
  3.  
     
  4.  
    const testDLL = ffi.Library('./testDLL', {
  5.  
    setCallback: ['int', [
  6.  
    ffi. Function(ref.types.void, // ffi.Function申明類型, 用`'pointer'`申明類型也可以
  7.  
    [ref. types.int, ref.types.CString])]]
  8.  
    })
  9.  
     
  10.  
     
  11.  
    const uiInfocallback = ffi.Callback(ref.types.void, // ffi.callback返回函數實例
  12.  
    [ref. types.int, ref.types.CString],
  13.  
    (resultCount, resultText) => {
  14.  
    console.log(resultCount)
  15.  
    console.log(resultText)
  16.  
    },
  17.  
    )
  18.  
     
  19.  
    const result = testDLL.uiInfocallback(uiInfocallback)
  20.  
    復制代碼

注意!如果你的CallBack是在setTimeout中調用,可能存在被GC的BUG

  1.  
    process.on( 'exit', () => {
  2.  
    /* eslint-disable-next-line */
  3.  
    uiInfocallback // keep reference avoid gc
  4.  
    })
  5.  
    復制代碼

代碼實例

舉個完整引用例子

  1.  
    // 頭文件
  2.  
    #pragma once
  3.  
     
  4.  
    //#include "../include/MacroDef.h"
  5.  
    #define CertMaxNumber 10
  6.  
    typedef struct {
  7.  
    int length[CertMaxNumber];
  8.  
    char CertGroundId[CertMaxNumber][2];
  9.  
    char CertDate[CertMaxNumber][2048];
  10.  
    } CertGroud;
  11.  
     
  12.  
    #define DLL_SAMPLE_API __declspec(dllexport)
  13.  
     
  14.  
    extern "C"{
  15.  
     
  16.  
    //讀取證書
  17.  
    DLL_SAMPLE_API int My_ReadCert(char *pwd, CertGroud *data,int *iCertNumber);
  18.  
    }
  19.  
    復制代碼
  1.  
    const CertGroud = Struct({
  2.  
    certLen: RefArray(ref.types.int, 10),
  3.  
    certId: RefArray(RefArray(ref.types.char, 2), 10),
  4.  
    certData: RefArray(RefArray(ref.types.char, 2048), 10),
  5.  
    curCrtID: RefArray(RefArray(ref.types.char, 12), 10),
  6.  
    })
  7.  
     
  8.  
    const dll = ffi.Library(path.join(staticPath, '/key.dll'), {
  9.  
    My_ReadCert: ['int', ['string', ref.refType(CertGroud), ref.refType(ref.types.int)]],
  10.  
    })
  11.  
     
  12.  
    async function readCert({ ukeyPassword, certNum }) {
  13.  
    return new Promise(async (resolve) => {
  14.  
    // ukeyPassword為string類型, c中指代 char*
  15.  
    ukeyPassword = ukeyPassword. toString()
  16.  
    // 根據結構體類型 開辟一個新的內存空間
  17.  
    const certInfo = new CertGroud()
  18.  
    // 開辟一個int 4字節內存空間
  19.  
    const _certNum = ref.alloc(ref.types.int)
  20.  
    // certInfo.ref()作為certInfo的指針傳入
  21.  
    dll. My_ucRMydCert.async(ukeyPassword, certInfo.ref(), _certNum, () => {
  22.  
    // 清除無效空字段
  23.  
    let cert = bufferTrim.trimEnd(new Buffer(certInfo.certData[certNum]))
  24.  
    cert = cert. toString('binary')
  25.  
    resolve(cert)
  26.  
    })
  27.  
    })
  28.  
    }
  29.  
    復制代碼

常見錯誤

  • 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同目錄下的其他引用。可以通過如下方法解決:

  1.  
    //方法一, 調用winapi SetDllDirectoryA設置目錄
  2.  
    const ffi = require('ffi')
  3.  
     
  4.  
    const kernel32 = ffi.Library("kernel32", {
  5.  
    'SetDllDirectoryA': ["bool", ["string"]]
  6.  
    })
  7.  
    kernel32. SetDllDirectoryA("pathToAdd")
  8.  
     
  9.  
    //方法二(推薦),設置Path環境環境
  10.  
    process. env.PATH = `${process.env.PATH}${path.delimiter}${pathToAdd}`
  11.  
    復制代碼

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啟動。

  1.  
    dumpbin /headers [dll路徑] // 返回DLL頭部信息,會說明是32 bit word Machine/64 bit word Machine
  2.  
    dumpbin /exports [dll路徑] // 返回DLL導出信息,name列表為導出的函數名
  3.  
    復制代碼

閃崩問題

實際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()會造成程序閃崩。必須迂回采用指針的指針類型進行特判

  1.  
    HDEVNOTIFY
  2.  
    WINAPI
  3.  
    RegisterDeviceNotificationA(
  4.  
    _In_ HANDLE hRecipient,
  5.  
    _In_ LPVOID NotificationFilter,
  6.  
    _In_ DWORD Flags);
  7.  
     
  8.  
    HDEVNOTIFY hDevNotify = RegisterDeviceNotificationA(hwnd, &notifyFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
  9.  
    if (!hDevNotify) {
  10.  
    DWORD le = GetLastError();
  11.  
    printf("RegisterDeviceNotificationA() failed [Error: %x]\r\n", le);
  12.  
    return 1;
  13.  
    }
  14.  
    復制代碼
  1.  
    const apiDef = SetupDiGetClassDevsW: [W.PVOID_REF, [W.PVOID, W.PCTSTR, W.HWND, W.DWORD]] // 注意返回類型`W.PVOID_REF`必須設置成pointer,就是不設置type,則node-ffi不會嘗試`deref()`
  2.  
    const hDEVINFOPTR = this.setupapi.SetupDiGetClassDevsW(null, typeBuffer, null,
  3.  
    setupapiConst. DIGCF_PRESENT | setupapiConst.DIGCF_ALLCLASSES
  4.  
    )
  5.  
    const hDEVINFO = winapi.utils.getPtrValue(hDEVINFOPTR, W.PVOID) // getPtrValue特判,如果地址為全`FF`則返回空
  6.  
    if (!hDEVINFO) {
  7.  
    throw new ErrorWithCode(ErrorType.DEVICE_LIST_ERROR, ErrorCode.GET_HDEVINFO_FAIL)
  8.  
    }
  9.  
    復制代碼

轉載於:https://juejin.im/post/5b58038d5188251b186bc902

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM