如何开发 Node.js Native Add-on?
簡介:?來一起為 Node.js 的 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 的負擔。
目前 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
不多說,我們先來看看過去幾年被添加到 N-API 中的新特性吧。
新特性
越來越多的開發者們開始使用 N-API 與 node-addon-api 開發 Node.js add-on,我們也不斷地為 N-API 和 node-addon-api 添加新的關鍵特性和改進 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 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 修復問題、增加測試用例;
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的如何开发 Node.js Native Add-on?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云上虚拟IDC(私有池)如何为客户业务的
- 下一篇: 【ESSD技术解读-04】ESSD Au