本文由我首發(fā)于 GitChat 中。
前言
在 Node.js 開發(fā)領(lǐng)域中,原生 C++ 模塊的開發(fā)一直是一個(gè)被人冷落的角落。但是實(shí)際上在必要的時(shí)候,用 C++ 進(jìn)行 Node.js 的原生模塊開發(fā)能有意想不到的好處。
- 性能提升。很多情況下,使用 C++ 進(jìn)行 Node.js 原生模塊開發(fā)的性能會(huì)比純 Node.js 開發(fā)要高,少數(shù)情況除外。
- 開發(fā)成本節(jié)約。在一些即有的 C++ 代碼上做封裝,開發(fā)成本遠(yuǎn)遠(yuǎn)低于從零開始寫 Node.js 代碼。
- Node.js 無法完成的工作。個(gè)別情況,開發(fā)者只能得到一個(gè)庫的靜態(tài)連接庫或者動(dòng)態(tài)鏈接庫以及一堆 C++ 頭文件,其余都是黑盒的,這種情況就不得不使用 C++ 進(jìn)行模塊開發(fā)了。
本文將從早期的 Node.js 開始,逐漸披露 Node.js 原生 C++ 模塊開發(fā)方式的變遷。一直到最后,會(huì)比較詳細(xì)地對 Node.js v8.x 新出的原生模塊開發(fā)接口 N-API 做一次初步的嘗試和解析,使得大家對 Node.js 原生 C++ 模塊開發(fā)的固有印象(認(rèn)為特別麻煩)有一個(gè)比較好的改觀,讓大家都來嘗試一下 Node.js 原生 C++ 模塊的開發(fā)。
不變應(yīng)萬變
雖然 Node.js 原生 C++ 模塊開發(fā)方式有了很大的改變,但是有一些內(nèi)容是不變的,至少到現(xiàn)在來說都是基本上沒什么 Breaking 的變化。
原生模塊本質(zhì)
這就要從 Node.js 最本質(zhì)的 C++ 模塊開發(fā)講起了。舉個(gè)例子,我們在 Linux 下有一個(gè)合法的原生模塊 ons.node,它其實(shí)是一個(gè)二進(jìn)制文件,使用文本編輯器無法正常地看出什么鬼,直到我們遇到了二進(jìn)制文件查看器。
眼尖的同學(xué)會(huì)看到它的 Magic Number[1] 是 0x7F454C46,其按位的 ASCII 碼代表的字符串是 ELF。于是答案呼之欲出,這就是一個(gè) Linux 下的動(dòng)態(tài)鏈接庫文件。
事實(shí)上,不只是在 Linux 中。當(dāng)一個(gè) Node.js 的 C++ 模塊在 OSX 下編譯會(huì)得到一個(gè)后綴是 *.node 本質(zhì)上是 *.dylib 的動(dòng)態(tài)鏈接庫;而在 Windows 下則會(huì)得到一個(gè)后綴是 *.node 本質(zhì)上是 *.dll 的動(dòng)態(tài)鏈接庫。
這么一個(gè)模塊在 Node.js 中被 require 的時(shí)候,是通過 process.dlopen() 對其進(jìn)行引入的。我們來看一下 Node.js v6.9.4 的 DLOpen[2] 函數(shù)吧:
void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;
...
Local<Object> module = args[0]->ToObject(env->isolate());
node::Utf8Value filename(env->isolate(), args[1]);
// 使用 uv_dlopen 加載鏈接庫
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
node_module* const mp = modpending;
modpending = nullptr;
...
// 將加載的鏈接庫句柄轉(zhuǎn)移到 mp 上
mp->nm_dso_handle = lib.handle;
mp->nm_link = modlist_addon;
modlist_addon = mp;
Local<String> exports_string = env->exports_string();
// exports_string 其實(shí)就是 `"exports"`
// 這句的意思是 `exports = module.exports`
Local<Object> exports = module->Get(exports_string)->ToObject(env->isolate());
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
} else {
uv_dlclose(&lib);
env->ThrowError("Module has no declared entry point.");
return;
}
}
按照邏輯來講,這個(gè)加載過程其實(shí)就是下面這樣的。
- 通過
uv_dlopen加載鏈接庫。 - 將加載的鏈接庫掛到原生模塊鏈表中去。
- 通過
mp->nm_register_func()初始化這個(gè)模塊,并得到該有的module和module.exports。
流程走下來就跟這個(gè)流程圖差不多。
node-gyp
這貨是 Node.js 中編譯原生模塊用的。自從 Node.js v0.8 之后,它就跟 Node.js 黏上了,在此之前它的默認(rèn)編譯幫助包是 node-waf[3],對于老 Noder 來說應(yīng)該不會(huì)陌生的。
GYP
node-gyp 是基于 GYP[4] 的。它會(huì)識(shí)別包或者項(xiàng)目中的 binding.gyp[5] 文件,然后根據(jù)該配置文件生成各系統(tǒng)下能進(jìn)行編譯的項(xiàng)目,如 Windows 下生成 Visual Studio 項(xiàng)目文件(*.sln 等),Unix 下生成 Makefile。在生成這些項(xiàng)目文件之后,node-gyp 還能調(diào)用各系統(tǒng)的編譯工具(如 GCC)來將項(xiàng)目進(jìn)行編譯,得到最后的動(dòng)態(tài)鏈接庫 *.node 文件。
從上面的描述中大家可以看到,Windows 下編譯 C++ 原生模塊是依賴 Visual Studio 的,這就是為什么大家在安裝一些 Node.js 包的時(shí)候會(huì)需要你事先安裝好 Vusual Studio 了。
事實(shí)上,對于并沒有 Visual Studio 需求的同學(xué)們來說,它不是必須的,畢竟 node-gyp 只依賴它的編譯器,而不是 IDE。想要精簡化安裝的同學(xué)可以直接訪問 http://landinghub.visualstudio.com/visual-cpp-build-tools 下載 Visual CPP Build Tools 安裝,或者通過
$ npm install --global --production windows-build-tools命令行的方式安裝,就能得到你該得到的編譯工具了。
說了那么多,讓大家見識(shí)一下 binding.gyp 的基本結(jié)構(gòu)吧。
# binding.gyp
{
"targets": [{
"target_name": "addon1",
"sources": [ "1/addon.cc", "1/myobject.cc" ]
}, {
"target_name": "addon2",
"sources": [ "2/addon.cc", "2/myobject.cc" ]
}, {
"target_name": "addon3",
"sources": [ "3/addon.cc", "3/myobject.cc" ]
}, {
"target_name": "addon4",
"sources": [ "4/addon.cc", "4/myobject.cc" ]
}]
}
這段配置講述了這么一個(gè)故事:
- 定義了 4 個(gè) C++ 原生模塊。
- 每個(gè)模塊的源碼分別是 */addon.cc 和 */myobject.cc。
- 4 個(gè)模塊名分別是 addon1 至 addon4。
- 隱藏故事:通過正規(guī)途徑編譯好后,這些模塊存在于 build/Release/addon*.node 中。
關(guān)于 GYP 配置文件的更多內(nèi)容,大家可自行去官方文檔觀摩,在腳注中有 GYP 的鏈接。
做的事情
node-gyp 除了自身是基于 GYP 的之外,它還做了一些額外的事情。首先,在我們編譯一個(gè) C++ 原生擴(kuò)展的時(shí)候,它會(huì)去指定目錄下(通常是 ~/.node-gyp 目錄下)搜我們當(dāng)前 Node.js 版本的頭文件和靜態(tài)連接庫文件,若不存在,它就會(huì)火急火燎跑去 Node.js 官網(wǎng)下載。
這是一個(gè) Windows 下 node-gyp 下載的指定版本 Node.js 頭文件和庫文件的目錄結(jié)構(gòu)。
這個(gè)頭文件目錄會(huì)在 node-gyp 進(jìn)行編譯時(shí),以 "include_dirs" 字段的形式合并進(jìn)我們事先寫好的 binding.gyp 中,總而言之,這里面的所有頭文件能被直接 #include <>。
子命令
node-gyp 是一個(gè)命令行的程序,在安裝好后能通過 $ node-gyp 直接運(yùn)行它。它有一些子命令供大家使用。
-
$ node-gyp configure:通過當(dāng)前目錄的 binding.gyp 生成項(xiàng)目文件,如 Makefile 等; -
$ node-gyp build:將當(dāng)前項(xiàng)目進(jìn)行構(gòu)建編譯,前置操作必須先configure; -
$ node-gyp clean:清理生成的構(gòu)建文件以及輸出目錄,說白了就是把目錄清理了; -
$ node-gyp rebuild:相當(dāng)于依次執(zhí)行了clean、configure和build; -
$ node-gyp install:手動(dòng)下載當(dāng)前版本的 Node.js 的頭文件和庫文件到對應(yīng)目錄。
時(shí)代在召喚
第 N 套國際 Node.js 開發(fā)者原生 C++ 模塊開發(fā)方式,時(shí)代在召喚。
除去前文中講的一些不變的內(nèi)容,還有很多內(nèi)容是一直在變化的,雖然說用老舊的開發(fā)方式也是可以開發(fā)出能用的 C++ 原生模塊,但是舊不如新。
而且,其實(shí)目前來說 node-gyp 的地位也有可能在未來進(jìn)行變化。因?yàn)楫?dāng)年 Chromium 是通過 GYP 來管理它的構(gòu)建配置的,現(xiàn)如今已經(jīng)步入了 GN[6] 的殿堂,是否也意味著 node-gyp 有一天也會(huì)被可能叫做 node-gn 的東西給取代呢?
話不多說,先來看看滄海桑田的故事吧。
黑暗時(shí)代:node-waf
在 Node.js 0.8 之前,通常在開發(fā) C++ 原生模塊的時(shí)候,是通過 node-waf 構(gòu)建的。當(dāng)然彼 node-waf 不是現(xiàn)在在 NPM 倉庫上能搜到的 node-waf 了,當(dāng)年那個(gè) node-waf 早就年久失修了。
這個(gè)東西使用一種叫 wscript 的文件來配置。自 Node.js 升上 0.8 之后,就自帶了 node-gyp 的支持,從此就不再需要 wscript 了。
不過就是因?yàn)橛羞@個(gè)青黃交接的時(shí)候,那段時(shí)間的各種使用 C++ 來開發(fā) Node.js 原生擴(kuò)展的包為了兼容 0.8 前后版本的 Node.js,通常都是 binding.gyp 和 wscript 共存的。
大家可以來看一下 node-mysql-libmysqlclient 這個(gè)包在當(dāng)年相應(yīng)時(shí)間段的時(shí)候的倉庫文件。為了支持 node-gyp,有一個(gè) binding.gyp 文件,然后還存留著 wscript 配置文件。
封建時(shí)代:暴力!暴力!暴力!
在早期的時(shí)候,Node.js 原生 C++ 模塊開發(fā)方式是非常暴力的,直接使用其提供的原生模塊開發(fā)頭文件。
開發(fā)者直接深入到 Node.js 的各種 API,以及 Google V8 的 API。
舉個(gè)最簡單的例子,在幾年前,你的 Node.js C++ 原生擴(kuò)展代碼可能是長這樣的。
Handle<Value> Echo(const Arguments& args)
{
HandleScope scope;
if(args.Length() < 1)
{
ThrowException(
Exception::TypeError(
String::New("Wrong number of arguments.")));
return scope.Close(Undefined());
}
return scope.Close(args[0]);
}
void Init(Handle<Object> exports)
{
exports->Set(String::NewSymbol("echo"),
FunctionTemplate::New(Echo)->GetFunction());
}
這是一個(gè)最簡單的 echo 函數(shù),返回傳進(jìn)來的參數(shù)。寫作 JavaScript 相當(dāng)于是這樣的。
exports.echo = function() {
if(arguments.length < 1)
throw new Error("Wrong number of arguments.");
return arguments[0];
};
遺憾的是,這樣的代碼如果發(fā)成一個(gè)包,你現(xiàn)在是無論如何無法安裝的,除非你用的是 0.10.x 的 Node.js 版本。
為什么這么說呢,這段代碼的確是在 Node.js 0.10.x 的時(shí)候可以用的。但是再往上升 Google V8 的大版本,這段代碼就無法適用了,講粗暴點(diǎn)就是沒辦法再編譯通過了。
就拿 Node.js 6.x 版本的 Google V8 來說,函數(shù)聲明的對比是這樣的:
Handle<Value> Echo(const Arguments& args); // 0.10.x
void Echo(FunctionCallbackInfo<Value>& args); // 6.x
事實(shí)上,根本不需要等到 6.x。上面的代碼到 0.12 就已經(jīng)無法再編譯通過了。不只是函數(shù)聲明的變化,連句柄作用域[7]的聲明方式都變了。
如果要讓它在 Node.js 6.x 下能編譯,就需要改代碼,就像這樣。
void Echo(const FunctionCallbackInfo<Value>& args)
{
Isolate* isolate = args.GetIsolate();
if(args.Length() < 1)
{
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(isolate, "Wrong number of arguments.")));
return;
}
args.GetReturnValue().Set(args[0]);
}
void Init(Local<Object> exports)
{
NODE_SET_METHOD(exports, "echo", Echo);
}
也就是說,以黑暗時(shí)代的方式進(jìn)行 Node.js 原生模塊開發(fā)的時(shí)候,一個(gè)版本只能支持特定幾個(gè)版本的 Node.js,一旦 Node.js 的底層 API 以及 Google V8 的 API 發(fā)生變化,而這些原生模塊又依賴了變化了的 API 的話,包就作廢了。除非包的維護(hù)者去支持新版的 API,不過這樣依賴,老版 Node.js 下就又無法編譯通過新版的包了。
這就很尷尬了。
城堡時(shí)代:Native Abstractions for Node.js
在經(jīng)歷了黑暗時(shí)代的尷尬局面之后,2013 年年中,一個(gè)救世主突然現(xiàn)世。
它的名字叫作 NAN,全稱 Native Abstractions for Node.js,即 Node.js 原生模塊抽象接口。
NAN 由 Rod Vagg 和 Benjamin Byholm 兩手帶大,記名在 GitHub 的 Rod Vagg 賬號下。并且在 Node.js 與 io.js 黑歷史的年代,這個(gè)在 GitHub 上面項(xiàng)目移到了 io.js 的組織下面;后來由于兩家又重歸于好,NAN 最終歸屬到了 nodejs 這個(gè)組織下面。
總之在 NAN 出現(xiàn)之后,Node.js 的原生開發(fā)方式進(jìn)入了城堡時(shí)代,并且一直持續(xù)到現(xiàn)在,甚至可能會(huì)持續(xù)到好久之后。
說 NAN 是 Node.js 原生模塊抽象接口可能還是有點(diǎn)抽象,那么講明白點(diǎn),它就是一堆宏判斷。比如聲明一個(gè)函數(shù)的時(shí)候,只需要通過下面的一個(gè)宏就可以了:
NAN_METHOD(Echo)
{
}
NAN 的宏會(huì)判斷當(dāng)前編譯時(shí)候的 Node.js 版本,根據(jù)不同版本的 Node.js 來展開不同的結(jié)果。這會(huì)兒就又會(huì)提到先前的兩個(gè)函數(shù)聲明對比了。
Handle<Value> Echo(const Arguments& args); // 0.10.x
void Echo(FunctionCallbackInfo<Value>& args); // 6.x
NAN_METHOD 將會(huì)在不同版本的 Node.js 下被 NAN 展開成上面兩個(gè)這樣。
而且 NAN 可不只是提供了 NAN_METHOD 一個(gè)宏,它還有一坨一坨數(shù)不清的宏供開發(fā)者使用。
比如聲明句柄作用域的 Nan::HandleScope、能黑盒調(diào)起 libuv[8] 進(jìn)行事件循環(huán)上的異步操作的 Nan::AsyncWorker 等。
于是,在城堡時(shí)代,大家的 C++ 原生模塊代碼都差不多長這樣。
NAN_METHOD(Echo)
{
if(info.Length() < 1)
{
Nan::ThrowError("Wrong number of arguments.");
return info.GetReturnValue().Set(Nan::Undefined());
}
info.GetReturnValue().Set(info[0]);
}
NAN_MODULE_INIT(InitAll)
{
Nan::Set(
target,
Nan::New<String>("echo").ToLocalChecked(),
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(Echo)).ToLocalChecked());
}
這樣做的好處就是,代碼只需要隨著 NAN 的升級做改變就好,它會(huì)幫你兼容各不同 Node.js 版本,使其在任意版本都能被編譯使用。
即使是 NAN 這樣的好物,也有自己的一個(gè)使命,使命之外的東西會(huì)被逐漸剝離。比如 0.10.x 和 0.12.x 等版本就應(yīng)該要退出歷史舞臺(tái)了,NAN 會(huì)逐漸放棄對它們的兼容和支持。
帝國時(shí)代:符合 ABI 的 N-API
自從前幾天 Node.js v8.0.0 發(fā)布之后,Node.js 推出了全新的用于開發(fā) C++ 原生模塊的接口,N-API。
據(jù)官方文檔所述,它的發(fā)音就是一個(gè)單獨(dú)的
N,加上 API,即四個(gè)英文字母單獨(dú)發(fā)音。
這東西相較于先前三個(gè)時(shí)代有什么不同呢?為什么會(huì)是更進(jìn)一步的帝國時(shí)代呢?
首先,我們知道,即使是在 NAN 的開發(fā)方式下,一次編寫好的代碼在不同版本的 Node.js 下也需要重新編譯,否則版本不符的話 Node.js 無法正常載入一個(gè) C++ 擴(kuò)展。即一次編寫,到處編譯。
而 N-API 相較于 NAPI 來說,它把 Node.js 的所有底層數(shù)據(jù)結(jié)構(gòu)全部黑盒化,抽象成 N-API 當(dāng)中的接口。
不同版本的 Node.js 使用同樣的接口,這些接口是穩(wěn)定地 ABI 化的,即應(yīng)用二進(jìn)制接口(Application Binary Interface)。這使得在不同 Node.js 下,只要 ABI 的版本號一致,編譯好的 C++ 擴(kuò)展就可以直接使用,而不需要重新編譯。事實(shí)上,在支持 N-API 接口的 Node.js 中,的確就指定了當(dāng)前 Node.js 所使用的 ABI 版本。
為了使得以后的 C++ 擴(kuò)展開發(fā)、維護(hù)更方便,N-API 致力于以下的幾個(gè)目標(biāo):
- 以 C 的風(fēng)格提供穩(wěn)定 ABI 接口;
- 消除 Node.js 版本的差異;
- 消除 JavaScript 引擎的差異(如 Google V8、Microsoft ChakraCore 等)。
而這些 API 主要就是用來創(chuàng)建和操作 JavaScript 的值了,我們就再也不用直接使用 Google V8 提供的數(shù)據(jù)類型了。畢竟在 NAN 中,就算我們有時(shí)候看不到 Google V8 的影子,實(shí)際上在宏展開后還是無數(shù)的 Google V8 數(shù)據(jù)結(jié)構(gòu)。
為了達(dá)成上述隱藏的目標(biāo),N-API 的姿勢就變成了這樣:
- 提供頭文件 node_api.h;
- 任何 N-API 調(diào)用都返回一個(gè)
napi_status枚舉,來表示這次調(diào)用成功與否; - N-API 的返回值由于被
napi_status占坑了,所以真實(shí)返回值由傳入的參數(shù)來繼承,如傳入一個(gè)指針讓函數(shù)操作; - 所有 JavaScript 數(shù)據(jù)類型都被黑盒類型
napi_value封裝,不再是類似于v8::Object、v8::Number等類型; - 如果函數(shù)調(diào)用不成功,可以通過
napi_get_last_error_info函數(shù)來獲取最后一次出錯(cuò)的信息。
注意:哪怕是現(xiàn)在的 Node.js v8.x 版本,N-API 仍處于一個(gè)實(shí)驗(yàn)狀態(tài),個(gè)人認(rèn)為還有非常長的一段路要走,所以大家在生產(chǎn)環(huán)境中還不必太過于激進(jìn),不過 N-API 依然是大勢所趨;不過對于使用老版本的 Node.js 開發(fā)者來說,大家也不要著急,即使 N-API 是在 v8.x 才正式集成進(jìn) Node.js,在其它舊版本的 Node.js 中依然可以將 N-API 作為外掛式的頭文件9中使用,只不過無法做到跨版本的特性,這只是它做的向后兼容的一個(gè)事情而已。
關(guān)于 N-API 一系列的函數(shù)可以訪問它的文檔了解更多詳情,現(xiàn)在我們來點(diǎn)料兒讓大家對 N-API 的印象不是那么抽象。
模塊初始化
在封建時(shí)代和 NAN 所處的,模塊的初始化是交給 Node.js 提供的宏來實(shí)現(xiàn)的。
NODE_MODULE(addon, Init)
而到了當(dāng)前的 N-API,它就變成了 N-API 的一個(gè)宏了。
NAPI_MODULE(addon, Init)
相應(yīng)地,這個(gè)初始化函數(shù) Init 的寫法也會(huì)有所改變。比如這是封建時(shí)代和 NAN 時(shí)代的兩種不同寫法:
// 暴力寫法
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "echo", Echo);
}
// NAN 寫法
NAN_MODULE_INIT(Init)
{
Nan::Set(
target,
Nan::New<String>("echo").ToLocalChecked(),
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(Echo)).ToLocalChecked());
}
而到了 N-API 的時(shí)候,這個(gè) Init 函數(shù)就該是這樣的了。
void Init(napi_env env, napi_value exports, napi_value module, void* priv)
{
napi_status status;
// 用于設(shè)置 exports 對象的描述結(jié)構(gòu)體
napi_property_descriptor desc =
{ "echo", 0, Echo, 0, 0, 0, napi_default, 0 };
// 把 "echo" 設(shè)置到 exports 去
status = napi_define_properties(env, exports, 1, &desc);
}
napi_property_descriptor是用于設(shè)置對象屬性的描述結(jié)構(gòu)體,它的聲明如下:typedef struct { const char* utf8name; napi_callback method; napi_callback getter; napi_callback setter; napi_value value; napi_property_attributes attributes; void* data; } napi_property_descriptor;那么上面
Init函數(shù)中的desc意思就是,即將被掛在的對象下會(huì)掛一個(gè)叫"echo"的東西,它的函數(shù)是Echo,其它的getter、setter等全是空指針,而屬性則是napi_default。
napi_property_attributes除了napi_default之外,還有諸如只讀、是否可枚舉等屬性。
函數(shù)聲明
還記得之前的兩種函數(shù)聲明嗎?第三次再搬過來。
Handle<Value> Echo(const Arguments& args); // 0.10.x
void Echo(FunctionCallbackInfo<Value>& args); // 6.x
在 N-API 中,你不用再被告知需要有 C++ 基礎(chǔ),C 即可。因?yàn)樵?N-API 里面,聲明一個(gè) Echo 是這樣的:
napi_value Echo(napi_env env, napi_callback_info info)
{
napi_status status;
size_t argc = 1;
napi_value argv[1];
status = napi_get_cb_info(env, info, &argc, argv, 0, 0);
if(status != napi_ok || argc < 1)
{
napi_throw_type_error(env, "Wrong number of arguments");
return 0; // napi_value 實(shí)際上是一個(gè)指針,返回空指針表示無返回值
}
return argv[0];
}
重要:目前 8.0.0 和 8.1.0 版本的 Node.js 官方文檔中,關(guān)于 N-API 的各種接口文檔錯(cuò)誤頗多,所以還是要以能使用的接口為準(zhǔn)。
而且現(xiàn)在大家也有很多人正在幫忙一起修復(fù)文檔。例如現(xiàn)在的 JavaScript 函數(shù)聲明返回值其實(shí)是
napi_value,而官方文檔上還是老舊的void。又比如 ``napi_property_descriptor_desc結(jié)構(gòu)體中,在utf8name之后還有一個(gè)napi_value` 的變量,而文檔中卻是沒有的。這也是為什么我前面強(qiáng)調(diào)目前來說 N-API 還處于試驗(yàn)階段。畢竟 API 并沒有完全穩(wěn)定下來,還處于一個(gè)快速迭代的步伐中,文檔的更新并未跟上代碼的更新。至少在筆者寫作的當(dāng)前是這樣的(現(xiàn)在日期 2017 年 6 月 9 日)。
上面代碼分步解析。
- 通過
napi_get_cb_info獲取當(dāng)次函數(shù)請求的參數(shù)信息,包括參數(shù)數(shù)量和參數(shù)體(參數(shù)體以napi_value的數(shù)組形式體現(xiàn)); - 看看解析有無出錯(cuò)(
status不等于napi_ok)或者看看參數(shù)數(shù)量是否小于 1;- 若解析出錯(cuò)或者參數(shù)數(shù)量小于 1,通過
napi_throw_type_error在 JavaScript 層拋出一個(gè)錯(cuò)誤對象,并返回; - 若無錯(cuò)則繼續(xù)進(jìn)行;
- 若解析出錯(cuò)或者參數(shù)數(shù)量小于 1,通過
- 返回
argv[0],即第一個(gè)參數(shù)。
Demo 完整代碼
這里放上這個(gè) Echo 樣例的完整代碼,大家可以拿回家試試看。
binding.gyp
{
"targets": [{
"target_name": "addon",
"sources": [ "addon.cc" ],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_CXX_LIBRARY": "libc++",
"MACOSX_DEPLOYMENT_TARGET": "10.7"
},
"msvs_settings": {
"VCCLCompilerTool": { "ExceptionHandling": 1 }
}
}]
}
addon.cc
#include <node_api.h>
napi_value Echo(napi_env env, napi_callback_info info)
{
napi_status status;
size_t argc = 1;
napi_value argv[1];
status = napi_get_cb_info(env, info, &argc, argv, 0, 0);
if(status != napi_ok || argc < 1)
{
napi_throw_type_error(env, "Wrong number of arguments");
status = napi_get_undefined(env, argv);
}
return argv[0];
}
void Init(napi_env env, napi_value exports, napi_value module, void* priv)
{
napi_status status;
napi_property_descriptor desc =
{ "echo", 0, Echo, 0, 0, 0, napi_default, 0 };
status = napi_define_properties(env, exports, 1, &desc);
}
NAPI_MODULE(addon, Init)
乘風(fēng)破浪
在完成了代碼之后,大家趕緊試一下代碼吧。
首先在 Node.js v8.x 下進(jìn)行試驗(yàn),把這兩段代碼分別放到同一個(gè)目錄下,命名好后,執(zhí)行這樣的終端命令:
$ node-gyp rebuild
...
$ node --napi-modules
(node:52264) Warning: N-API is an experimental feature and could change at any time
> const addon = require("./build/Release/addon");
undefined
> addon.echo("2333");
'2333'
> addon.echo("蛋花湯??", "南瓜餅??");
'蛋花湯??'
> addon.echo();
TypeError: Wrong number of arguments
at repl:1:7
at ContextifyScript.Script.runInThisContext (vm.js:44:33)
at REPLServer.defaultEval (repl.js:239:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:433:10)
at emitOne (events.js:120:20)
at REPLServer.emit (events.js:210:7)
at REPLServer.Interface._onLine (readline.js:278:10)
at REPLServer.Interface._line (readline.js:625:8)
注意:還是因?yàn)樵囼?yàn)特性,目前在 Node.js v8.x 要加載和執(zhí)行 N-API 的 C++ 擴(kuò)展的話,在啟動(dòng)
node的時(shí)候需要加上--napi-modules參數(shù),表示這次執(zhí)行要啟用 N-API 特性。
效果顯而易見,在剛啟動(dòng) Node.js REPL 的時(shí)候,你會(huì)得到一個(gè)警告。
(node:52264) Warning: N-API is an experimental feature and could change at any time
表示它目前還不是特別穩(wěn)定,但是值得我們展望未來。然后在我們 require() 擴(kuò)展的時(shí)候,我們就得到了一個(gè)擁有 echo 函數(shù)的對象了。
我們嘗試了三種調(diào)用方式。第一次是規(guī)規(guī)矩矩傳入一個(gè)參數(shù),echo 如期返回我們傳入的參數(shù) "2333";第二次傳入兩個(gè)參數(shù),echo 返回了第一個(gè)參數(shù) "蛋花湯??";最后一次我們沒傳任何參數(shù),這個(gè)時(shí)候就走到了 C++ 擴(kuò)展中判斷函數(shù)參數(shù)數(shù)量失敗的條件分支,就拋出了一個(gè) Wrong number of arguments 的錯(cuò)誤對象。
總之,它按照我們的預(yù)期跑起來了。并且代碼里面并沒有任何 Node.js 非 N-API 所暴露出來的數(shù)據(jù)結(jié)構(gòu)和 V8 的數(shù)據(jù)結(jié)構(gòu)——版本差異消除了。
接下來激動(dòng)人心的時(shí)刻到了,如果讀者是使用 nvm 來管理自己的 Node.js 版本的話,可以嘗試著安裝一個(gè) 8.1.0 的 Node.js 版本。
$ nvm install 8.1.0
在安裝成功切換版本成功后,嘗試著直接打開 Node.js RELP,忘掉再次編譯剛才編譯好的擴(kuò)展這一步。(不過別忘了 --napi-module 參數(shù))
把剛才用于測試的幾句 JavaScript 代碼再重復(fù)地輸入——N-API 誠不我欺,居然還是能輸出結(jié)果。這對于以前的暴力做法和 NAN 做法來說,無疑是非常大的一個(gè)進(jìn)步。
向下兼容
至此,我希望大家還沒有忘記 N-API 是自 Node.js 8.0 之后出的特性。所以之前 Demo 的代碼并不能在 Node.js 8.0 之前的版本如期編譯和運(yùn)行。
辛辛苦苦寫好的包,居然不能在 Node.js 6.x 下面跑,搞什么。
先別急著摔。文中之前也說了,有一個(gè)外掛式頭文件的包,其包名是 node-addon-api。
我們就試著通過它來進(jìn)行向下兼容吧。首先在我們剛才的源碼目錄把這個(gè)包給安裝上。
$ npm install --save node-addon-api
還是由于快速迭代的原因,我不能保證這個(gè)包當(dāng)前版本的時(shí)效性,不過我相信大家都有探索精神,在未來版本不符導(dǎo)致的 API 不符的問題應(yīng)該都能解決。
然后,給我們的 binding.gyp 函數(shù)加點(diǎn)料,加兩個(gè)字段,里面是兩個(gè)指令展開。
"include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ],
"dependencies": [ "<!(node -p \"require('node-addon-api').gyp\")" ]
<!@ 和 <! 開頭的字符串在 GYP 中代表指令,表示它的值是后面的指令的執(zhí)行結(jié)果。上面兩條指令的返回結(jié)果分別是外掛式頭文件的頭文件搜索路徑,以及外掛式 N-API 這個(gè)包編譯成靜態(tài)連接庫供我們自己的包使用的依賴聲明。
有了這兩個(gè)字段后,就表示我們依賴了外掛式 N-API 頭文件。而且它內(nèi)部自帶判斷,如果版本已經(jīng)達(dá)到了有 N-API 的要求,它的依賴就會(huì)是一個(gè)空依賴,即不依賴外掛式 N-API 編譯的靜態(tài)連接庫。
也就是說,用了外掛式的 N-API,能自動(dòng)適配 Node.js 8.x 和低版本。
于是這個(gè) binding.gyp 現(xiàn)在看起來是這樣子的。
{
"targets": [{
"target_name": "addon",
"sources": [ "addon.cc" ],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_CXX_LIBRARY": "libc++",
"MACOSX_DEPLOYMENT_TARGET": "10.7"
},
"msvs_settings": {
"VCCLCompilerTool": { "ExceptionHandling": 1 }
},
"include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ],
"dependencies": [ "<!(node -p \"require('node-addon-api').gyp\")" ]
}]
}
至于源碼層面,我們就不需要作任何修改。在 Node.js v6.x 下面試試看吧。同樣是使用 node-gyp rebuild 進(jìn)行編譯。然后通過 Node.js REPL 進(jìn)去測試。
具體的終端輸出這里就不放出來了,相信經(jīng)過實(shí)驗(yàn)的大家都得到了自己想要的結(jié)果。
小結(jié)
本次內(nèi)容主要講解了在 Node.js 領(lǐng)域中原生 C++ 模塊開發(fā)的方式變遷。
- 從 node-waf 到 node-gyp,這是構(gòu)建工具的一個(gè)變遷,未來說不定會(huì)是 GN 或者其它的構(gòu)建工具。
- 從暴力寫碼,到 NAN 的出現(xiàn),見證了 Node.js 社區(qū)的各種愛恨情仇,一直到現(xiàn)在的新生兒 N-API,為原生 C++ 模塊的開發(fā)輸送了新鮮的血液。
目前的中堅(jiān)力量仍然是 NAN 的開發(fā)方式,甚至我猜測是否未來有可能 NAN 會(huì)提供關(guān)于 N-API 的各種宏封裝,使其徹底消除版本差異,包括 ABI 版本上的差異。當(dāng)然這種 ABI 版本差異導(dǎo)致的需要多次編譯問題應(yīng)該還是存在的,這里指的是一次編碼的差異。
在大家跟著本文對 N-API 進(jìn)行了一次淺嘗輒止的嘗試之后,希望能對當(dāng)下仍然處于實(shí)驗(yàn)狀態(tài)的 N-API 充滿了希冀,并對現(xiàn)在存在的各種坑處以包容的心態(tài)。
畢竟,Node.js loves you all。
參考資料
- 「Consider moving from gyp to gn」:https://github.com/nodejs/node/issues/6089
- 「Getting Started with Embedding · v8/v8 Wiki」:https://github.com/v8/v8/wiki/Getting-Started-with-Embedding
- 「Drop support for v0.10 and v0.12?」:https://github.com/nodejs/nan/issues/676
- 「Node Loves Rust」:https://cnodejs.org/topic/593353775b07c1b24afa0638
- 「N-API | Node.js v8.0.0 Documentation」:https://nodejs.org/docs/v8.0.0/api/n-api.html
- 「doc: fix out of date sections in n-api doc」:https://github.com/nodejs/node/pull/13508
-
用于定義某種文件類型的特殊標(biāo)識(shí),詳見 https://en.wikipedia.org/wiki/Magic_number_(programming) ?
-
代碼參見 https://github.com/nodejs/node/blob/v6.9.4/src/node.cc#L2427-L2502 ?
-
年久失修,當(dāng)前 NPM 上搜索到的 node-waf 已經(jīng)不是當(dāng)年的了,不過這個(gè)是 Waf 的官方倉庫 https://github.com/waf-project/waf。 ?
-
全稱 Generate Your Projects,是谷歌開發(fā)的一套構(gòu)建系統(tǒng),未盡事宜詳詢 https://gyp.gsrc.io。 ?
-
GYP 的配置文件的后綴就是 *.gyp 或者 *.gypi 等,是個(gè)類 JSON 文件。 ?
-
GN 是谷歌開發(fā)的相較于 GYP 更新更快的一套構(gòu)建工具,可以參考 https://chromium.googlesource.com/chromium/src/tools/gn/+/HEAD/docs/quick_start.md ?
-
讓垃圾回收機(jī)制來管理 JavaScript 對象生命周期的一種類,即 HandleScope,在我的新書中將會(huì)有詳解。 ?
-
Node.js 的異步事件循環(huán)支撐者,詳詢 http://www.libuv.org/ ?