天天看點

如何開發 Node.js Native Add-on?

作者 | 吳成忠(昭朗)
如何開發 Node.js Native Add-on?
這篇文章是由 Chengzhong Wu (@legendecas),Gabriel Schulhof (@gabrielschulhof) ,Jim Schlight (@jimschlight),Kevin Eady,Michael Dawson (@mhdawson1),Nicola Del Gobbo (@NickNaso) 等人編寫的,首發在 Node.js Medium 部落格。

關于N-API

N-API 為 Node.js 帶來了一個 ABI 穩定的 add-on API,簡化了建構和開發支援跨 Node.js 版本的 add-on 的負擔。

如何開發 Node.js Native Add-on?

目前 N-API 的 C++ 封裝 node-addon-api 每周的下載下傳量已經超過了 250萬次,并且所有 Node.js LTS(長期支援版本)都已經支援了 N-API v3 或者更高版本 ,Node.js 15.x 更已經開始支援最新的 N-API v7。是以我們認為這是一個非常好的時間點來回頭看一看目前 Node.js add-on 的開發體驗。

當我們在 2016 年開始投入 N-API 的工作(最開始的提案是在 2016 年 12 月 12 日提出的),我們就知道這會是一個非常長期的任務。Node.js 社群生态中已經有非常多現存的包,是以這個遷移過程将會持續相當長的一段時間。

不過好消息是,從最初的想法,到現在這段路程我們已經走過了非常長的路途。許許多多的困難已經由多位 Node.js Collaborator、N-API 團隊和子產品包作者們攻克。目前,N-API 已經成為了預設、推薦的編寫 Node.js add-on 的方式。

随着 N-API 的發展,不斷有新的 API 加入到 N-API 中去來滿足 Node.js 子產品包作者将他們的庫向 N-API 遷移中提出新需求,當然這個過程也按照我們預先的設計 N-API 一直保持着穩定、向前相容性。

我們也非常高興地看到這些子產品包作者們的積極回報,比如

https://twitter.com/mafintosh/status/1256180505210433541
如何開發 Node.js Native Add-on?

不多說,我們先來看看過去幾年被添加到 N-API 中的新特性吧。

新特性

越來越多的開發者們開始使用 N-API 與 node-addon-api 開發 Node.js add-on,我們也不斷地為 N-API 和 node-addon-api 添加新的關鍵特性和改進 add-on 開發體驗。

如何開發 Node.js Native Add-on?

這些改進可以分為 3 個主要的類别,我們下文将一一介紹。

多線程與異步程式設計

随着 Node.js 的使用在開發者群體中越來越顯著,需要與 OS 接口、異步事件打交道的需求也越來越旺盛。Node.js 是一個 JavaScript 單線程模型的實作,一個 Node.js 環境隻會有一個主線程可以通路 JavaScript 值。

是以,在主線程執行重 CPU 的任務就會導緻 JavaScript 程式被阻塞,導緻事件與回調都堆積在事件隊列中。為了改程序式的跨線程資料完整性的開發體驗,我們收集了非常多的真實案例的需求,在 N-API 和 N-API 的 C++ 封裝 node-addon-api 中都帶來了多種機制來解決工作線程回調回 JavaScript 線程的問題。根據使用場景,可以分為:

  • AsyncWorker,提供單向、單次的回調任務封裝,可以通知 JavaScript 這個任務的最終執行結果或者異常資訊;
  • AsyncProgressWorker,與 AsyncWorker 類似,提供單向、單次的回調任務封裝,不過增加了向 JavaScript 異步傳遞進度資訊的機制;
  • Thread-safe functions,提供了從任意線程、任意數量的線程、任意時間點向 Node.js JavaScript 線程回調的機制。

多 Node.js 上下文支援

Node.js 近期最讓人興奮的特性之一就是 [worker_threads],它提供了一個完整的、但是獨立于 Node.js 主 JavaScript 線程的并發執行的 Node.js JavaScript 執行線程。這也意味着 Node.js 的 add-on 也同樣可以在這些 worker 線程中随着這些 worker 的啟動與銷毀被多次加載、解除安裝。

