初識(shí) NAPI-RS
?????♀? 編者按:NAPI-RS 是一個(gè)使用 Rust 構(gòu)建預(yù)編譯 Node.js 原生擴(kuò)展的框架,本文作者是螞蟻集團(tuán)前端工程師伊北,帶大家初步了解 NAPI-RS,包括如何接入、一些用法、工作機(jī)制以及要注意的點(diǎn)等等,歡迎一起交流~
建議查閱 napi-rs 官網(wǎng) https://napi.rs/
對(duì)于更深入的部分理解,可閱讀?https://juejin.cn/post/7202541740934709303
如何接入
目前提供:
提供自動(dòng)化的多平臺(tái)編譯發(fā)布的解決方案:使用?
@napi-rs/cli
?初始化項(xiàng)目,或者通過(guò)?napi-rs/package-template
?從 github 模板初始化。提供?全平臺(tái)自動(dòng)化跨平臺(tái)編譯?(https://napi.rs/docs/cross-build/summary)的解決方案,示例參考 https://github.com/napi-rs/cross-build,提供基于 GitHub Linux CI 構(gòu)建跨平臺(tái)(橫跨 Windows、macOS、Linux、Android 等不同 os、不同 libc、不同 arch 指令集平臺(tái)),通過(guò) zig + cargo-xwin 實(shí)現(xiàn)。
提供 cli 覆蓋本地開(kāi)發(fā)和?github CI?(可選)全流程。
生成的項(xiàng)目的目錄結(jié)構(gòu)
自動(dòng)按平臺(tái)分發(fā)安裝包
不同于 node-gyp 本地編譯 C++ 包的方式,napi-rs 框架設(shè)計(jì)為
為各個(gè) triples 分配一個(gè) npm 包,并指定 os、cpu、libc 的值,按照對(duì)應(yīng)的 triples 提前編譯好對(duì)應(yīng)的 .node 文件,并作為 main 字段導(dǎo)出。
主包中,將所有平臺(tái)的包作為 optionalDependencies 進(jìn)行聲明。
當(dāng)用戶安裝主包時(shí),會(huì)安裝當(dāng)前機(jī)器對(duì)應(yīng)的 optionalDependencies 下來(lái)。比如在 mac 下安裝?
@node-rs/xxhash
,則會(huì)同時(shí)安裝?@node-rs/xxhash-darwin-arm64
?包下來(lái)。

用戶通過(guò)導(dǎo)入主包的形式進(jìn)行使用,主包的 index.js 中會(huì)判斷好 os、arch、libc 類型,來(lái)決定 require 哪個(gè)平臺(tái)包(或者是本地 .node 文件)
一些用法
自動(dòng)生成 d.ts
napi-rs 提供了額外的類型支持,以便生成跟 ts 對(duì)應(yīng)的上的類型文件。
將對(duì)應(yīng)的 Rust 代碼中的類型生成相應(yīng)的 .d.ts 文件,為 binding 庫(kù)提供類型支持。
命名轉(zhuǎn)換
默認(rèn)將 Rust 風(fēng)格的蛇形轉(zhuǎn)為駝峰風(fēng)格,
hello_world
?->?helloWorld
通過(guò)?
#[napi(js_name = "yourFnName")]
?修改宏的行為來(lái)自定義 js 中變量名
JS 類型映射
bindgen_prelude
JS 的基本類型的映射支持如下,具體可見(jiàn) https://napi.rs/docs/concepts/values
Undefined
Null
Number/BigInt
String
Boolean
Buffer
Object
Array
TypedArray
關(guān)于 JSValue 可見(jiàn) https://napi.rs/docs/compat-mode/concepts/js-values
工作機(jī)制
模塊注冊(cè)
#[napi] 宏自動(dòng)編譯生成對(duì)應(yīng)的模塊導(dǎo)出,相比于 v1,v2 版本通過(guò)宏編譯 + 約定的方式簡(jiǎn)化了寫法。
上述代碼的目的是將?plus_100
導(dǎo)出并暴露給 JS 使用。我們來(lái)看看 napi-rs 的宏是怎么編譯這段代碼的??吹捻樞蚴菑南峦系姆催^(guò)來(lái)。
補(bǔ)充 FFI 和 ABI 的基本概念
FFI(Foreign Function Interface/外部函數(shù)接口)和 ABI(Application Binary Interface/應(yīng)用程序二進(jìn)制接口)是兩個(gè)相關(guān)但不同的概念,它們都涉及到不同語(yǔ)言或系統(tǒng)之間的函數(shù)調(diào)用和數(shù)據(jù)交換。
FFI 是實(shí)現(xiàn)不同語(yǔ)言間交互的接口或者說(shuō)是機(jī)制,允許不同語(yǔ)言編寫的程序互相通信和調(diào)用,且不需要額外的轉(zhuǎn)換或者序列化/反序列化。
ABI 則類似規(guī)范/協(xié)議,定義了不同語(yǔ)言或系統(tǒng)之間的函數(shù)調(diào)用和數(shù)據(jù)交換的細(xì)節(jié),例如函數(shù)參數(shù)和返回值的傳遞方式、寄存器和棧的使用、可執(zhí)行文件的格式、虛擬地址空間布局等等。
FFI 依賴于 ABI 來(lái)實(shí)現(xiàn)跨語(yǔ)言或跨系統(tǒng)的函數(shù)調(diào)用,F(xiàn)FI 通過(guò) ABI 來(lái)確認(rèn)函數(shù)的簽名,包括函數(shù)的名稱、參數(shù)類型、返回類型,以及函數(shù)的地址(即函數(shù)在內(nèi)存中的位置)。
FFI 可以看作是 ABI 的一個(gè)高層抽象,它隱藏了 ABI 的復(fù)雜性,提供了一個(gè)簡(jiǎn)單易用的接口。
而?
register_module_export
?會(huì)在 addon 程序被初次載入時(shí):
將函數(shù)的指針?lè)诺揭粋€(gè) local thread queue 中
在 Node 初始化 addon 后,會(huì)調(diào)用 addon 中?
napi_register_module_v1
?函數(shù),傳入 env 和 exports 對(duì)象NAPI-RS 在?
napi_register_module_v1
?中拿到 env 后遍歷 local thread queque 中存儲(chǔ)的函數(shù)并傳入?env
,進(jìn)而得到?register_js_function
?等 register_xxx 注冊(cè)的值(函數(shù)/常量/Class)等,掛載到 exports 對(duì)象上
這就是初始化的完整過(guò)程。更多可見(jiàn)源碼?napi/src/bindgen_runtime/module_register.rs
?中的實(shí)現(xiàn)。
調(diào)用順序
Node 和 Rust 互相調(diào)用建立在 C ABI 基礎(chǔ)上的 FFI 調(diào)用
Node.js 中調(diào)用 plus100 -> 調(diào)用到 FFI 函數(shù) __napi__plus_100 -> 提取參數(shù)給 Rust fn plus_100
要注意的點(diǎn)
包名修改
強(qiáng)烈建議直接使用?napi rename
?命令執(zhí)行:會(huì)直接更新模板里所有跟 pkg.json#name 和 pkg.json#napi.name 相關(guān)的變量命名。
初始化項(xiàng)目時(shí),后續(xù)若要修改 root 下 package.json 中包名(name),需要將 npm 目錄下所有的平臺(tái)產(chǎn)物包名也要同步修改。
package.json 下的?
napi.name
?修改時(shí),會(huì)影響生成的 .node 產(chǎn)物命名,需要修改掉:index.js 中針對(duì)不同平臺(tái)時(shí) require local 和 require 對(duì)應(yīng) npm 包名的規(guī)則
CI 配置中的 env.app_name,影響到 job 間持久化產(chǎn)物的規(guī)則,類似 https://github.com/vagusX/rs-html-text-content/actions/runs/4459416487/jobs/7831855982
平臺(tái)產(chǎn)物包的 main 字段以及 files 字段
Rust Eum 與 TS Enum 不對(duì)等
js 中沒(méi)有 Enum,而 TS 中 enum 長(zhǎng)這樣。
Rust 的 enum 通過(guò) napi 導(dǎo)出到 js 后,跟 TS 中 enum 的區(qū)別是:缺少 TS 中?reverse mapping 的行為
JS 和 Rust 之間的 Object 轉(zhuǎn)換成本比其他基本類型高
每次調(diào)用?Object.get("key")
?實(shí)際上都會(huì)分派到 Node,包括兩個(gè)步驟:fetch value、將 JS 值轉(zhuǎn)為 Rust 值,調(diào)用?Object.set("key", v)
?也是一樣。同樣的 JS Array 也是一樣。推薦將對(duì)應(yīng)的參數(shù)通過(guò)?struct
定義好,這樣避免直接使用?Object
類型。
TypedArray 可在 Node 和 Rust 間共享數(shù)據(jù)
同理 Buffer 也是 Unit8Array 的子類,具體參見(jiàn) https://napi.rs/docs/concepts/typed-array
高級(jí)用法
Rust 類型中?
u64
,?u128
,?i128
,轉(zhuǎn)為 JS 需要配置 napi6(node 10.7) 開(kāi)啟 BigInt 支持,開(kāi)啟方式:在?Cargo.toml
中開(kāi)啟?features = ["napi6"]
。(默認(rèn)為 napi4)
Types Overwrite:使用字符串作為 napi 宏的參數(shù),達(dá)到覆蓋掉自動(dòng)生成類型的效果。可以覆蓋參數(shù)、返回值、
struct
?中的字段
Async/Await:配合?tokio?(https://tokio.rs/) 一起使用
Rust 傳給 js 一個(gè) Promise 作為結(jié)果:
js 傳給 Rust 一個(gè) Promise ?作為參數(shù):高級(jí)用法,但是可以不用
使用?AsyncTask/Task?(https://napi.rs/docs/concepts/async-task)(前者是后者的 wrapper,讓 js 可以調(diào)用):Task 讓我們具備在 libuv 線程池中異步執(zhí)行任務(wù)(如一些高 cpu 耗時(shí)任務(wù)),而不阻塞 Node.js 的 Event Loop。
External(https://napi.rs/docs/concepts/external):在 JS 中,調(diào)用后產(chǎn)生一個(gè)對(duì)象以表示 Native 值,該對(duì)象上有一些方法可以再跟 Native 交互。
如何調(diào)試
調(diào)試宏
vscode rust-analyzer 自帶


調(diào)試源碼
以下部分展示的是使用同一個(gè) js 入口文件進(jìn)行調(diào)試
調(diào)試 nodejs 部分
使用標(biāo)準(zhǔn)的 vscode js debugger 配置即可,或者創(chuàng)建一個(gè) JavaScript Debug Terminal
調(diào)試 Rust 部分
需要先構(gòu)建出一個(gè)支持調(diào)試的產(chǎn)物,再使用 lldb 進(jìn)行調(diào)試
我將它編寫為兩個(gè)配置文件,分別是 .vscode/tasks.json 和 .vscode/launch.json