如何開(kāi)發(fā) Node.js Native Add-on?
時(shí)間:2023-05-25 03:06:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2023-05-25 03:06:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
如何開(kāi)發(fā) Node.js Native Add-on?:
這篇文章是由 Chengzhong Wu (@legendecas),Gabriel Schulhof (@gabrielschulhof) ,Jim Schlight (@jimschlight),Kevin Eady,Michael Dawson (@mhdawson1),Nicola Del Gobbo (@NickNaso) 等人編寫(xiě)的,首發(fā)在 Node.js Medium 博客。
關(guān)于N-API
N-API 為 Node.js 帶來(lái)了一個(gè) ABI 穩(wěn)定的 add-on API,簡(jiǎn)化了構(gòu)建和開(kāi)發(fā)支持跨 Node.js 版本的 add-on 的負(fù)擔(dān)。
目前 N-API 的 C++ 封裝 node-addon-api 每周的下載量已經(jīng)超過(guò)了 250萬(wàn)次,并且所有 Node.js LTS(長(zhǎng)期支持版本)都已經(jīng)支持了 N-API v3 或者更高版本 ,Node.js 15.x 更已經(jīng)開(kāi)始支持最新的 N-API v7。所以我們認(rèn)為這是一個(gè)非常好的時(shí)間點(diǎn)來(lái)回頭看一看目前 Node.js add-on 的開(kāi)發(fā)體驗(yàn)。
當(dāng)我們?cè)?2016 年開(kāi)始投入 N-API 的工作(最開(kāi)始的提案是在 2016 年 12 月 12 日提出的),我們就知道這會(huì)是一個(gè)非常長(zhǎng)期的任務(wù)。Node.js 社區(qū)生態(tài)中已經(jīng)有非常多現(xiàn)存的包,所以這個(gè)遷移過(guò)程將會(huì)持續(xù)相當(dāng)長(zhǎng)的一段時(shí)間。
不過(guò)好消息是,從最初的想法,到現(xiàn)在這段路程我們已經(jīng)走過(guò)了非常長(zhǎng)的路途。許許多多的困難已經(jīng)由多位 Node.js Collaborator、N-API 團(tuán)隊(duì)和模塊包作者們攻克。目前,N-API 已經(jīng)成為了默認(rèn)、推薦的編寫(xiě) Node.js add-on 的方式。
隨著 N-API 的發(fā)展,不斷有新的 API 加入到 N-API 中去來(lái)滿足 Node.js 模塊包作者將他們的庫(kù)向 N-API 遷移中提出新需求,當(dāng)然這個(gè)過(guò)程也按照我們預(yù)先的設(shè)計(jì) N-API 一直保持著穩(wěn)定、向前兼容性。
我們也非常高興地看到這些模塊包作者們的積極反饋,比如
https://twitter.com/mafintosh/status/1256180505210433541不多說(shuō),我們先來(lái)看看過(guò)去幾年被添加到 N-API 中的新特性吧。
新特性
越來(lái)越多的開(kāi)發(fā)者們開(kāi)始使用 N-API 與 node-addon-api 開(kāi)發(fā) Node.js add-on,我們也不斷地為 N-API 和 node-addon-api 添加新的關(guān)鍵特性和改進(jìn) add-on 開(kāi)發(fā)體驗(yàn)。
這些改進(jìn)可以分為 3 個(gè)主要的類(lèi)別,我們下文將一一介紹。
多線程與異步編程隨著 Node.js 的使用在開(kāi)發(fā)者群體中越來(lái)越顯著,需要與 OS 接口、異步事件打交道的需求也越來(lái)越旺盛。Node.js 是一個(gè) JavaScript 單線程模型的實(shí)現(xiàn),一個(gè) Node.js 環(huán)境只會(huì)有一個(gè)主線程可以訪問(wèn) JavaScript 值。
因此,在主線程執(zhí)行重 CPU 的任務(wù)就會(huì)導(dǎo)致 JavaScript 程序被阻塞,導(dǎo)致事件與回調(diào)都堆積在事件隊(duì)列中。為了改進(jìn)程序的跨線程數(shù)據(jù)完整性的開(kāi)發(fā)體驗(yàn),我們收集了非常多的真實(shí)案例的需求,在 N-API 和 N-API 的 C++ 封裝 node-addon-api 中都帶來(lái)了多種機(jī)制來(lái)解決工作線程回調(diào)回 JavaScript 線程的問(wèn)題。根據(jù)使用場(chǎng)景,可以分為:
- AsyncWorker,提供單向、單次的回調(diào)任務(wù)封裝,可以通知 JavaScript 這個(gè)任務(wù)的最終執(zhí)行結(jié)果或者異常信息;
- AsyncProgressWorker,與 AsyncWorker 類(lèi)似,提供單向、單次的回調(diào)任務(wù)封裝,不過(guò)增加了向 JavaScript 異步傳遞進(jìn)度信息的機(jī)制;
- Thread-safe functions,提供了從任意線程、任意數(shù)量的線程、任意時(shí)間點(diǎn)向 Node.js JavaScript 線程回調(diào)的機(jī)制。
多 Node.js 上下文支持Node.js 近期最讓人興奮的特性之一就是 [worker_threads],它提供了一個(gè)完整的、但是獨(dú)立于 Node.js 主 JavaScript 線程的并發(fā)執(zhí)行的 Node.js JavaScript 執(zhí)行線程。這也意味著 Node.js 的 add-on 也同樣可以在這些 worker 線程中隨著這些 worker 的啟動(dòng)與銷(xiāo)毀被多次加載、卸載。
不過(guò)因?yàn)檫@些同一個(gè)進(jìn)程中的 worker 線程是共享了同一個(gè)內(nèi)存空間的,多個(gè) add-on 的實(shí)例必須考慮到多個(gè) worker 線程的同時(shí)存在的可能性。另外,每一個(gè) Node.js 進(jìn)程只會(huì)加載了一次這些 add-on 的動(dòng)態(tài)庫(kù),這意味著這些 add-on 線程不安全的全局屬性(比如全局靜態(tài)變量)可以被多個(gè)線程同時(shí)訪問(wèn),也就不能再這么簡(jiǎn)單粗暴地存儲(chǔ)了。
類(lèi)似的,C++ 類(lèi)的靜態(tài)數(shù)據(jù)成員也是通過(guò)線程不安全的方式存儲(chǔ)的,所以這個(gè)方式也需要被避免。另外,其實(shí)對(duì)于 add-on 來(lái)說(shuō),Node.js 也不保證單個(gè)線程只會(huì)用來(lái)執(zhí)行一個(gè) worker,所以 thread-local 也應(yīng)該被避免。
在 N-API v6 中,我們?yōu)槊恳粋€(gè) Node.js 實(shí)例(主線程 JavaScript 實(shí)例、worker 實(shí)例等)都引入了一個(gè)用來(lái)給 add-on 使用的存儲(chǔ)空間。這樣,add-on 在一個(gè)進(jìn)程中就可以獲得對(duì)于單個(gè) Node.js 實(shí)例唯一的存儲(chǔ)空間了。同時(shí)我們也提供了一些輔助方法來(lái)幫助 add-on 開(kāi)始使用這個(gè)特性:
- NAPI_MODULE_INIT() 宏,會(huì)將 add-on 標(biāo)記為可以被 Node.js 在同一個(gè)進(jìn)程中可以多次加載、卸載的模塊。
- napi_get_instance_data() 和 napi_set_instance_data() 用來(lái)安全地訪問(wèn)單個(gè) Node.js 實(shí)例給 add-on 創(chuàng)建的全局唯一存儲(chǔ)空間;
- node-addon-api 還提供了 Addon<T> 類(lèi),這個(gè)類(lèi)包裝了上面說(shuō)所的方法,以 C++ 友好的方式封裝了這個(gè)給予 add-on 可以在不同的 worker 線程中使用的存儲(chǔ)空間。因此,add-on 開(kāi)發(fā)者可以將 add-on 的數(shù)據(jù)比如全局變量通過(guò) Addon<T> 來(lái)存儲(chǔ)并創(chuàng)建,而 Node.js 則會(huì)負(fù)責(zé)在當(dāng)前線程使用這個(gè) add-on 的時(shí)候創(chuàng)建這片空間。
其他輔助函數(shù)除了以上幾個(gè)重要功能之外,我們也發(fā)現(xiàn)了許多在維護(hù) Node.js add-on 的過(guò)程中經(jīng)常會(huì)使用到的類(lèi)型方法與函數(shù),包括:
- Date 對(duì)象;
- BigInts;
- 從 JavaScript 對(duì)象上獲取任意鍵(如 Symbol 等);
- 將 Add-on 創(chuàng)建的 ArrayBuffer 底層存儲(chǔ)從 ArrayBuffer 上脫離;
構(gòu)建
構(gòu)建工作流對(duì)于 Node.js add-on 維護(hù)者與 add-on 使用者來(lái)說(shuō)是非常重要的一個(gè)環(huán)節(jié),也是N-API 團(tuán)隊(duì)其中一個(gè)工作重心,比如 CMake.js, node-pre-gyp 和 prebuild。
曾經(jīng) Node.js add-on 只能使用 node-gyp 來(lái)構(gòu)建。對(duì)于一些已經(jīng)在使用 CMake 的庫(kù)來(lái)說(shuō),CMake.js 就是除了 node-gyp 依賴(lài)用來(lái)構(gòu)建 add-on 的一個(gè)非常吸引人的選項(xiàng)。我們也已經(jīng)發(fā)布了一個(gè)使用 CMake 構(gòu)建 add-on 的例子。
其他關(guān)于如何將 CMake.js 與 N-API add-on 一起使用的詳細(xì)信息可以在 N-API Resource 獲取到。
開(kāi)發(fā) Node.js add-on 之后一個(gè)重要的現(xiàn)實(shí)問(wèn)題就是在 npm install 時(shí),add-on 的 C/C++ 代碼必須在本地編譯、鏈接。這個(gè)編譯過(guò)程需要本地安裝有一個(gè)可以正常使用的 C/C++ 工具鏈。而這個(gè)依賴(lài)通常會(huì)成為沒(méi)有安裝這些工具鏈的 add-on 用戶(hù)使用這個(gè) add-on 的一個(gè)阻礙?,F(xiàn)行的方案對(duì)于這個(gè)問(wèn)題一般都是預(yù)先構(gòu)建二進(jìn)制包,然后在安裝時(shí)直接下載這些預(yù)先構(gòu)建的包。
有許多工具可以用來(lái)預(yù)先構(gòu)建二進(jìn)制包。node-pre-gyp 通常會(huì)將構(gòu)建出來(lái)的二進(jìn)制包上傳到 AWS S3。prebuild 也類(lèi)似,不過(guò)是將包上傳到 GitHub Release。
prebuildify 則是另外一個(gè)可選項(xiàng)。而 prebuildify 相比于上述的工具來(lái)說(shuō),優(yōu)點(diǎn)在于在 npm install 安裝好時(shí),本地就已經(jīng)有這些二進(jìn)制包了,而不需要再次從第三方服務(wù)上下載。雖然安裝的 npm 包可能會(huì)更大,不過(guò)在實(shí)際實(shí)踐中因?yàn)椴恍枰俅螐?AWS 或者 GitHub 上下載,整個(gè)安裝過(guò)程會(huì)相對(duì)更加快速。
開(kāi)始上手
我們已經(jīng)在 GitHub 上準(zhǔn)備了非常多的 node-addon-examples 來(lái)給開(kāi)發(fā)者快速了解常見(jiàn)場(chǎng)景該如何使用 N-API 和 node-addon-api 來(lái)開(kāi)發(fā) Node.js add-on。這個(gè)倉(cāng)庫(kù)的根目錄包含了許多的文件夾,這些文件夾就代表了不同的使用場(chǎng)景,比如從簡(jiǎn)單的 Hello World add-on,到復(fù)雜的多線程 add-on。每一個(gè)樣例目錄會(huì)包含 3 個(gè)子目錄,分別代表了傳統(tǒng)的 NAN,N-API,和 node-addon-api 開(kāi)發(fā) add-on 的例子。我們可以直接運(yùn)行下面的命令,立刻從 Hello World 的例子開(kāi)始使用 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 .
另一個(gè)重要的資源就是 N-API Resource。這個(gè)網(wǎng)站包含了開(kāi)發(fā)、構(gòu)建 Node.js add-on 的從入門(mén)到深入的許多信息與資料,比如
- 上手所需的工具;
- 從 NAN 向 N-API 的遷移導(dǎo)引;
- 不同構(gòu)建系統(tǒng)的對(duì)比(node-gyp,CMake 等等);
- 多 Node.js 上下文支持和線程安全。
結(jié)語(yǔ)
從 Node.js 誕生之初,Node.js 就支持通過(guò) C/C++ 代碼來(lái)給 JavaScript 暴露更多的特性接口。隨著時(shí)間積累,我們也認(rèn)識(shí)到實(shí)現(xiàn)、維護(hù)、分發(fā)這些 add-on 一直存在許許多多的難點(diǎn)。而 N-API 就被 add-on 維護(hù)者們認(rèn)為是解決這些難點(diǎn)的一個(gè)非常核心的領(lǐng)域。所以整個(gè)N-API 團(tuán)隊(duì)和社區(qū)都開(kāi)始為 Node.js 核心建立起這樣一套 ABI 穩(wěn)定的 add-on API。
而代表了 N-API 的這些 C API 現(xiàn)在已經(jīng)是每一個(gè) Node.js 發(fā)布版本的一部分,并且我們也有了可以通過(guò) npm 安裝的 node-addon-api 來(lái)提供這些 C API 的 C++ 封裝。N-API 在誕生之初,就是以在不同 Node.js 版本之間,甚至是 Major 版本之間保證 ABI 與 API 兼容性為目標(biāo),而這也已經(jīng)可以證明能夠提供更多額外的好處:
- 我們不再需要在切換 Node.js 大版本之后重新編譯 add-on 模塊;
- 我們可以在除了使用 V8 作為 JavaScript 引擎的 Node.js 之外的運(yùn)行環(huán)境實(shí)現(xiàn) N-API,也意味著這些為 Node.js 開(kāi)發(fā)的 add-on 無(wú)需修改任何代碼即可兼容這些運(yùn)行環(huán)境,比如 Babylon Native,IoT.js 和 Electron。
- N-API 是單純的 C API,這意味著我們可以使用 C/C++ 之外的語(yǔ)言、運(yùn)行時(shí)開(kāi)發(fā) Node.js add-on,比如 Go 或者是 Rust。
N-API 從 Node.js v8.0.0 開(kāi)始以實(shí)驗(yàn)性功能發(fā)布到現(xiàn)在,雖然廣泛應(yīng)用的過(guò)程比較緩慢,但是模塊開(kāi)發(fā)者們也不斷地給我們提交反饋與貢獻(xiàn),這也幫助我們不斷地增加新特性和開(kāi)發(fā)新的工具來(lái)幫助開(kāi)發(fā)者們構(gòu)建一個(gè)更好的 add-on 生態(tài)。
今天,N-API 在 add-on 的開(kāi)發(fā)中使用已經(jīng)非常廣泛。比如一些使用非常多的 add-on 模塊都已經(jīng)遷移至基于 N-API 開(kāi)發(fā):
- sharp (每周 ~900k 下載量)
- bcrypt (每周 ~500k 下載量)
- sqlite3 (每周 ~300k 下載量)
在過(guò)去的幾年中,N-API 獲得了非常多的改進(jìn)。而對(duì)于 add-on 開(kāi)發(fā)者與用戶(hù)來(lái)說(shuō),這也給他們帶來(lái)了接近于原生 JavaScript 模塊的開(kāi)發(fā)、使用體驗(yàn)。
開(kāi)始貢獻(xiàn)
我們?cè)诔掷m(xù)不斷地改進(jìn) N-API 和 Node.js 的 add-on 生態(tài),但是我們也一直非常需要幫助。你可以在以下途徑在多種場(chǎng)景幫助 N-API 做的更好:
- 將你的 add-on 遷移到 N-API;
- 幫助你的應(yīng)用依賴(lài)的 add-on 遷移到 N-API;
- 為 N-API 提出、實(shí)現(xiàn)新的特性;
- 為 node-addon-api 提出、實(shí)現(xiàn)新的基于 N-API 的特性;
- 為 node-addon-api 修復(fù)問(wèn)題、增加測(cè)試用例;
- 為 node-addon-examples 修復(fù)問(wèn)題、增加測(cè)試用例;
作者 | 吳成忠(昭朗)
原文鏈接本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。