使用C++開發(fā)Node原生插件

場景:之前做了一個(gè)彈幕姬鳍置,其中有一個(gè)選擇字體的功能莲祸,但有個(gè)不太舒服的地方是js如果要獲取可用字體只能一個(gè)一個(gè)試族淮,這種方法不可能做到完美地列出所有可用字體,而且性能不佳晴裹,所以我就需要找到一個(gè)可以直接獲取系統(tǒng)可用字體的方法被济,這里選擇了使用更底層的語言C++來實(shí)現(xiàn)這個(gè)功能。

開始

首先我們了解一下Node的原生插件Node Native Addon涧团,它像一群.js文件一樣雷袋,是一個(gè)包裝了這些文件的集合躏尉,也就是說,我們可以把我們所以要的邏輯包含在這個(gè)集合中,但是我們知道巧娱,C++和JS的代碼原本是不兼容的,互用是不可能的典蜕,但是它們所實(shí)現(xiàn)的功能在更底層是共通的稚伍,比如當(dāng)C++編譯為.exe文件以后,它就僅僅只是一個(gè)邏輯的二進(jìn)制表示,JS在執(zhí)行的過程中有點(diǎn)類似(刊棕?)炭晒,總之,在更底層甥角,我們可以通過內(nèi)存地址讓二者通信网严,這給了我們一個(gè)實(shí)現(xiàn)用C++為Node編寫插件的思路。

這里還有個(gè)概念——?jiǎng)討B(tài)鏈接庫(Dynamically Linked Library)嗤无,也稱DLL震束,我們經(jīng)常會(huì)在Windows系統(tǒng)中看到擴(kuò)展名為.dll的文件,這就是動(dòng)態(tài)鏈接庫翁巍,動(dòng)態(tài)鏈接庫會(huì)在程序運(yùn)行時(shí)被載入驴一,它包含了編譯后的C/C++原生代碼和API,可以實(shí)現(xiàn)與程序本體的通信灶壶。

那么Node是如何工作的呢肝断?

Node使用Google開源的v8引擎作為默認(rèn)JS引擎,為了實(shí)現(xiàn)事件循環(huán)驰凛、異步輸入I/O操作胸懈,它使用了 Libuv 庫。

這和我們要編寫的插件有什么關(guān)系恰响?

如果我們點(diǎn)到這兩個(gè)庫里去趣钱,會(huì)發(fā)現(xiàn)他們一個(gè)使用C++編寫,一個(gè)使用C編寫胚宦,暫且不提V8首有,Libuv作為Node使用的一個(gè)庫,他竟然是使用C編寫的枢劝?這意味著Node可以使用C編寫的庫井联。

具體一點(diǎn)怎么實(shí)現(xiàn)?

動(dòng)態(tài)鏈接庫是程序運(yùn)行時(shí)被動(dòng)態(tài)載入的您旁,我們的插件也有這個(gè)特點(diǎn)烙常,它作為我們程序本體外的一個(gè)存在,本身和我們的程序是完全分離的鹤盒,所以我們需要在程序運(yùn)行的時(shí)候使用Node提供的接口動(dòng)態(tài)載入我們的插件蚕脏。

動(dòng)態(tài)載入插件,然后呢侦锯?

這里需要提到一個(gè)概念驼鞭,Application Binary Interface (ABI),平時(shí)我們說的都是API尺碰,這個(gè)ABI是什么玩意挣棕?其實(shí)和API很像汇竭,只是ABI通過內(nèi)存地址實(shí)現(xiàn)通訊,這在開頭提過了穴张,這是我們實(shí)現(xiàn)插件的手段。具體一點(diǎn)两曼,當(dāng)插件在內(nèi)存中創(chuàng)建了【對(duì)象/類/變量/方法】之后皂甘,Node可以通過內(nèi)存地址去訪問這些創(chuàng)建好了的【對(duì)象/類/變量/方法】,由此獲取插件實(shí)現(xiàn)的功能悼凑。

總結(jié)

  1. 創(chuàng)建插件偿枕,并將它編譯為動(dòng)態(tài)鏈接庫
  2. 使用Node與動(dòng)態(tài)鏈接庫通信,獲取其實(shí)現(xiàn)的邏輯或變量
那么問題來了户辫,與JS不同渐夸,編譯后的C++源碼并不會(huì)保留函數(shù)名、變量名渔欢、類名這類東西墓塌,如果我們要在JS中使用一個(gè)在C++中實(shí)現(xiàn)的sayHello方法該怎么整?

