訂閱以接收新文章的通知:

讓 Rust Workers 更加可靠:wasm‑bindgen 中的 panic 與 abort 復原

2026-04-22

閱讀時間:7 分鐘
本貼文還提供以下語言版本:English日本語한국어简体中文

Rust Workers 在 Cloudflare Workers 平台上執行時,會將 Rust 編譯為 WebAssembly,但正如我們所發現的,WebAssembly 存在一些棘手的邊緣問題。當發生 panic(恐慌)或非預期的 abort(中止)而導致問題時,執行時期可能會停留在未定義的狀態。對於 Rust Workers 的使用者來說,過去 panic 是致命的——它會損毀整個執行個體,甚至可能導致 Worker 在一段時間內完全無法運作。

雖然我們能夠偵測並減輕這些問題,但仍有微小機率導致 Rust Worker 意外失敗,並連帶使其他請求失敗。Worker 中一個未處理的 Rust abort 可能影響單一請求,進而升級為影響同級請求的更廣泛故障,甚至持續影響新進來的請求。其根本原因在於 wasm-bindgen(這個核心專案負責產生 Rust Workers 所依賴的 Rust 到 JavaScript 繫結)缺乏內建的復原語意。

在本文中,我們將分享最新版的 Rust Workers 如何處理全面的 Wasm 錯誤復原,以解決由中止導致的沙箱毒化問題。這項工作已作為我們去年成立的 wasm-bindgen 組織內部合作的一部分,回饋到 wasm-bindgen 專案中。首先是支援 panic=unwind,確保單一失敗請求永不毒化其他請求;其次是中止復原機制,保證 Wasm 上的 Rust 程式碼在中止後絕不會重新執行。

初步的復原緩解措施

我們最初解決此領域可靠性的嘗試,著重於理解並控制在生產環境中 Rust Workers 因 Rust panic 與 abort 所導致的故障。我們引入了一個自訂的 Rust panic 處理器,用於追蹤 Worker 內的故障狀態,並在處理後續請求前觸發完整的應用程式重新初始化。在 JavaScript 端,這需要透過基於代理的間接層包裹 Rust-JavaScript 呼叫邊界,以確保所有進入點都被一致地封裝。我們還對產生的繫結進行了針對性修改,以便在故障後正確地重新初始化 WebAssembly 模組。

儘管這種方法依賴自訂的 JavaScript 邏輯,但它證明了可靠的復原是可以實現的,並消除了我們在實踐中所見的持續性故障模式。此解決方案自 0.6 版起已預設提供給所有 workers‑rs 使用者,並為後續章節中描述的、更通用且已貢獻給上游的 abort 復原機制奠定了基礎。

使用 WebAssembly 異常處理實現 panic=unwind

前面所述的 abort 復原機制,確保了 Worker 能夠在故障後存活下來,但代價是重新初始化整個應用程式。對於無狀態的請求處理器來說,這沒問題。但對於那些在記憶體中保存有意義狀態的工作負載(例如 Durable Objects),重新初始化就意味著完全丟失該狀態。一個請求中的單一 panic,就可能清除掉其他並行請求正在使用的記憶體狀態。

在大多數原生 Rust 環境中,panic 可以被展開 (unwind),讓解構子 (destructor) 得以執行,程式也能在不遺失狀態的情況下復原。然而在 WebAssembly 中,情況歷來截然不同。透過 wasm32-unknown-unknown 編譯為 Wasm 的 Rust,預設是 panic=abort,因此 Rust Worker 內部的 panic 會突然觸發一個 unreachable 指令,然後拋出 WebAssembly.RuntimeError,從 Wasm 退出並回到 JavaScript。

為了在不丟棄執行個體狀態的情況下從 panic 復原,我們需要在 wasm-bindgen 中為 wasm32-unknown-unknown 提供 panic=unwind 的支援。這透過 WebAssembly 異常處理提案得以實現,該提案在 2023 年獲得了廣泛的引擎支援。

