通過ffi在node.js中調用動態鏈接庫(.so/.dll文件)


 

概述

為什么要在node.js中調用動態鏈接庫

  1. 由於騰訊體系下的許多公共的后台服務(L5, CKV, msgQ等)已經有了非常成熟的C/C++編寫的API,以供應用程序調用,node.js作為在公司內新興的后台runtime在調用這些公共服務的時候沒必要再造一遍輪子,而是可以將這些API編譯成.so文件直接使用。
  2. 對於一些密集計算型的任務可以由C++編寫好模塊,生成.so文件后由node.js調用。

ffi簡介與安裝

我們使用node-ffi來幫助我們調用動態鏈接庫。

FFI的全稱是Foreign Function Interface,該項目生來就是解決NodeJS的本地調用問題的,其流程就相當於Windows下的LoadLibrary()和GetProcAddress(),亦可以理解為NodeJS下的平台調用。為了調用一個小小的本地函數而創建一個addon實在是有點過頭了,這個時候,FFI這把殺雞刀就順手得多了。有了它,本地調用變得異常簡單,因為它在NodeJS環境中為JavaScript提供了一套強大的工具集用來調用動態鏈接庫。

notice: 本人的node使用環境是64bit的Linux系統。 安裝ffi:

  1. 全局或局部安裝node-gyp: npm install -g node-gyp,裝之前要安裝python 2.7,而node-gyp不支持Python 3.x,所以安裝了多個版本Python的讀者得留意一下自己當前的Python版本了。Linux下pythonbrew一鍵搞定,Windows下還得去改環境變量。並且,如果你使用的node.js版本是4.0+,node-gyp的安裝依賴支持C++11語法的gcc,你需要確定當前環境的gcc版本至少高於4.8。
  2. 安裝ffi:npm install ffi

    注意事項!

  3. ffi只能調用C風格的模塊。
  4. 需要將C源碼build成動態鏈接庫以供調用,在Linux下將C源碼build成.so文件,在windows下build成.dll文件。本文只闡述.so文件的調用方法,調用.dll差別不大。
  5. 在Linux下如果使用C++編寫的addon來調用.so文件,需要將.so文件為系統共享。具體方法可以參看ldconfig命令,這是一個Linux下的動態鏈接庫管理命令。ldconfig命令的主要用途是在默認搜尋目錄(/lib和/usr/lib)以及動態庫配置文件/etc/ld.so.conf內所列的目錄下,搜索出可共享的動態鏈接庫(格式如lib.so),進而創建出動態裝入程序(ld.so)所需的連接和緩存文件。緩存文件默認為 /etc/ld.so.cache,此文件保存已排好序的動態鏈接庫名字列表。ldconfig通常在系統啟動時運行,而當用戶安裝了一個新的動態鏈接庫時,就需要手工運行這個命令。

煎蛋栗子

這里就不演示利用node-gyp將.cc文件生成.node文件了,一般我都是找后台同學幫我把C源碼文件編譯成.so文件,然后直接拿過來用!哈哈哈! 這個栗子是nodejs調用C接口發送短信,這個C的API也非常簡單:

int send_msg(char * phone, char * content) 

參數是手機號和短信內容,類型都是char *,返回的retcode是一個整型,返回0就代表發送成功,其他就是失敗,方法名是send_msg。下面是如果利用ffi在nodejs中調用這個接口,該接口的源碼已經被封裝成libsend_msg.so這個動態鏈接庫了,我們直接調用就好。

'use strict' /** * 短信下發服務模塊 * 由於項目是使用node 5.0+,所以安裝node-ffi模塊需要依賴gcc 4.8+以上版本 */ var ffi = require('ffi'); // int send_msg(char * phone, char * content) var libm = ffi.Library(__dirname + '/msgQ/libsend_msg', { 'send_msg': ['int', ['string', 'string']] }); let smsExport = { sendMsg(opt) { let phone = opt.phone; let content = opt.text; // 調用c接口,發送短信 let retcode = libm.send_msg(phone, content); if (retcode === 0) { // TODO succ } else { // TODO fail } } }; module.exports = smsExport; 

可以看到,在使用ffi調用C接口傳參時,C的char *類型在nodejs源碼中可以直接用string類型表示,而對於nodejs沒有的int類型,我們也可以直接寫成int。並且可以看出來,這里我們使用同步的方式調用send_msg方法的。

獲取C接口的指針內容

上面這個栗子非常簡單,主要是簡單在傳參和出參的類型。由於javascript和C這兩種語言的基本類型並不能完全對齊,所以有時候在調用的時候,對於傳參出參的處理比較麻煩。經常遇到的一個問題就是如何在JS中針對C的指針類型進行操作。 例如有5個C接口如下:

double do_some_number_fudging(double a, int b); myobj * create_object(); double do_stuff_with_object(myobj *obj); void use_string_with_object(myobj *obj, char *value); void delete_object(myobj *obj); 

