從暴力到 NAN 再到 NAPI——Node.js 原生模塊開發方式變遷

本文由我首發於 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

ons.node 二進制內容

眼尖的同窗會看到它的 Magic Number10x7F454C46,其按位的 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 的 DLOpen2 函數吧:

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;
  }
}

按照邏輯來說,這個加載過程其實就是下面這樣的。

  1. 經過 uv_dlopen 加載連接庫。

  2. 將加載的連接庫掛到原生模塊鏈表中去。

  3. 經過 mp->nm_register_func() 初始化這個模塊,並獲得該有的 modulemodule.exports

流程走下來就跟這個流程圖差很少。

DLOpen 流程圖

node-gyp

這貨是 Node.js 中編譯原生模塊用的。自從 Node.js v0.8 以後,它就跟 Node.js 黏上了,在此以前它的默認編譯幫助包是 node-waf3,對於老 Noder 來講應該不會陌生的。

GYP

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 個模塊名分別是 addon1addon4

  • 隱藏故事:經過正規途徑編譯好後,這些模塊存在於 build/Release/addon*.node 中。

關於 GYP 配置文件的更多內容,你們可自行去官方文檔觀摩,在腳註中有 GYP 的連接。

作的事情

node-gyp 除了自身是基於 GYP 的以外,它還作了一些額外的事情。首先,在咱們編譯一個 C++ 原生擴展的時候,它會去指定目錄下(一般是 ~/.node-gyp 目錄下)搜咱們當前 Node.js 版本的頭文件和靜態鏈接庫文件,若不存在,它就會火急火燎跑去 Node.js 官網下載。

這是一個 Windows 下 node-gyp 下載的指定版本 Node.js 頭文件和庫文件的目錄結構。

Windows 下的 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:至關於依次執行了 cleanconfigurebuild

  • $ node-gyp install:手動下載當前版本的 Node.js 的頭文件和庫文件到對應目錄。

時代在召喚

第 N 套國際 Node.js 開發者原生 C++ 模塊開發方式,時代在召喚。

除去前文中講的一些不變的內容,還有不少內容是一直在變化的,雖說用老舊的開發方式也是能夠開發出能用的 C++ 原生模塊,可是舊不如新。

並且,其實目前來講 node-gyp 的地位也有可能在將來進行變化。由於當年 Chromium 是經過 GYP 來管理它的構建配置的,現現在已經步入了 GN6 的殿堂,是否也意味着 node-gyp 有一天也會被可能叫作 node-gn 的東西給取代呢?

話很少說,先來看看滄海桑田的故事吧。

黑暗時代:node-waf

在 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 下就又沒法編譯經過新版的包了。

這就很尷尬了。

城堡時代:Native Abstractions for Node.js

在經歷了黑暗時代的尷尬局面以後,2013 年年中,一個救世主忽然現世。

它的名字叫做 NAN,全稱 Native Abstractions for Node.js,即 Node.js 原生模塊抽象接口。

NAN 由 Rod VaggBenjamin 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 會逐漸放棄對它們的兼容和支持。

帝國時代:符合 ABI 的 N-API

自從前幾天 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::Objectv8::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,其它的 gettersetter 等全是空指針,而屬性則是 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],即第一個參數。

Demo 完整代碼

這裏放上這個 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)

乘風破浪

在完成了代碼以後,你們趕忙試一下代碼吧。

首先在 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。

參考資料


  1. 用於定義某種文件類型的特殊標識,詳見 https://en.wikipedia.org/wiki...
  2. 代碼參見 https://github.com/nodejs/nod...
  3. 年久失修,當前 NPM 上搜索到的 node-waf 已經不是當年的了,不過這個是 Waf 的官方倉庫 https://github.com/waf-projec...
  4. 全稱 Generate Your Projects,是谷歌開發的一套構建系統,未盡事宜詳詢 https://gyp.gsrc.io
  5. GYP 的配置文件的後綴就是 .gyp 或者 .gypi 等,是個類 JSON 文件。
  6. GN 是谷歌開發的相較於 GYP 更新更快的一套構建工具,能夠參考 https://chromium.googlesource...
  7. 讓垃圾回收機制來管理 JavaScript 對象生命週期的一種類,即 HandleScope,在個人新書中將會有詳解。
  8. Node.js 的異步事件循環支撐者,詳詢 http://www.libuv.org/
相關文章
相關標籤/搜索