騰訊雲技術社群-掘金主頁持續為大家呈現雲端計算技術文章,歡迎大家關注!
作者:link
概述
為什麼要在node.js中呼叫動態連結庫
由於騰訊體系下的許多公共的後臺服務(L5, CKV, msgQ等)已經有了非常成熟的C/C++編寫的API,以供應用程式呼叫,node.js作為在公司內新興的後臺runtime在呼叫這些公共服務的時候沒必要再造一遍輪子,而是可以將這些API編譯成.so檔案直接使用。
對於一些密集計算型的任務可以由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:
- 全域性或區域性安裝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。 - 安裝ffi:
npm install ffi
注意事項!
- ffi只能呼叫C風格的模組。
- 需要將C原始碼build成動態連結庫以供呼叫,在Linux下將C原始碼build成
.so
檔案,在windows下build成.dll
檔案。本文只闡述.so檔案的呼叫方法,呼叫.dll差別不大。 - 在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可以參看他們的官方文件:github.com/TooTallNate…
值得一提的是,還有一個名為edge.js的開源專案,整個流程和FFI類似,不過支援呼叫C#、Python,相當有意思。這樣一來,NodeJS相當於可以用C/C++、C#、Python擴充套件了,潛力無限啊。當然,你可以說我直接拿其它語言寫程式然後NodeJS裡fork()就好了,不過其靈活性顯然是不如以上思路的。
相關閱讀:
騰訊雲從零部署nodejs站點
一次 Node.js 記憶體溢位
此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處
原文連結:www.qcloud.com/community/a…
獲取更多騰訊海量技術實踐乾貨,歡迎大家前往騰訊雲技術社群