我們從編譯命令 RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std 開始,此命令以支援展開的方式重新建置標準函式庫,並產生帶有正確 panic 展開行為的程式碼。例如:

struct HasDropA;
struct HasDropB;
extern "C" {
    fn imported_func();
}

fn some_func() {
    let a = HasDropA;
    let b = HasDropB;
    imported_func();
}

編譯為 WebAssembly 程式碼後,其結構大致如下:

try
  call <imported_func>
catch_all
  call <drop_b>
  call <drop_a>
  rethrow
end
call <drop_b>
call <drop_a>

這確保了即使 imported_func() 發生 panic,解構子仍會執行。類似地,std::panic::catch_unwind(|| some_func()) 會被編譯成:

try
  call <some_func>
  ;; set result to Ok(return value)
catch
  try
    call <std::panicking::catch_unwind::cleanup>
    ;; set result to Err(panic payload)
  catch_all
    call <core::panicking::cannot_unwind>
    unreachable
  end
end

要使這項功能端到端正常運作,需要對 wasm-bindgen 工具鏈進行多項變更。WebAssembly 剖析器 Walrus 原本不知道如何處理 try/catch 指令,因此我們增加了對它們的支援。描述子解釋器也需要學習如何評估包含異常處理區塊的程式碼。至此,完整的應用程式就可以用 panic=unwind 建構了。

最後一步是修改 wasm-bindgen 產生的匯出,使其在 Rust-JavaScript 邊界捕捉 panic,並將其表示為 JavaScript 的 PanicError 異常。一個細節需要注意:當在透過 extern "C" 函式展開時,Rust 會捕捉外來異常並中止,因此匯出需要標記為 extern "C-unwind" 以明確允許跨邊界展開。對於 future,panic 會拋出 PanicError 並拒絕 JavaScript Promise

Closure(閉包)需要特別關注,以確保展開安全性被正確檢查。我們透過一個新的 MaybeUnwindSafe 特質來實現,該特質僅在以 panic=unwind 建構時才檢查 UnwindSafe。不過,這也迅速暴露了一個問題:許多 closure 會捕獲那些在展開後仍然存在的參照,這使得它們本質上不具備展開安全性。為了避免鼓勵使用者為了滿足編譯器而錯誤地將 closure 包裝在 AssertUnwindSafe 中,我們加入了 Closure::new_aborting 變體。在無法保證展開安全性的情況下,這些變體會在發生 panic 時終止而不是展開。

啟用 panic 展開後:

  • 在匯出的 Rust 函式中發生的 panic 會被 wasm-bindgen 捕捉

  • panic 以 PanicError 異常的形式呈現給 JavaScript

  • 非同步匯出會拋出 PanicError 並拒絕其傳回的 Promise

  • Rust 解構子正確執行

  • WebAssembly 執行個體保持有效且可重複使用

關於此方法的完整詳細資訊,以及如何在 wasm-bindgen 中使用它,請參閱最新的 Wasm Bindgen: Catching Panics 指南頁面。

Abort 復原

即使有了 panic=unwind 的支援,abort 仍然會發生——記憶體不足錯誤就是一個常見的原因。由於 abort 無法展開,根本不可能進行狀態復原,但我們至少可以偵測並從 abort 中復原,以便未來的操作能夠執行,避免因無效狀態導致後續請求失敗。

Panic unwind 的支援為 abort 復原帶來了一個新問題。當我們從 Wasm 收到一個錯誤時,我們無法得知它是來自 extern "C-unwind" 外部函式所造成的錯誤,還是一個真正的 abort。在 WebAssembly 中,abort 可能有多種形式。

