天天看點

c語言調用dll别名,Node.js調用dll動态連結庫

很神奇我自己寫的第一個Nodejs的小程式就直接上手動态連結庫的調用了,也是不容易。

起因是需要調用CUDA套件裡的nvml.dll中的API來讀取顯示卡資訊,其中函數的參數會涉及基本類型、指針、結構體指針、指針的指針。對于一個Node初學者來說,花了兩天的業餘時間搞明白也是不容易。

子產品介紹

首先是在Node中調用dll,這裡的dll似乎隻能是滿足C規範的(C++的dll因為涉及名字空間、重載等問題,還沒搞懂會怎樣變化)(好在nvml.h的頭檔案中可以看出來結構和函數都是包在extern "C"{}裡面的)。調用C語言程式需要用到FFI(Foreign function interface),它為動态連結庫提供了一個很簡潔的接口,而FFI是基于node-gyp的,這樣使用有一個很不舒服之處是發生錯誤時隻會報node-gyp編譯錯誤,沒有其他有效資訊,但是對于小的調用,應該也足夠了。

其次,為了正确處理指針類型,需要引入ref子產品,它是對Buffer一個良好封裝。ffi自身提供了一些預設的類型,如int,pointer,char,string等,基本的C語言類型都已經包括了,其中pointer對應的是void*,而string是char*的别名。ref本身提供了一個非常漂亮的文檔,裡面介紹了内置的類型。并且可以通過Buffer來擴充(當然對于結構體和數組的擴充已經有很好的實作了就不需要在小場景裡重新造了)。ref提供了ref.ref(var)将var轉換為引用(指針),ref.deref(var)将var解引用。與之相應,對類型提供了ref.refType(type)和ref.derefType(type),當然對于内置類型可以用字元串描述的,直接加星号*就足夠了。對于已有的Buffer,提供了大量的read*函數用來輔助讀取。

ref-struct和ref-array為ref提供了結構體和數組的類型擴充,它們可以友善地生成ref的類型格式,直接用在ffi調用中或者用ref來取得引用。

配置

安裝node-gyp,出于個人喜好,我覺得将它安裝到全局比較好,以後别的地方很有可能用到

npm install node-gyp --global

安裝ffi及引用類型庫

npm install ffi ref ref-struct ref-array --save

在nodejs中使用(下文會繼續使用這幾個變量名)

const ffi = require('ffi'),

ref = require('ref'),

struct = require('ref-struct'),

arrayType = require('ref-array');

包裝調用

使用ffi的Library函數可以将dll中的函數load到js裡,文法是

ffi.Library(libname, {

foo: [returnType, argTypes]

});

其中'libname'是庫調用時的對象名,foo是函數名,returnType是傳回值類型,既可以是ref支援的類型字元串如int等,也可以是類型對象。實際上内部會先用ref.coerceType(type)方法将字元串也轉換成類型對象。類型對象是指如ref.types中定義的一系列對象(參見ref文檔),或者ref-struct/ref-array生成的類型。argTypes是類型數組,表示函數的形參清單。

基本類型

基本類型已經定義在了ref.types裡,可以直接使用類型字元串或者'ref.types'中的類型對象。而對于枚舉類型,簡單當作int或者uint處理就可以了。

指針

對于指針類型,簡單類型可以直接在參數清單裡寫int *,pointer這樣的,生成類型的指針可以用ref.refType(type)。在調用函數時,執行var vRef = ref.alloc(type)得到一個引用對象,将對象傳入函數即可。需要擷取vRef指向的值v可以用var v = ref.deref(vRef)。對字元串類型的buffer可能需要使用ref.readCString(buffer)來讀取(它可以處理末尾的'\0')。

結構體

對于結構體類型,首先要使用ref-struct定義出來結構類型,文法

var foo_t = struct({

'bar': type

});

其中foo為結構體名,bar為成員變量名,type為變量類型,與ffi中的函數參數可以取類似的值。在使用時,直接将結建構立出來var foo = new foo_t();,那麼foo中存儲的是之前所定義的那些變量(如bar),以及一個Buffer對象。它的引用就直接用foo.ref()得到。

數組

對于數組類型,用ref-array定義出來類型,文法

var arr_t = arrayType(type, length);

其中type如前,而length是這個數組的長度(固定)。以後将之當作一個正常的ref子產品支援的類型使用就好了。它的資料是存儲在内部的buffer成員裡。

