跳至主要內容
蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Kobako:WebAssembly 的記憶體交換

這篇文章是 Kobako 系列的一部分。

前幾篇說明了選擇 MessagePack 的過程,然而使用 WebAssembly 還要能夠互動,跟原生的套件互動方式有一點不太一樣。

理由是我們要打造的是沙盒(Sandbox)來隔離不安全的程式碼,這表示操作基本上是接近單向的,在沙盒中執行的程式,是不能碰到任何宿主(Host)的記憶體,那麼原本預想的方法呼叫,就需要一些特別的方式來處理。

異質環境

會想到使用 RPC(Remote Procedure Call)來設計,很多時候就是因為我們在處理異質的環境,因此我們需要可以在不同環境都可使用的共同語言,以及溝通的方法。

共同語言決定是 MessagePack 後,我們還需要決定的是如何溝通,之前只有初步的了解到 WebAssembly 提供了 Host Function(從裡面呼叫外面)和 Exported Function(從外面呼叫裡面)兩種機制。

也因此,在實際的運作上就會很類似網路傳輸的行為,也跟 RPC 呼叫的方式很像。

  • 發起方把請求編碼成共通的格式
  • 發起方將請求寫入到指定的位置
  • 接收方從指定的位置讀取出來
  • 接收方解碼發起方的請求進行處理

至少想像上是這樣,但現實中 WebAssembly 的運作還是有點不同。

安全限制

WebAssembly 既然可以作為沙盒的選項,這就表示在安全機制上有一定的實現,也就是只有宿主可以存取客體(Guest)的記憶體,而客體則完全無法碰到宿主記憶體,這就造成我們需要想辦法讓互動在客體的記憶體發生。

另一方面,對 WebAssembly 來說他是以接近組合語言的形式表示,因此我們很直覺的 rpc_call(request) 實際上是無法運作的,取而代之的是我們需要自己處理兩個動作。

  • 寫入記憶體,拿到指標位置
  • 進行呼叫時,用指標說明從哪裡開始操作

不過在概念上,跟 RPC 的做法是很接近的,我們會在一個宿主跟客體共用的記憶體區塊工作,進行類似下面的處理

  • 客體想呼叫 Host::Logger.info(...)
  • Host::Logger.info(...) 打包成 MessagePack 格式
  • 寫入到記憶體位置 0x01
  • 呼叫宿主 rpc_call(0x01, size)
  • 宿主處理完請求,把結果打包成 MessagePack 格式
  • 寫入到記憶體位置 0x31
  • 呼叫客體 rpc_reply(0x31, size)

這塊記憶體會是由客體所管理,因此宿主所有的記憶體都跟客體隔離,這也減少洩漏記憶體的問題,基本上可以安全地進行操作。

原本我認為 WebAssembly 的呼叫是可以傳遞物件這種資料的,結果確認後資料只能放在記憶體裡,呼叫能帶的就只有位置跟長度這種數值,而且一次呼叫只能回傳一個值,位置跟長度需要打包在一起,才會有這樣繞一段的處理

回傳結果

因為我想讓 stdoutstderr 留給客體的輸出,因此不能直接透過解析 stdout 或者 stderr 來判斷回傳的數值,需要用其他方式處理。

除此之外,WebAssembly 通常會使用 command 模式來呼叫,有點類似我們使用 echo "HI" 會看到 stdout 顯示在畫面上,並且得到一個 exit code: 0 這樣的回傳,但對於 Kobako 的目標,我想看到一個 Ruby 數值回傳,這樣做就不能滿足條件。

因此還需要轉換為 reactor 模式,跟 command 模式的做法不太一樣,他更接近把編譯出來的檔案當作函式庫(Library)的方式使用,因此在初始化記憶體的處理被呼叫後,就會停下來等待使用者,結束後也不會清理記憶體,完全由使用者決定時機。

這就很適合 Kobako 的情境,原本的 stdoutstderr 就會是單純客體的輸出,不需要為此拿來當作解析輸出的依賴,真正的回傳值放在 WebAssembly 的記憶體中,也就是我們前面用來模擬出 RPC 呼叫的記憶體區域。

當執行結束後,必須要使用一次 Exported Function 從 WebAssembly 拿回來,但這樣就可以在使用過程中更加彈性。

以往使用 WebAssembly 大多是直接編譯後直接呼叫,但這些在概念驗證階段做的實驗,都變成後續 Kobako 開發的基礎。

現在回頭看,RPC 本質上只是一個類比,宿主跟客體其實共用同一條執行緒跟同一塊記憶體,連跨行程的邊界都沒有,所以後來 Kobako 也就沒有用 RPC 來命名。