可以看到這些接口,有的方法的出參是一個指向object類型的指針,有的入參是一個指向object類型的指針,如果使用C語言調用這5個接口,可能會是這樣:

#include "mylibrary.h" int main() { myobj *fun_object; double res, fun; res = do_some_number_fudging(1.5, 5); fun_object = create_object(); if (fun_object == NULL) { printf("Oh no! Couldn't create object!\n"); exit(2); } use_string_with_object(fun_object, "Hello World!"); fun = do_stuff_with_object(fun_object); delete_object(fun_object); } 

那用JS如何調用這些接口呢?我們先使用ffi來包裝一下這些接口:

var ref = require("ref"); var ffi = require("ffi"); // typedefs var myobj = ref.types.void // 僅僅只是用來演示如何用ref創建C語言中的類型,由於我們這里不知道myobj將來會是啥類型,所以先定義成void類型 var MyLibrary = ffi.Library('libmylibrary', { "do_some_number_fudging": [ 'double', [ 'double', 'int' ] ], "create_object": [ "pointer", [] ], "do_stuff_with_object": [ "double", [ "pointer" ] ], "use_string_with_object": [ "void", [ "pointer", "string" ] ], "delete_object": [ "void", [ "pointer" ] ] }); 

好啦,下面編寫JS代碼來調用這些接口:

var res = MyLibrary.do_some_number_fudging(1.5, 5); // 單純的計算 var fun_object = MyLibrary.create_object(); // 調用接口,創建一個指向object類型的指針 if (fun_object.isNull()) { console.log("Oh no! Couldn't create object!\n"); } else { // 將剛剛創建的指針作為入參傳入其他方法。 MyLibrary.use_string_with_object(fun_object, "Hello World!"); var fun = MyLibrary.do_stuff_with_object(fun_object); MyLibrary.delete_object(fun_object); // 使用完之后記得調用C接口釋放指針指向的內存 } 

有時候,有時候,我會相信一切有盡頭,相愛分離都有時候,沒有什么會永垂不朽。。。 不對,跑偏了。有時候,我們會把一個指針作為入參傳給一個C接口,接口方法執行完之后會給這個指針指向的內存地址賦值,那么我們如何把這個值取出來呢?下面給出一個栗子。 C接口:

void* xyz_create(int id, unsigned int network_timeout_ms, float something_timeout_s); int xyz_get(int id, void *obj, const char *key, char **val, int *something); int xyz_set(int id, void *obj, const char *key, const char *val, int *something); void xyz_destroy(void *obj); void xyz_free(char *p); 

ffi包裝C接口生成的動態鏈接庫,並使用ref進行一些類型映射。

'use strict' const ref = require("ref"); const ffi = require("ffi"); // 生成兼容C的指向string類型的指針,即char** let stringPointer = ref.refType(ref.types.CString); let libxyz = ffi.Library(__dirname + '/so/example.so', { 'xyz_create': ['pointer', ['int', 'uint', 'float']], 'xyz_get': ['int', ['int', 'pointer', 'string', stringPointer, 'int *']], 'xyz_set': ['int', ['int', 'pointer', 'string', 'string', 'int *']], 'xyz_destroy': ['void', ['pointer']], 'xyz_free': ['void', ['string']] }); 

使用JS調用C接口:

'use strict' let id = 123; let network_timeout_ms = 3000; let something_timeout_s = 0.3; let obj = libxyz .xyz_create(id , config.cmdid, network_timeout_ms, something_timeout_s); // 調用C接口創建一個指針 if (obj .isNull()) { console.log("Oh no! Couldn't create object!\n"); } else { let pointerSomething= ref.alloc(ref.types.int, 666); // something的值,固定為666,將js中的number類型轉化成C中的int * let key = 'key'; let value = 'value'; let retcode = libxyz.cmem_set(id , obj, key, value , pointerSomething); if(retcode === 0) { let val2 = ref.alloc('string'); // 聲明一個char **類型的指針,即指向string的指針 // 如果設置key/value成功,我們可以利用key取出剛剛設置的value值,並進行比較,看看有木有設置正確。取出來的值,是存在val2這個值里面的,但val2是一個指向string的指針類型,我們來看看如何取出val2的值,並與value進行比較。 let getRetcode = libcmem.cmem_get(config.bid, obj, key, val2, pointerSomething); if(getRetcode === 0) { if(value === ref.readPointer(val2, 0, value.length)) { console.log('set value succ!'); } } else { console.log('get value failed!'); } } else { console.log('set key/value failed!'); } } 

關於ref的詳細api可以參看他們的官方文檔:https://github.com/TooTallNate/ref

值得一提的是,還有一個名為edge.js的開源項目,整個流程和FFI類似,不過支持調用C#、Python,相當有意思。這樣一來,NodeJS相當於可以用C/C++、C#、Python擴展了,潛力無限啊。當然,你可以說我直接拿其它語言寫程序然后NodeJS里fork()就好了,不過其靈活性顯然是不如以上思路的。


免責聲明!

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



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