---
title: "Kobako：WebAssembly 的記憶體交換"
date: 2026-08-19T00:00:00+08:00
publishDate: 2026-08-19T00:00:00+08:00
lastmod: 2026-08-18T20:41:48+08:00
tags: ["LLM","AI","經驗","Ruby","WebAssembly","Gem","mruby"]
series: "kobako"
toc: true
permalink: "https://blog.aotoki.me/posts/2026/08/19/kobako-webassembly-memory-exchange/"
language: "zh-tw"
---


前幾篇說明了[選擇 MessagePack 的過程](https://blog.aotoki.me/posts/2026/08/05/kobako-from-ruby-to-mruby/)，然而使用 WebAssembly 還要能夠互動，跟原生的套件互動方式有一點不太一樣。

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

<!--more-->

## 異質環境{#heterogeneous}

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

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

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

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

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

## 安全限制{#security-limitation}

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 的呼叫是可以傳遞物件這種資料的，結果確認後資料只能放在記憶體裡，呼叫能帶的就只有位置跟長度這種數值，而且一次呼叫只能回傳一個值，位置跟長度需要打包在一起，才會有這樣繞一段的處理

## 回傳結果{#return-value}

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

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

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

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

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

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

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