不要方奥额,Node給了我們實(shí)現(xiàn)這個(gè)的api苫幢,它是一堆C++的庫
所以呢,我們需要先安裝一下這個(gè)玩意垫挨,首先創(chuàng)建項(xiàng)目文件夾韩肝,并初始化項(xiàng)目。

打開終端

mkdir greet
cd ./greet
npm init -y

安裝Napi九榔,這是Node提供給我們的一個(gè)庫

npm install -S node-addon-api

創(chuàng)建源文件文件夾

mkdir src
cd ./src

創(chuàng)建源文件 greet.h greet.cpp哀峻,如下

//greet.h
#include<string>
std::string helloUser( std::string name );
//greet.cpp
#include"greet.h"
#include<iostream>

std::string helloUser(std::string name){
    return "Hello " + name + "!"; 
}

定睛一看這不就是個(gè)平平無奇的C++代碼嘛?
是的哲泊,但我們的目的是讓它能為Node服務(wù)

來回想一下剩蟀,如果我們要調(diào)用一個(gè)函數(shù),需要知道些什么攻旦?

  1. 函數(shù)名
  2. 形參
  3. 返回值類型

Napi所做的正是定義這些喻旷,讓我們?cè)賱?chuàng)建一個(gè)文件index.cpp,我們?cè)谶@個(gè)文件中定義這些

#include<napi.h>
#include<string>
#include"greet.h"

Napi::String greetHello(const Napi::CallbackInfo& info){
    Napi::Env env = info.Env();

    std::string name = (std::string)info[0].ToString();
    std::string result = helloUser(name);

    return Napi::String::New(env, result);
}

Napi::Object Init(Napi::Env env, Napi::Object exports){
    exports.Set(
        Napi::String::New(env, "greet"),
        Napi::Function::New(env, greetHello)
    );

    return exports;
}

NODE_API_MODULE(greet, Init);

這個(gè)看起來有點(diǎn)多牢屋,但實(shí)質(zhì)上還是很簡單的且预,首先我們使用Napi的數(shù)據(jù)類型定義了一個(gè)greetHello方法,它的形參是一個(gè)info烙无,這個(gè)里面有我們調(diào)用方法時(shí)傳進(jìn)來的參數(shù)锋谐,所以可以看到

std::string name = (std::string)info[0].ToString();

這一行代碼我們獲取了傳進(jìn)來的參數(shù)——用戶名,info[*]會(huì)返回info中的參數(shù)列表中的某一個(gè)參數(shù)截酷,我們把它轉(zhuǎn)換為std::string類型涮拗,再講其傳入我們一開始定義的函數(shù)helloUser中

std::string result = helloUser(name);

獲取了處理結(jié)果,最后再返回結(jié)果。
好了三热,只是把我們的邏輯使用Napi提供的數(shù)據(jù)類型再處理一遍鼓择,這樣可以避免雜七雜八的數(shù)據(jù)類型,使得其更加規(guī)范就漾。
再看下一個(gè)呐能,Init,這是啥玩意抑堡?
顧名思義摆出,我們通過Init來初始化插件,里面做的就是我們上面提到的首妖,提供函數(shù)名和邏輯

exports.Set(
    Napi::String::New(env, "greet"),
    Napi::Function::New(env, greetHello)
);

這是最關(guān)鍵的偎漫,它告訴了Node,我們這個(gè)是個(gè)函數(shù)有缆,它的名字是greet象踊,它通過greetHello實(shí)現(xiàn)邏輯。現(xiàn)在妒貌,我們完美解決了上面的疑惑:怎么在JS中調(diào)用C++的方法
最后通危,我們通過一個(gè)宏函數(shù)

NODE_API_MODULE(greet, Init);

定義了一個(gè)Node模塊greet,它的初始化方法是Init

預(yù)備工作

回到根目錄下

cd ..

這里我們需要提前安裝python解釋器與vc++構(gòu)建工具灌曙,這些大家自行搜索安裝吧菊碟,裝好后,我們?cè)侔惭bnode-gyp在刺,這是node提供給我們的編譯插件用的腳手架

npm install -g node-gyp

配置

像package.json一樣逆害,我們的插件也得有個(gè)放自己配置的文件,它叫binding.gyp蚣驼,內(nèi)部如下

