場景:之前做了一個(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é)
- 創(chuàng)建插件偿枕,并將它編譯為動(dòng)態(tài)鏈接庫
- 使用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ù),需要知道些什么攻旦?
- 函數(shù)名
- 形參
- 返回值類型
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)大的