不過因為這些同一個程序中的 worker 線程是共享了同一個記憶體空間的,多個 add-on 的執行個體必須考慮到多個 worker 線程的同時存在的可能性。另外,每一個 Node.js 程序隻會加載了一次這些 add-on 的動态庫,這意味着這些 add-on 線程不安全的全局屬性(比如全局靜态變量)可以被多個線程同時通路,也就不能再這麼簡單粗暴地存儲了。

類似的,C++ 類的靜态資料成員也是通過線程不安全的方式存儲的,是以這個方式也需要被避免。另外,其實對于 add-on 來說,Node.js 也不保證單個線程隻會用來執行一個 worker,是以 thread-local 也應該被避免。

在 N-API v6 中,我們為每一個 Node.js 執行個體(主線程 JavaScript 執行個體、worker 執行個體等)都引入了一個用來給 add-on 使用的存儲空間。這樣,add-on 在一個程序中就可以獲得對于單個 Node.js 執行個體唯一的存儲空間了。同時我們也提供了一些輔助方法來幫助 add-on 開始使用這個特性:

  • NAPI_MODULE_INIT()

    宏,會将 add-on 标記為可以被 Node.js 在同一個程序中可以多次加載、解除安裝的子產品。
  • napi_get_instance_data()

    napi_set_instance_data()

    用來安全地通路單個 Node.js 執行個體給 add-on 建立的全局唯一存儲空間;
  • node-addon-api 還提供了

    Addon<T>

    類,這個類包裝了上面說所的方法,以 C++ 友好的方式封裝了這個給予 add-on 可以在不同的 worker 線程中使用的存儲空間。是以,add-on 開發者可以将 add-on 的資料比如全局變量通過

    Addon<T>

    來存儲并建立,而 Node.js 則會負責在目前線程使用這個 add-on 的時候建立這片空間。
如何開發 Node.js Native Add-on?

其他輔助函數

除了以上幾個重要功能之外,我們也發現了許多在維護 Node.js add-on 的過程中經常會使用到的類型方法與函數,包括:

  • Date 對象;
  • BigInts;
  • 從 JavaScript 對象上擷取任意鍵(如 Symbol 等);
  • 将 Add-on 建立的 ArrayBuffer 底層存儲從 ArrayBuffer 上脫離;

建構

建構工作流對于 Node.js add-on 維護者與 add-on 使用者來說是非常重要的一個環節,也是N-API 團隊其中一個工作重心,比如 CMake.js, node-pre-gyp 和 prebuild。

曾經 Node.js add-on 隻能使用 node-gyp 來建構。對于一些已經在使用 CMake 的庫來說,CMake.js 就是除了 node-gyp 依賴用來建構 add-on 的一個非常吸引人的選項。我們也已經釋出了一個使用 CMake 建構 add-on 的例子。

其他關于如何将 CMake.js 與 N-API add-on 一起使用的詳細資訊可以在 N-API Resource 擷取到。

開發 Node.js add-on 之後一個重要的現實問題就是在 npm install 時,add-on 的 C/C++ 代碼必須在本地編譯、連結。這個編譯過程需要本地安裝有一個可以正常使用的 C/C++ 工具鍊。而這個依賴通常會成為沒有安裝這些工具鍊的 add-on 使用者使用這個 add-on 的一個阻礙。現行的方案對于這個問題一般都是預先建構二進制包,然後在安裝時直接下載下傳這些預先建構的包。

有許多工具可以用來預先建構二進制包。node-pre-gyp 通常會将建構出來的二進制包上傳到 AWS S3。prebuild 也類似,不過是将包上傳到 GitHub Release。

prebuildify 則是另外一個可選項。而 prebuildify 相比于上述的工具來說,優點在于在 npm install 安裝好時,本地就已經有這些二進制包了,而不需要再次從第三方服務上下載下傳。雖然安裝的 npm 包可能會更大,不過在實際實踐中因為不需要再次從 AWS 或者 GitHub 上下載下傳,整個安裝過程會相對更加快速。

開始上手