{
  "targets": [
    {
      "target_name": "greet",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "sources": [
        "./src/greet.cpp",
        "./src/index.cpp"
      ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

targets就是編譯目標(biāo)魄幕,我們這里就一個(gè)greet模塊
target_name就是模塊名,這需要與宏函數(shù)中的模塊名一致
sources是源文件列表
include_dirs是給node用的颖杏,node需要require相應(yīng)模塊纯陨,我們這里用了node-addon-api模塊,那就安排上
其他的就不具體解釋了留储,自行搜索就好

隨后翼抠,我們使用node-gyp創(chuàng)建配置模板

node-gyp configure

會(huì)發(fā)現(xiàn)多出來一個(gè)文件夾 ./build,這是node-gyp生成的模板获讳,不用管了

編譯

node-gyp build

這么簡單暴力的一行命令后阴颖,會(huì)多出來一個(gè)文件夾
./build/Release
這里頭就是編譯后的文件了,我們需要的是以.node為擴(kuò)展名的那個(gè)
我們建一個(gè)index.js用來測試

const greetModule = require('./build/Release/greet.node');

console.log('module:', greetModule);
console.log('hello:', greetModule.greet('Yeuoly'));

發(fā)現(xiàn)輸出

module: { greet: [Function],
  path:
   'I:\\H\\Shared\\Lib\\YeuolyDanmuNodeModules\\build\\Release\\greet.node' }
hello: Hello Yeuoly!

成功完成了我們用C++實(shí)現(xiàn)的邏輯丐膝,但是這里有個(gè)問題量愧,路徑問題钾菊,不同電腦上這個(gè)地方會(huì)出大問題,所以我們需要一個(gè)用來避免這個(gè)的東西偎肃,那就是bindings煞烫,這是個(gè)插件

npm install -S bindings

裝完之后,更改index.js

const greetModule = require('bindings')('greet');

console.log('module:', greetModule);
console.log('hello:', greetModule.greet('Yeuoly'));

再次執(zhí)行累颂,發(fā)現(xiàn)結(jié)果一樣红竭,很完美
接下來考慮發(fā)布的問題,我們?cè)谑褂眠@個(gè)模塊的時(shí)候不止于還要require這么復(fù)雜吧喘落?我們肯定希望簡單一點(diǎn),所以我們封裝一下index.js

const greetModule = require('bindings')('greet');

export default{
  greet : greetModule.greet
}

于是我們就可以在js中愉快地使用這個(gè)插件了

參考文章:A simple guide to load C/C++ code into Node.js JavaScript Applications
在原文的基礎(chǔ)上加上了一個(gè)自己的理解最冰,刪去了一些我認(rèn)為沒太必要講得太詳細(xì)的內(nèi)容瘦棋,讀全英文的文章還是有點(diǎn)頭疼,但收獲也是相當(dāng)大的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暖哨,一起剝皮案震驚了整個(gè)濱河市赌朋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌篇裁,老刑警劉巖沛慢,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異达布,居然都是意外死亡团甲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門黍聂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躺苦,“玉大人,你說我怎么就攤上這事产还∑ダ澹” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵脐区,是天一觀的道長愈诚。 經(jīng)常有香客問我,道長牛隅,這世上最難降的妖魔是什么炕柔? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮倔叼,結(jié)果婚禮上汗唱,老公的妹妹穿的比我還像新娘。我一直安慰自己丈攒,他們只是感情好哩罪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布授霸。 她就那樣靜靜地躺著,像睡著了一般际插。 火紅的嫁衣襯著肌膚如雪碘耳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天框弛,我揣著相機(jī)與錄音辛辨,去河邊找鬼。 笑死瑟枫,一個(gè)胖子當(dāng)著我的面吹牛斗搞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慷妙,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼僻焚,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了膝擂?” 一聲冷哼從身側(cè)響起虑啤,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎架馋,沒想到半個(gè)月后狞山,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叉寂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年萍启,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屏鳍。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伊约,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孕蝉,到底是詐尸還是另有隱情屡律,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布降淮,位于F島的核電站超埋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏佳鳖。R本人自食惡果不足惜霍殴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望系吩。 院中可真熱鬧来庭,春花似錦、人聲如沸穿挨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至帽衙,卻和暖如春菜皂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厉萝。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工恍飘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谴垫。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓章母,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翩剪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胳施,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容