本文由我首發於 GitChat 中。html
在 Node.js 開發領域中,原生 C++ 模塊的開發一直是一個被人冷落的角落。可是實際上在必要的時候,用 C++ 進行 Node.js 的原生模塊開發能有意想不到的好處。node
性能提高。不少狀況下,使用 C++ 進行 Node.js 原生模塊開發的性能會比純 Node.js 開發要高,少數狀況除外。mysql
開發成本節約。在一些即有的 C++ 代碼上作封裝,開發成本遠遠低於從零開始寫 Node.js 代碼。c++
Node.js 沒法完成的工做。個別狀況,開發者只能獲得一個庫的靜態鏈接庫或者動態連接庫以及一堆 C++ 頭文件,其他都是黑盒的,這種狀況就不得不使用 C++ 進行模塊開發了。git
本文將從早期的 Node.js 開始,逐漸披露 Node.js 原生 C++ 模塊開發方式的變遷。一直到最後,會比較詳細地對 Node.js v8.x 新出的原生模塊開發接口 N-API 作一次初步的嘗試和解析,使得你們對 Node.js 原生 C++ 模塊開發的固有印象(認爲特別麻煩)有一個比較好的改觀,讓你們都來嘗試一下 Node.js 原生 C++ 模塊的開發。github
雖然 Node.js 原生 C++ 模塊開發方式有了很大的改變,可是有一些內容是不變的,至少到如今來講都是基本上沒什麼 Breaking 的變化。面試
這就要從 Node.js 最本質的 C++ 模塊開發講起了。舉個例子,咱們在 Linux 下有一個合法的原生模塊 ons.node,它實際上是一個二進制文件,使用文本編輯器沒法正常地看出什麼鬼,直到咱們遇到了二進制文件查看器。sql
眼尖的同窗會看到它的 Magic Number1 是 0x7F454C46
,其按位的 ASCII 碼錶明的字符串是 ELF
。因而答案呼之欲出,這就是一個 Linux 下的動態連接庫文件。npm
事實上,不僅是在 Linux 中。當一個 Node.js 的 C++ 模塊在 OSX 下編譯會獲得一個後綴是 *.node 本質上是 *.dylib 的動態連接庫;而在 Windows 下則會獲得一個後綴是 *.node 本質上是 *.dll 的動態連接庫。json
這麼一個模塊在 Node.js 中被 require
的時候,是經過 process.dlopen()
對其進行引入的。咱們來看一下 Node.js v6.9.4 的 DLOpen
2 函數吧:
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; ... // 將加載的連接庫句柄轉移到 mp 上 mp->nm_dso_handle = lib.handle; mp->nm_link = modlist_addon; modlist_addon = mp; Local<String> exports_string = env->exports_string(); // exports_string 其實就是 `"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; } }
按照邏輯來說,這個加載過程其實就是下面這樣的。
經過 uv_dlopen
加載連接庫。
將加載的連接庫掛到原生模塊鏈表中去。
經過 mp->nm_register_func()
初始化這個模塊,並獲得該有的 module
和 module.exports
。
流程走下來就跟這個流程圖差很少。
這貨是 Node.js 中編譯原生模塊用的。自從 Node.js v0.8 以後,它就跟 Node.js 黏上了,在此以前它的默認編譯幫助包是 node-waf3,對於老 Noder 來講應該不會陌生的。
node-gyp 是基於 GYP4 的。它會識別包或者項目中的 binding.gyp5 文件,而後根據該配置文件生成各系統下能進行編譯的項目,如 Windows 下生成 Visual Studio 項目文件(*.sln 等),Unix 下生成 Makefile。在生成這些項目文件以後,node-gyp 還能調用各系統的編譯工具(如 GCC)來將項目進行編譯,獲得最後的動態連接庫 *.node 文件。
從上面的描述中你們能夠看到,Windows 下編譯 C++ 原生模塊是依賴 Visual Studio 的,這就是爲何你們在安裝一些 Node.js 包的時候會須要你事先安裝好 Vusual Studio 了。
事實上,對於並無 Visual Studio 需求的同窗們來講,它不是必須的,畢竟 node-gyp 只依賴它的編譯器,而不是 IDE。想要精簡化安裝的同窗能夠直接訪問 http://landinghub.visualstudi... 下載 Visual CPP Build Tools 安裝,或者經過
$ npm install --global --production windows-build-tools
命令行的方式安裝,就能獲得你該獲得的編譯工具了。
說了那麼多,讓你們見識一下 binding.gyp 的基本結構吧。
# 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" ] }] }
這段配置講述了這麼一個故事:
定義了 4 個 C++ 原生模塊。
每一個模塊的源碼分別是 */addon.cc 和 */myobject.cc。
4 個模塊名分別是 addon1 至 addon4。
隱藏故事:經過正規途徑編譯好後,這些模塊存在於 build/Release/addon*.node 中。
關於 GYP 配置文件的更多內容,你們可自行去官方文檔觀摩,在腳註中有 GYP 的連接。
node-gyp 除了自身是基於 GYP 的以外,它還作了一些額外的事情。首先,在咱們編譯一個 C++ 原生擴展的時候,它會去指定目錄下(一般是 ~/.node-gyp 目錄下)搜咱們當前 Node.js 版本的頭文件和靜態鏈接庫文件,若不存在,它就會火急火燎跑去 Node.js 官網下載。
這是一個 Windows 下 node-gyp 下載的指定版本 Node.js 頭文件和庫文件的目錄結構。
這個頭文件目錄會在 node-gyp 進行編譯時,以 "include_dirs"
字段的形式合併進咱們事先寫好的 binding.gyp 中,總而言之,這裏面的全部頭文件能被直接 #include <>
。
node-gyp 是一個命令行的程序,在安裝好後能經過 $ node-gyp
直接運行它。它有一些子命令供你們使用。
$ node-gyp configure
:經過當前目錄的 binding.gyp 生成項目文件,如 Makefile 等;
$ node-gyp build
:將當前項目進行構建編譯,前置操做必須先 configure
;
$ node-gyp clean
:清理生成的構建文件以及輸出目錄,說白了就是把目錄清理了;
$ node-gyp rebuild
:至關於依次執行了 clean
、configure
和 build
;
$ node-gyp install
:手動下載當前版本的 Node.js 的頭文件和庫文件到對應目錄。
第 N 套國際 Node.js 開發者原生 C++ 模塊開發方式,時代在召喚。
除去前文中講的一些不變的內容,還有不少內容是一直在變化的,雖說用老舊的開發方式也是能夠開發出能用的 C++ 原生模塊,可是舊不如新。
並且,其實目前來講 node-gyp 的地位也有可能在將來進行變化。由於當年 Chromium 是經過 GYP 來管理它的構建配置的,現現在已經步入了 GN6 的殿堂,是否也意味着 node-gyp 有一天也會被可能叫作 node-gn 的東西給取代呢?
話很少說,先來看看滄海桑田的故事吧。
在 Node.js 0.8 以前,一般在開發 C++ 原生模塊的時候,是經過 node-waf 構建的。固然彼 node-waf 不是如今在 NPM 倉庫上能搜到的 node-waf 了,當年那個 node-waf 早就年久失修了。
這個東西使用一種叫 wscript 的文件來配置。自 Node.js 升上 0.8 以後,就自帶了 node-gyp 的支持,今後就再也不須要 wscript 了。
不過就是由於有這個青黃交接的時候,那段時間的各類使用 C++ 來開發 Node.js 原生擴展的包爲了兼容 0.8 先後版本的 Node.js,一般都是 binding.gyp 和 wscript 共存的。
你們能夠來看一下 node-mysql-libmysqlclient 這個包在當年相應時間段的時候的倉庫文件。爲了支持 node-gyp,有一個 binding.gyp 文件,而後還存留着 wscript 配置文件。
在早期的時候,Node.js 原生 C++ 模塊開發方式是很是暴力的,直接使用其提供的原生模塊開發頭文件。
開發者直接深刻到 Node.js 的各類 API,以及 Google V8 的 API。
舉個最簡單的例子,在幾年前,你的 Node.js C++ 原生擴展代碼多是長這樣的。
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()); }
這是一個最簡單的 echo
函數,返回傳進來的參數。寫做 JavaScript 至關因而這樣的。
exports.echo = function() { if(arguments.length < 1) throw new Error("Wrong number of arguments."); return arguments[0]; };
遺憾的是,這樣的代碼若是發成一個包,你如今是不管如何沒法安裝的,除非你用的是 0.10.x 的 Node.js 版本。
爲何這麼說呢,這段代碼的確是在 Node.js 0.10.x 的時候能夠用的。可是再往上升 Google V8 的大版本,這段代碼就沒法適用了,講粗暴點就是沒辦法再編譯經過了。
就拿 Node.js 6.x 版本的 Google V8 來講,函數聲明的對比是這樣的:
Handle<Value> Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo<Value>& args); // 6.x
事實上,根本不須要等到 6.x。上面的代碼到 0.12 就已經沒法再編譯經過了。不僅是函數聲明的變化,連句柄做用域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); }
也就是說,以黑暗時代的方式進行 Node.js 原生模塊開發的時候,一個版本只能支持特定幾個版本的 Node.js,一旦 Node.js 的底層 API 以及 Google V8 的 API 發生變化,而這些原生模塊又依賴了變化了的 API 的話,包就做廢了。除非包的維護者去支持新版的 API,不過這樣依賴,老版 Node.js 下就又沒法編譯經過新版的包了。
這就很尷尬了。
在經歷了黑暗時代的尷尬局面以後,2013 年年中,一個救世主忽然現世。
它的名字叫做 NAN,全稱 Native Abstractions for Node.js,即 Node.js 原生模塊抽象接口。
NAN 由 Rod Vagg 和 Benjamin Byholm 兩手帶大,記名在 GitHub 的 Rod Vagg 帳號下。而且在 Node.js 與 io.js 黑歷史的年代,這個在 GitHub 上面項目移到了 io.js 的組織下面;後來因爲兩家又重歸於好,NAN 最終歸屬到了 nodejs 這個組織下面。
總之在 NAN 出現以後,Node.js 的原生開發方式進入了城堡時代,而且一直持續到如今,甚至可能會持續到很久以後。
說 NAN 是 Node.js 原生模塊抽象接口可能仍是有點抽象,那麼講明白點,它就是一堆宏判斷。好比聲明一個函數的時候,只須要經過下面的一個宏就能夠了:
NAN_METHOD(Echo) { }
NAN 的宏會判斷當前編譯時候的 Node.js 版本,根據不一樣版本的 Node.js 來展開不一樣的結果。這會兒就又會提到先前的兩個函數聲明對比了。
Handle<Value> Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo<Value>& args); // 6.x
NAN_METHOD
將會在不一樣版本的 Node.js 下被 NAN 展開成上面兩個這樣。
並且 NAN 可不僅是提供了 NAN_METHOD
一個宏,它還有一坨一坨數不清的宏供開發者使用。
好比聲明句柄做用域的 Nan::HandleScope
、能黑盒調起 libuv8 進行事件循環上的異步操做的 Nan::AsyncWorker
等。
因而,在城堡時代,你們的 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 的升級作改變就好,它會幫你兼容各不一樣 Node.js 版本,使其在任意版本都能被編譯使用。
即便是 NAN 這樣的好物,也有本身的一個使命,使命以外的東西會被逐漸剝離。好比 0.10.x 和 0.12.x 等版本就應該要退出歷史舞臺了,NAN 會逐漸放棄對它們的兼容和支持。
自從前幾天 Node.js v8.0.0 發佈以後,Node.js 推出了全新的用於開發 C++ 原生模塊的接口,N-API。
據官方文檔所述,它的發音就是一個單獨的
N
,加上 API,即四個英文字母單獨發音。
這東西相較於先前三個時代有什麼不一樣呢?爲何會是更進一步的帝國時代呢?
首先,咱們知道,即便是在 NAN 的開發方式下,一次編寫好的代碼在不一樣版本的 Node.js 下也須要從新編譯,不然版本不符的話 Node.js 沒法正常載入一個 C++ 擴展。即一次編寫,處處編譯。
而 N-API 相較於 NAPI 來講,它把 Node.js 的全部底層數據結構所有黑盒化,抽象成 N-API 當中的接口。
不一樣版本的 Node.js 使用一樣的接口,這些接口是穩定地 ABI 化的,即應用二進制接口(Application Binary Interface)。這使得在不一樣 Node.js 下,只要 ABI 的版本號一致,編譯好的 C++ 擴展就能夠直接使用,而不須要從新編譯。事實上,在支持 N-API 接口的 Node.js 中,的確就指定了當前 Node.js 所使用的 ABI 版本。
爲了使得之後的 C++ 擴展開發、維護更方便,N-API 致力於如下的幾個目標:
以 C 的風格提供穩定 ABI 接口;
消除 Node.js 版本的差別;
消除 JavaScript 引擎的差別(如 Google V八、Microsoft ChakraCore 等)。
而這些 API 主要就是用來建立和操做 JavaScript 的值了,咱們就不再用直接使用 Google V8 提供的數據類型了。畢竟在 NAN 中,就算咱們有時候看不到 Google V8 的影子,實際上在宏展開後仍是無數的 Google V8 數據結構。
爲了達成上述隱藏的目標,N-API 的姿式就變成了這樣:
提供頭文件 node_api.h;
任何 N-API 調用都返回一個 napi_status
枚舉,來表示此次調用成功與否;
N-API 的返回值因爲被 napi_status
佔坑了,因此真實返回值由傳入的參數來繼承,如傳入一個指針讓函數操做;
全部 JavaScript 數據類型都被黑盒類型 napi_value
封裝,再也不是相似於 v8::Object
、v8::Number
等類型;
若是函數調用不成功,能夠經過 napi_get_last_error_info
函數來獲取最後一次出錯的信息。
注意:哪怕是如今的 Node.js v8.x 版本,N-API 仍處於一個實驗狀態,我的認爲還有很是長的一段路要走,因此你們在生產環境中還沒必要太過於激進,不過 N-API 依然是大勢所趨;不過對於使用老版本的 Node.js 開發者來講,你們也不要着急,即便 N-API 是在 v8.x 才正式集成進 Node.js,在其它舊版本的 Node.js 中依然能夠將 N-API 做爲外掛式的頭文件9中使用,只不過沒法作到跨版本的特性,這只是它作的向後兼容的一個事情而已。
關於 N-API 一系列的函數能夠訪問它的文檔瞭解更多詳情,如今咱們來點料兒讓你們對 N-API 的印象不是那麼抽象。
在封建時代和 NAN 所處的,模塊的初始化是交給 Node.js 提供的宏來實現的。
NODE_MODULE(addon, Init)
而到了當前的 N-API,它就變成了 N-API 的一個宏了。
NAPI_MODULE(addon, Init)
相應地,這個初始化函數 Init
的寫法也會有所改變。好比這是封建時代和 NAN 時代的兩種不一樣寫法:
// 暴力寫法 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 的時候,這個 Init
函數就該是這樣的了。
void Init(napi_env env, napi_value exports, napi_value module, void* priv) { napi_status status; // 用於設置 exports 對象的描述結構體 napi_property_descriptor desc = { "echo", 0, Echo, 0, 0, 0, napi_default, 0 }; // 把 "echo" 設置到 exports 去 status = napi_define_properties(env, exports, 1, &desc); }
napi_property_descriptor
是用於設置對象屬性的描述結構體,它的聲明以下: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
函數中的desc
意思就是,即將被掛在的對象下會掛一個叫"echo"
的東西,它的函數是Echo
,其它的getter
、setter
等全是空指針,而屬性則是napi_default
。
napi_property_attributes
除了napi_default
以外,還有諸如只讀、是否可枚舉等屬性。
還記得以前的兩種函數聲明嗎?第三次再搬過來。
Handle<Value> Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo<Value>& args); // 6.x
在 N-API 中,你不用再被告知須要有 C++ 基礎,C 便可。由於在 N-API 裏面,聲明一個 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 其實是一個指針,返回空指針表示無返回值 } return argv[0]; }
重要:目前 8.0.0 和 8.1.0 版本的 Node.js 官方文檔中,關於 N-API 的各類接口文檔錯誤頗多,因此仍是要以能使用的接口爲準。
並且如今你們也有不少人正在幫忙一塊兒修復文檔。例如如今的 JavaScript 函數聲明返回值實際上是
napi_value
,而官方文檔上仍是老舊的void
。又好比`napi_property_descriptor_desc
結構體中,在utf8name
以後還有一個napi_value
的變量,而文檔中倒是沒有的。這也是爲何我前面強調目前來講 N-API 還處於試驗階段。畢竟 API 並無徹底穩定下來,還處於一個快速迭代的步伐中,文檔的更新並未跟上代碼的更新。至少在筆者寫做的當前是這樣的(如今日期 2017 年 6 月 9 日)。
上面代碼分步解析。
經過 napi_get_cb_info
獲取當次函數請求的參數信息,包括參數數量和參數體(參數體以 napi_value
的數組形式體現);
看看解析有無出錯(status
不等於 napi_ok
)或者看看參數數量是否小於 1;
若解析出錯或者參數數量小於 1,經過 napi_throw_type_error
在 JavaScript 層拋出一個錯誤對象,並返回;
若無錯則繼續進行;
返回 argv[0]
,即第一個參數。
這裏放上這個 Echo 樣例的完整代碼,你們能夠拿回家試試看。
{ "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 <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)
在完成了代碼以後,你們趕忙試一下代碼吧。
首先在 Node.js v8.x 下進行試驗,把這兩段代碼分別放到同一個目錄下,命名好後,執行這樣的終端命令:
$ 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)
注意:仍是由於試驗特性,目前在 Node.js v8.x 要加載和執行 N-API 的 C++ 擴展的話,在啓動
node
的時候須要加上--napi-modules
參數,表示此次執行要啓用 N-API 特性。
效果顯而易見,在剛啓動 Node.js REPL 的時候,你會獲得一個警告。
(node:52264) Warning: N-API is an experimental feature and could change at any time
表示它目前還不是特別穩定,可是值得咱們展望將來。而後在咱們 require()
擴展的時候,咱們就獲得了一個擁有 echo
函數的對象了。
咱們嘗試了三種調用方式。第一次是規規矩矩傳入一個參數,echo
如期返回咱們傳入的參數 "2333"
;第二次傳入兩個參數,echo
返回了第一個參數 "蛋花湯?"
;最後一次咱們沒傳任何參數,這個時候就走到了 C++ 擴展中判斷函數參數數量失敗的條件分支,就拋出了一個 Wrong number of arguments
的錯誤對象。
總之,它按照咱們的預期跑起來了。而且代碼裏面並無任何 Node.js 非 N-API 所暴露出來的數據結構和 V8 的數據結構——版本差別消除了。
接下來激動人心的時刻到了,若是讀者是使用 nvm
來管理本身的 Node.js 版本的話,能夠嘗試着安裝一個 8.1.0 的 Node.js 版本。
$ nvm install 8.1.0
在安裝成功切換版本成功後,嘗試着直接打開 Node.js RELP,忘掉再次編譯剛纔編譯好的擴展這一步。(不過別忘了 --napi-module
參數)
把剛纔用於測試的幾句 JavaScript 代碼再重複地輸入——N-API 誠不我欺,竟然仍是能輸出結果。這對於之前的暴力作法和 NAN 作法來講,無疑是很是大的一個進步。
至此,我但願你們尚未忘記 N-API 是自 Node.js 8.0 以後出的特性。因此以前 Demo 的代碼並不能在 Node.js 8.0 以前的版本如期編譯和運行。
辛辛苦苦寫好的包,竟然不能在 Node.js 6.x 下面跑,搞什麼。
先別急着摔。文中以前也說了,有一個外掛式頭文件的包,其包名是 node-addon-api
。
咱們就試着經過它來進行向下兼容吧。首先在咱們剛纔的源碼目錄把這個包給安裝上。
$ npm install --save node-addon-api
仍是因爲快速迭代的緣由,我不能保證這個包當前版本的時效性,不過我相信你們都有探索精神,在將來版本不符致使的 API 不符的問題應該都能解決。
而後,給咱們的 binding.gyp 函數加點料,加兩個字段,裏面是兩個指令展開。
"include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], "dependencies": [ "<!(node -p \"require('node-addon-api').gyp\")" ]
<!@
和 <!
開頭的字符串在 GYP 中表明指令,表示它的值是後面的指令的執行結果。上面兩條指令的返回結果分別是外掛式頭文件的頭文件搜索路徑,以及外掛式 N-API 這個包編譯成靜態鏈接庫供咱們本身的包使用的依賴聲明。
有了這兩個字段後,就表示咱們依賴了外掛式 N-API 頭文件。並且它內部自帶判斷,若是版本已經達到了有 N-API 的要求,它的依賴就會是一個空依賴,即不依賴外掛式 N-API 編譯的靜態鏈接庫。
也就是說,用了外掛式的 N-API,能自動適配 Node.js 8.x 和低版本。
因而這個 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 } }, "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], "dependencies": [ "<!(node -p \"require('node-addon-api').gyp\")" ] }] }
至於源碼層面,咱們就不須要做任何修改。在 Node.js v6.x 下面試試看吧。一樣是使用 node-gyp rebuild 進行編譯。而後經過 Node.js REPL 進去測試。
具體的終端輸出這裏就不放出來了,相信通過實驗的你們都獲得了本身想要的結果。
本次內容主要講解了在 Node.js 領域中原生 C++ 模塊開發的方式變遷。
從 node-waf 到 node-gyp,這是構建工具的一個變遷,將來說不定會是 GN 或者其它的構建工具。
從暴力寫碼,到 NAN 的出現,見證了 Node.js 社區的各類愛恨情仇,一直到如今的新生兒 N-API,爲原生 C++ 模塊的開發輸送了新鮮的血液。
目前的中堅力量仍然是 NAN 的開發方式,甚至我猜想是否將來有可能 NAN 會提供關於 N-API 的各類宏封裝,使其完全消除版本差別,包括 ABI 版本上的差別。固然這種 ABI 版本差別致使的須要屢次編譯問題應該仍是存在的,這裏指的是一次編碼的差別。
在你們跟着本文對 N-API 進行了一次淺嘗輒止的嘗試以後,但願能對當下仍然處於實驗狀態的 N-API 充滿了希冀,並對如今存在的各類坑處以包容的心態。
畢竟,Node.js loves you all。
「Consider moving from gyp to gn」:https://github.com/nodejs/nod...
「Getting Started with Embedding · v8/v8 Wiki」:https://github.com/v8/v8/wiki...
「Drop support for v0.10 and v0.12?」:https://github.com/nodejs/nan...
「Node Loves Rust」:https://cnodejs.org/topic/593...
「N-API | Node.js v8.0.0 Documentation」:https://nodejs.org/docs/v8.0....
「doc: fix out of date sections in n-api doc」:https://github.com/nodejs/nod...