我們已經在 GitHub 上準備了非常多的 node-addon-examples 來給開發者快速了解常見場景該如何使用 N-API 和 node-addon-api 來開發 Node.js add-on。這個倉庫的根目錄包含了許多的檔案夾,這些檔案夾就代表了不同的使用場景,比如從簡單的 Hello World add-on,到複雜的多線程 add-on。每一個樣例目錄會包含 3 個子目錄,分别代表了傳統的 NAN,N-API,和 node-addon-api 開發 add-on 的例子。我們可以直接運作下面的指令,立刻從 Hello World 的例子開始使用 node-addon-api:

$ git clone https://github.com/nodejs/node-addon-examples.git
$ cd node-addon-examples/1_hello_world/node-addon-api/
$ npm i
$ node .           

另一個重要的資源就是 N-API Resource。這個網站包含了開發、建構 Node.js add-on 的從入門到深入的許多資訊與資料,比如

  • 上手所需的工具;
  • 從 NAN 向 N-API 的遷移導引;
  • 不同建構系統的對比(node-gyp,CMake 等等);
  • 多 Node.js 上下文支援和線程安全。

結語

從 Node.js 誕生之初,Node.js 就支援通過 C/C++ 代碼來給 JavaScript 暴露更多的特性接口。随着時間積累,我們也認識到實作、維護、分發這些 add-on 一直存在許許多多的難點。而 N-API 就被 add-on 維護者們認為是解決這些難點的一個非常核心的領域。是以整個N-API 團隊和社群都開始為 Node.js 核心建立起這樣一套 ABI 穩定的 add-on API。

而代表了 N-API 的這些 C API 現在已經是每一個 Node.js 釋出版本的一部分,并且我們也有了可以通過 npm 安裝的 node-addon-api 來提供這些 C API 的 C++ 封裝。N-API 在誕生之初,就是以在不同 Node.js 版本之間,甚至是 Major 版本之間保證 ABI 與 API 相容性為目标,而這也已經可以證明能夠提供更多額外的好處:

  • 我們不再需要在切換 Node.js 大版本之後重新編譯 add-on 子產品;
  • 我們可以在除了使用 V8 作為 JavaScript 引擎的 Node.js 之外的運作環境實作 N-API,也意味着這些為 Node.js 開發的 add-on 無需修改任何代碼即可相容這些運作環境,比如 Babylon Native,IoT.js 和 Electron。
  • N-API 是單純的 C API,這意味着我們可以使用 C/C++ 之外的語言、運作時開發 Node.js add-on,比如 Go 或者是 Rust。

N-API 從 Node.js v8.0.0 開始以實驗性功能釋出到現在,雖然廣泛應用的過程比較緩慢,但是子產品開發者們也不斷地給我們送出回報與貢獻,這也幫助我們不斷地增加新特性和開發新的工具來幫助開發者們建構一個更好的 add-on 生态。

今天,N-API 在 add-on 的開發中使用已經非常廣泛。比如一些使用非常多的 add-on 子產品都已經遷移至基于 N-API 開發:

  • sharp (每周 ~900k 下載下傳量)
  • bcrypt (每周 ~500k 下載下傳量)
  • sqlite3 (每周 ~300k 下載下傳量)

在過去的幾年中,N-API 獲得了非常多的改進。而對于 add-on 開發者與使用者來說,這也給他們帶來了接近于原生 JavaScript 子產品的開發、使用體驗。

開始貢獻

我們在持續不斷地改進 N-API 和 Node.js 的 add-on 生态,但是我們也一直非常需要幫助。你可以在以下途徑在多種場景幫助 N-API 做的更好:

  • 将你的 add-on 遷移到 N-API;
  • 幫助你的應用依賴的 add-on 遷移到 N-API;
  • 為 N-API 提出、實作新的特性;
  • 為 node-addon-api 提出、實作新的基于 N-API 的特性;
  • 為 node-addon-api 修複問題、增加測試用例;
  • 為 node-addon-examples 修複問題、增加測試用例;

如果你對加入我們的工作感興趣,可以查閱

https://github.com/nodejs/abi-stable-node#meeting

來加入我們每周的工作組會議。

如何開發 Node.js Native Add-on?

繼續閱讀