應用

我是個不擅長寫例子說明的人,是以,就直接使用我在調用nvml時的代碼來作執行個體吧。

首先是nvml.h中我所用到的一部分結構和函數的定義。我會在node中調用它們以擷取顯示卡的資訊。

資料結構

typedef struct nvmlDevice_st* nvmlDevice_t;

#define NVML_DEVICE_PCI_BUS_ID_BUFFER_SIZE 16

typedef struct nvmlPciInfo_st {

char busId[NVML_DEVICE_PCI_BUS_ID_BUFFER_SIZE];

unsigned int domain;

unsigned int bus;

unsigned int device;

unsigned int pciDeviceId;

unsigned int pciSubSystemId;

unsigned int reserved0;

unsigned int reserved1;

unsigned int reserved2;

unsigned int reserved3;

} nvmlPciInfo_t;

函數

nvmlReturn_t DECLDIR nvmlInit(void);

nvmlReturn_t DECLDIR nvmlDeviceGetCount(unsigned int *deviceCount);

nvmlReturn_t DECLDIR nvmlDeviceGetHandleByIndex(unsigned int index, nvmlDevice_t *device);

nvmlReturn_t DECLDIR nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length);

nvmlReturn_t DECLDIR nvmlDeviceGetPciInfo(nvmlDevice_t device, nvmlPciInfo_t *pci);

其中nvmlReturn_t是枚舉類型的error code,DECLDIR是一個dllexport的宏注明這是dll的導出函數。注意到nvmlDevice_t隻是一個nvmlDevice_st指針,但是沒有給出nvmlDevice_st的具體實作,因為它隻是一個裝置句柄,該結構體的真實内容其實并不需要知道。以上幾個函數基本就包含了我對ffi的用法的認知。

庫定義

将所有庫函數定義在一個ffi.Library生成的對象裡