我們有兩種技術選項來解決此問題:一是標記所有確定是 abort 的錯誤,二是標記所有確定是 unwind 的錯誤。這兩者都可運作,但我們選擇了後者。由於我們的外部異常處理已直接使用原生 WAT 級別(WebAssembly 文字格式)異常處理指令,因此,我們發現外部異常的標籤實作相對簡便,從而能夠將其與那些導致中止且無法安全展開的異常區分開來。

得益於 WebAssembly 異常處理中的 Exception.Tag 功能,我們能夠清楚區分可復原和不可復原的錯誤,從而整合一個新的中止處理器 (abort handler) 以及中止重入防護 (abort reentrancy guards)。 在初始化時,可以使用新的掛勾 set_on_abort 來附加一個處理器,根據平台嵌入的需求進行相應的復原。

強化 panic 和 abort 處理對於避免無效執行狀態至關重要。WebAssembly 允許深度交織的呼叫堆疊,Wasm 可以呼叫 JavaScript,而 JavaScript 也可以在任意深度重新進入 Wasm,與此同時,多個任務可以在同一個執行個體中運作。過去,在一個任務或巢狀堆疊中發生的 abort,並不保證會透過 JavaScript 使更高層的堆疊失效,這導致了未定義行為。我們需要仔細確保能夠保證執行模型,這方面的工作仍在持續進行。

雖然中止絕非理想情況,且在故障時重新初始化是最壞的狀況,但實作關鍵錯誤復原作為最後一道防線,能確保執行的正確性,並使未來操作能夠成功。無效狀態不會持續存在,確保單一故障不會引發連鎖故障。

擴展:為 wasm-bindgen 函式庫提供 abort 重新初始化

在進行這項工作時,我們意識到這對於被 JavaScript 使用且透過 wasm-bindgen 建置的函式庫來說,是一個常見問題。若能為其附加上中止處理器以執行復原,這些函式庫也將從中受益。

但是,當將 Wasm 建置為 ES 模組並直接匯入時(例如透過 import { func } from 'wasm-dep'),如果在呼叫 func() 時發生 Wasm abort,對於一個已經連結並初始化、正在使用者 JS 應用程式中執行的函式庫來說,目前尚不清楚其具體的復原機制應如何設計。

雖然這嚴格來說不是 Rust Workers 的使用情境,但我們的團隊也支援執行基於 Rust 的 Wasm 函式庫相依項、基於 JavaScript 的 Workers 使用者。如果我們能同時解決這個問題,這將間接惠及 Cloudflare Workers 平台上的 Wasm 使用情境。

為了支援 Wasm 函式庫使用情境的自動 abort 復原,我們在 wasm‑bindgen 中新增了對實驗性重新初始化機制 --reset-state-function 的支援。這提供了一個函式,允許 Rust 應用程式在下次呼叫時,有效地請求將其內部的 Wasm 執行個體重設回初始狀態,而無需所產生之繫結的使用者重新導入或重新建立它們。來自舊執行個體的類別執行個體會因為其控制代碼 (handles) 變成孤立 (orphaned) 而拋出錯誤,但之後可以建構新的類別。使用 Wasm 函式庫的 JS 應用程式會收到錯誤,但不會完全失效。

此功能的完整技術細節,以及如何在 wasm-bindgen 中使用它,請參閱最新的 wasm-bindgen 指南章節:Wasm Bindgen: Handling Aborts

完善 Rust Wasm 異常處理生態系統

這項工作的上游貢獻並未止步於 wasm-bindgen 專案。使用 panic=unwind 建置 Wasm 仍然需要一個實驗性的 nightly Rust 目標,因此我們也一直在努力推動 Rust 對 WebAssembly 異常處理的 Wasm 支援,以協助其進入穩定的 Rust 版本。

在 WebAssembly 異常處理的開發過程中,一個後期的規範變更導致了兩個變體的出現:舊式異常處理和最終的現代異常處理 (exception handling "with exnref")。目前,Rust 的 WebAssembly 目標預設仍產生舊式變體的程式碼。雖然舊式異常處理獲得廣泛支援,但現已遭棄用。