let libnvml = ffi.Library('nvml.dll', {

'nvmlInit': ['int', []],

'nvmlDeviceGetCount': ['int', ['int *']],

'nvmlDeviceGetHandleByIndex': ['int', ['uint', ref.refType('pointer')]],

'nvmlDeviceGetName': ['int', ['pointer', 'char *', 'uint']],

'nvmlDeviceGetPciInfo': ['int', ['pointer', 'ref.refType(nvmlPciInfo_t)]]

});

無參數函數

如nvmlInit(void)這樣的,沒有輸入參數,是以在ffi中的定義為

'nvmlInit': ['int', []]

調用時直接執行

let err = libnvml.nvmlInit();

// check error code and do sth...

簡單參數函數

使用基本類型作為參數的函數,這個應用中沒有使用到,簡單舉個例子如:

let foobar = ffi.Library({

'foo': ['int', ['int']]

});

調用就當成普通函數調用就是。

let x = 0;

let y = foobar.foo(x);

含指針參數的函數

為了傳回多參數,C語言中常将要傳回的位址傳入,直接在函數中修改。這裡如nvmlDeviceGetCount(unsigned int *deviceCount),它的傳回值是error code,而将顯示卡的個數寫在*deviceCount裡。

在lib對象的聲明時使用

'nvmlDeviceGetCount': ['int', ['int *']]

這裡的'int*'用字元串表示了類型,也可以用ref.refType('int')或者ref.refType(ref.types.int),當然用類型字元串是最簡潔的了,可以看出ref的細膩用心。

調用時需要先構造出一個整數對象的引用,傳入函數,最後解引用取出值。

let deviceCountRef = ref.alloc(ref.types.int); // or ref.alloc('int')

if (libnvml.nvmlDeviceGetCount(deviceCountRef) == 0) { // 0 is success

let deviceCount = ref.deref(deviceCountRef);

// use deviceCount to do sth...

}

含指針(無類型)參數的函數

在有的時候,C指針隻是用來作為一個傳到其他函數的句柄,那麼可以把它當作是無類型的,類似于C語言中的void*,這一點使用ref的pointer類型很容易做到。

如nvmlDeviceGetHandleByIndex(unsigned int index, nvmlDevice_t *device),它的參數device隻會用于傳給其他函數,是以我們無需知道nvmlDevice_t到底是什麼類型的指針(注意它是個指針類型),就可以當作一個void*也即這裡的pointer來處理。

'nvmlDeviceGetHandleByIndex': ['int', ['uint', ref.refType('pointer')]]

這裡需要注意的是,是将nvmlDevice_t當作了無類型指針pointer,而不是将nvmlDevice_t *當作了pointer,是以在使用nvmlDevice_t的指針時,需要用ref.refType('pointer')将它變成指向指針的指針(引用)類型才可以

在調用的時候先定義一個空指針,将它傳入函數中,傳回後對其解引用即可得到我們真正想要的那個句柄(無類型指針)。

let deviceRef = ref.NULL_POINTER;

if (libnvml.nvmlDeviceGetHandleByIndex(deviceId, deviceRef) == 0) {

let deviceHandle = ref.deref(deviceRef);

// do sth. with deviceHandle

}

對字元串進行操作的函數

當函數需要傳入字元串并對其進行操作時,如果隻是傳入使用(如const char*),那麼直接傳入字元串應該就可以Buffer對象,對其進行操作然後讀取出來了。如nvmlDeviceGetName(nvmlDevice_t device, char *name, unsigned int length)将顯示卡名寫入傳入的name字元串裡,最大長度也作為參數傳入。(注意到這裡用到了上文擷取的nvmlDevice_t對象,并且是直接作為參數傳入的)

在定義函數時使用"char*"、"string"和ref.types.CString都可以。如這裡

'nvmlDeviceGetName': ['int', ['pointer', 'char*', 'uint']]

調用時先構造一個一定大小的Buffer,操作完後讀取出來

const MaxStringLength = 128;

let nameBuffer = new Buffer(MaxStringLength);

if (libnvml.nvmlDeviceGetName(deviceHandle, nameBuffer, nameBuffer.length)) {

let name = ref.readCString(nameBuffer, 0);

// do sth. with name

}

有的時候Buffer還需要做一下字元編碼的轉換,那就需要去查相應的文檔了,這裡略過。

含結構體參數的函數

這也是寫這篇博文的初衷,因為能找到的資料大都是簡單給出了基本類型的例子,或者加個指針或者加個結構體,但都是比較簡略的。這裡由于我用到的結構體内部包含了一個字元數組,是以是值得記錄的東西。

直接看pciInfo的例子(類型定義見上文),首先busId是一個定長字元串,需要用ref-array定義出來類型:new arrayType('char', 16)。其次利用ref-struct定義出結構體來

const nvmlPciInfo_t = struct({

'busId': new arrayType('char', 16),

'domain': 'uint',

'bus': 'uint',

'device': 'uint',

'pciDeviceId': 'uint',

'pciSubSystemId': 'uint',

'reserve0': 'uint',

'reserve1': 'uint',

'reserve2': 'uint',

'reserve3': 'uint'

});

在聲明函數時用到這一類型的引用

'nvmlDeviceGetPciInfo': ['int', ['pointer', ref.refType(nvmlPciInfo_t)]]

調用時先建立出具體對象,然後将其引用傳入函數即可

let info = new nvmlPciInfo_t();

if (libnvml.nvmlDeviceGetPciInfo(deviceHandle, info.ref())) {

// do sth. with info

}

在使用這個結構體時,直接讀取對應元素即可,而對字元數組,需要用readCString讀取數組的buffer成員。如下

let busId = ref.readCString(info.busId); // 讀取字元串

let bus = info.bus; // 讀取數值成員

結語

這篇文章主要是記錄我在學習用NodeJS調用C動态連結庫時學到的知識,基本參考自nodejs調用dll/so檔案的方法(了解到了ffi)、nodejs 如何使用 通過ffi調用傳回來的記憶體指針(了解到了ref-struct)、ref文檔以及ref-struct和ref-array(我也忘了怎麼搜到的,也許是StackOverflow)的源碼。

懶得寫例子來叙述,我基本上對于庫都是看個大概上手就用的,是以這篇文章中夾雜了我具體的應用場景,還好例子也是比較簡略,隻是可能會忽略掉某些用法或者使用了一些未必很有代表性的例子(比如無類型指針的使用:joy:)。

PS: 聽說有個叫swig的東西也能做到C語言到其他語言子產品的轉換,以前在用python時看到過,沒太注意,現在剛開始學NodeJS,也沒必要什麼都學,FFI暫時夠用了。

這裡的對象引用應該也可以直接用ref.alloc(type)這種方式構造出來,然後ref.deref()獲得内容,沒有試驗 ↩