自以下 JS 平台版本發布起,現代 WebAssembly 異常處理功能已正式獲得支援:

執行階段

版本

發布日期

v8

13.8.1

2025 年 4 月 28 日

workerd

v1.20250620.0

2025 年 6 月 19 日

Chrome

138

2025 年 6 月 28 日

Firefox

131

2024 年 10 月 1 日

Safari

18.4

2025 年 3 月 31 日

Node.js

25.0.0

2025 年 10 月 15 日

在我們調查這份支援矩陣時,最令人擔憂的問題最終落在 Node.js 24 LTS 的發布排程上,這可能會使整個生態系統直到 2028 年 4 月都停留在舊式 WebAssembly 異常處理上。

發現這個差異後,我們成功將現代異常處理反向移植 (backport) 到了 Node.js 24 版本中,甚至將必要的修正反向移植到 Node.js 22 版本線,以確保對此目標的支援。這應能使現代異常處理提案在明年成為預設目標。

在未來幾個月,我們將致力於讓轉向穩定的 panic=unwind 和現代異常處理的過程,對終端使用者盡可能無感。

雖然這些對生態系統的長期投入需要時間,但它們有助於為整個 Rust WebAssembly 社群建立更堅實的基礎,我們很高興能夠為這些改進做出貢獻。

在 Rust Workers 中使用 panic unwind

自 Rust Workers 0.8.0 版起,我們新增了一個 --panic-unwind 旗標,可依此處指示將其加入建置命令。

啟用此旗標後,panic 可以被完全復原,且 abort 復原將使用新的 abort 分類與復原鉤子機制。我們強烈建議您升級並嘗試使用,以獲得更穩定的 Rust Workers 體驗,並計劃在後續版本中將 panic=unwind 設為預設。繼續使用 panic=abort 的使用者,仍然可以享有從 0.6.0 版開始提供的自訂復原包裝處理。

致力於 Rust Workers 的穩定性

此項工作是我們持續邁向 Rust Workers 穩定版發布的一部分。透過從根源解決 Wasm 平台基礎的這些棘手問題,並在適當之處回饋生態系統,我們不僅為自身平台,也為整個 Rust、JS 和 Wasm 生態系統建立更強大的基礎。

我們為 Rust Workers 規劃了多項未來改進,並將很快分享關於這些額外工作的更新,包括 wasm-bindgen 泛型與自動化 bindgen。我們團隊的 Guy Bedford 上個月在 Wasm.io 的一場關於 Rust 與 JS 互通性的演講中,已經預告了這些內容。

歡迎在 Cloudflare Discord#rust‑on‑workers 頻道中與我們聯絡。我們也歡迎對 workers-rswasm-bindgenGitHub 專案的回饋與討論,並歡迎所有新的貢獻者加入。

我們保護整個企業網路,協助客戶有效地建置網際網路規模的應用程式,加速任何網站或網際網路應用程式抵禦 DDoS 攻擊,阻止駭客入侵,並且可以協助您實現 Zero Trust

從任何裝置造訪 1.1.1.1,即可開始使用我們的免費應用程式,讓您的網際網路更快速、更安全。

若要進一步瞭解我們協助打造更好的網際網路的使命,請從這裡開始。如果您正在尋找新的職業方向,請查看我們的職缺
Cloudflare WorkersRustRust WorkersWebAssemblyWASM可靠性工程設計開源資源開發人員平台開發人員

在 X 上進行關注

Cloudflare|@cloudflare

相關貼文

2026年4月30日

Agents can now create Cloudflare accounts, buy domains, and deploy

Starting today, agents can now be Cloudflare customers. They can create a Cloudflare account, start a paid subscription, register a domain, and get back an API token to deploy code right away. Humans can be in the loop to grant permission, but there’s no need to go to the dashboard, copy and paste API tokens, or enter credit card details. ...