如何通過 Host Function 擴充套件服務端的 WebAssembly
Host Function 是當下擴充套件 WebAssembly 的主要方法。 本文將通過兩個 Host Function 的例子,帶你開啟 WebAssembly 新世界!
作者:DarumaDocker,主要負責 WasmEdge-bindgen 的開發工作。
WebAssembly 最初是從瀏覽器發展出來的,當 Wasm 慢慢從瀏覽器遷移到服務端的時候,面臨的一大問題就是功能不完備、能力有限。WASI 的提出有望解決這些問題,但標準的制定與實施通常都是緩慢的。
如果你著急使用一個功能該怎麼辦呢?答案是使用 Host Function 來定製你的 WebAssembly Runtime。
什麼是 Host Function
顧名思義, Host Function 就是定義在 Host 程式中的函式. 對於 Wasm 來說, Host Function 可以做為匯入段 import
被註冊到一個模組 module
中, 之後便可以在 Wasm 執行時被呼叫.
Wasm 目前的能力有限,但那些 Wasm 本身做不了的事情, 都可以依靠 Host Function 來解決, 這極大地擴充套件了 Wasm 的能力範圍.
WasmEdge 在標準之外做的擴充套件基本都是依賴 Host Function 做的的,比如,WasmEdge 提供的 Tensorflow API, 是使用 Host Function 實現的,也因此實現了以原生速度執行 AI 推理的目標。
Networking socket 也是使用 host function 實現的,因此我們可以在 WasmEdge 執行非同步 HTTP 客戶端和伺服器,彌補了 WebAssembly 在網路上的不足。
再比如 Fastly 使用 Host Function 為 Wasm 增加了 Http Request 和 Key-value store 等介面, 進而增添了擴充套件功能。
如何編寫簡單的 Host Function
讓我們從一個最簡單的例子入手, 來看看如何在一個 Go 程式裡編寫 Host function。
先來編寫一個簡單的 rust 程式。國際慣例,Cargo.toml
不能少。
Cargo.toml
[package]
name = "rust_host_func"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
再來看看 Rust 程式碼是什麼樣的。
lib.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
#[no_mangle]
pub unsafe extern fn run() -> i32 {
add(1, 2)
}
上述程式中的 add
函式被宣告在 extern "C"
中, 這就是一個 Host Function。我們使用如下命令將這段 Rust 程式編譯為 wasm:
cargo build --target wasm32-wasi --release
然後我們使用 wasm2wat
來檢視 wasm 檔案的匯入段:
wasm2wat target/wasm32-wasi/release/rust_host_func.wasm | grep import
輸出如下:
(import "env" "add" (func $add (type 0)))
可以看到 add
函式被放到了預設名稱為 env
的模組的匯入段中.
接下來我們來看如何使用 WasmEdge-go SDK 來執行這段 wasm 程式.
hostfunc.go
package main
import (
"fmt"
"os"
"github.com/second-state/WasmEdge-go/wasmedge"
)
func add(_ interface{}, _ *wasmedge.Memory, params []interface{}) ([]interface{}, wasmedge.Result) {
// 將從 wasm 傳過來的兩個引數做加法運算
return []interface{}{params[0].(int32) + params[1].(int32)}, wasmedge.Result_Success
}
func main() {
vm := wasmedge.NewVM()
// 使用預設名稱 env 構建匯入段物件
obj := wasmedge.NewImportObject("env")
// 構建 Host Function 的引數和返回值型別
funcAddType := wasmedge.NewFunctionType(
[]wasmedge.ValType{
wasmedge.ValType_I32,
wasmedge.ValType_I32,
},
[]wasmedge.ValType{
wasmedge.ValType_I32,
})
hostAdd := wasmedge.NewFunction(funcAddType, add, nil, 0)
// 將 Host Function 加入到匯入段物件中
// 注意第一個引數 `add` 是 rust 中定義的外部函式的名稱
obj.AddFunction("add", hostAdd)
// 註冊匯入段物件
vm.RegisterImport(obj)
// 載入, 驗證並例項化 wasm 程式
vm.LoadWasmFile(os.Args[1])
vm.Validate()
vm.Instantiate()
// 執行 wasm 匯出的函式並取得返回值
r, _ := vm.Execute("run")
fmt.Printf("%d", r[0].(int32))
obj.Release()
vm.Release()
}
編譯並執行:
go build
./hostfunc rust_host_func.wasm
程式輸出 3
。
這樣我們就完成了一個最簡單的在 Host 中定義 Function, 並在 wasm 中呼叫的例子。
下面讓我們嘗試用 Host Function 做一些更有趣的事情.
傳遞複雜型別
受 Wasm 裡資料型別的制約, Host Function 只能傳遞如 int32 等少數幾種基本型別的資料, 這就會大大限制 Host Function 的應用範圍. 那有沒有什麼辦法能讓我們傳遞如 string 等複雜資料型別的資料呢?答案是當然可以, 下面我們就通過一個例子看看是如何做到的。
在這個例子中, 我們要統計 http://www.google.com
的網頁原始碼中 google
出現的次數。 例子的原始碼在這裡.
還是先上 Rust 程式碼。Cargo.toml
是必不可少的,只是我在這裡省略了。
lib.rs
extern "C" {
fn fetch(url_pointer: *const u8, url_length: i32) -> i32;
fn write_mem(pointer: *const u8);
}
#[no_mangle]
pub unsafe extern fn run() -> i32 {
let url = "http://www.google.com";
let pointer = url.as_bytes().as_ptr();
// call host function to fetch the source code, return the result length
let res_len = fetch(pointer, url.len() as i32) as usize;
// malloc memory
let mut buffer = Vec::with_capacity(res_len);
let pointer = buffer.as_mut_ptr();
// call host function to write source code to the memory
write_mem(pointer);
// find occurrences from source code
buffer.set_len(res_len);
let str = std::str::from_utf8(&buffer).unwrap();
str.matches("google").count() as i32
}
在這段程式碼中, 引入了兩個 Host Function:
fetch
用於傳送 http 請求以獲取網頁原始碼write_mem
用於把網頁原始碼寫到 wasm 的記憶體
你可能已經看出來了, 要在 Host Function 裡傳遞 string, 實際是通過傳遞這段 string 所在記憶體指標和長度來實現的. fetch
接收兩個引數, 他們就分別是字串 http://www.google.com
的指標和位元組長度.
fetch
在獲取到原始碼後, 將原始碼的位元組長度做為返回值返回。Rust 在分配了此長度的記憶體後, 將記憶體指標傳遞給 write_mem
, host 將原始碼寫入到這段記憶體, 進而達到了返回 string 的目的.
編譯的過程同上不再贅述, 接下來展示如何使用 WasmEdge-go SDK 來執行這段 Wasm 程式。
hostfun.go
package main
import (
"fmt"
"io"
"os"
"net/http"
"github.com/second-state/WasmEdge-go/wasmedge"
)
type host struct {
fetchResult []byte
}
// do the http fetch
func fetch(url string) []byte {
resp, err := http.Get(string(url))
if err != nil {
return nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return body
}
// Host function for fetching
func (h *host) fetch(_ interface{}, mem *wasmedge.Memory, params []interface{}) ([]interface{}, wasmedge.Result) {
// get url from memory
pointer := params[0].(int32)
size := params[1].(int32)
data, _ := mem.GetData(uint(pointer), uint(size))
url := make([]byte, size)
copy(url, data)
respBody := fetch(string(url))
if respBody == nil {
return nil, wasmedge.Result_Fail
}
// store the source code
h.fetchResult = respBody
return []interface{}{len(respBody)}, wasmedge.Result_Success
}
// Host function for writting memory
func (h *host) writeMem(_ interface{}, mem *wasmedge.Memory, params []interface{}) ([]interface{}, wasmedge.Result) {
// write source code to memory
pointer := params[0].(int32)
mem.SetData(h.fetchResult, uint(pointer), uint(len(h.fetchResult)))
return nil, wasmedge.Result_Success
}
func main() {
conf := wasmedge.NewConfigure(wasmedge.WASI)
vm := wasmedge.NewVMWithConfig(conf)
obj := wasmedge.NewImportObject("env")
h := host{}
// Add host functions into the import object
funcFetchType := wasmedge.NewFunctionType(
[]wasmedge.ValType{
wasmedge.ValType_I32,
wasmedge.ValType_I32,
},
[]wasmedge.ValType{
wasmedge.ValType_I32,
})
hostFetch := wasmedge.NewFunction(funcFetchType, h.fetch, nil, 0)
obj.AddFunction("fetch", hostFetch)
funcWriteType := wasmedge.NewFunctionType(
[]wasmedge.ValType{
wasmedge.ValType_I32,
},
[]wasmedge.ValType{})
hostWrite := wasmedge.NewFunction(funcWriteType, h.writeMem, nil, 0)
obj.AddFunction("write_mem", hostWrite)
vm.RegisterImport(obj)
vm.LoadWasmFile(os.Args[1])
vm.Validate()
vm.Instantiate()
r, _ := vm.Execute("run")
fmt.Printf("There are %d 'google' in source code of google.com\n", r[0])
obj.Release()
vm.Release()
conf.Release()
}
有了對 Rust 程式碼的理解, 這段 go 程式碼其實就很容易理解了。 比較關鍵的就是對 Wasm 記憶體的存取:
mem.GetData(uint(pointer), uint(size))
取得 Wasm 中網頁的 urlmem.SetData(h.fetchResult, uint(pointer), uint(len(h.fetchResult)))
將網頁原始碼寫入 wasm 記憶體
這個例子的編譯執行步驟和前一個例子一模一樣, 最後執行的結果是:
There are 79 'google' in source code of google.com
結語
通過以上兩個例子的拋磚引玉, 相信你已經對 Host Function 有了一個初步印象。 雖然因為 Wasm 的諸多限制, 在開發體驗上還不太理想, 但隨著我們對工具及庫的不斷完善, 將會為 Wasm 的應用場景帶來無盡可能。
歡迎持續關注 WasmEdge 專案,如果你覺得 WasmEdge 不錯,也歡迎 star 一下,謝謝。
關於 WasmEdge
WasmEdge 是輕量級、安全、高效能、實時的軟體容器與執行環境。目前是 CNCF 沙箱專案。WasmEdge 被應用在 SaaS、雲原生,service mesh、邊緣計算、汽車等領域。
- GSoC 2023 報名開啟:和 WasmEdge 一起構建雲端計算的未來吧
- LFX Mentorship 2023年第一期實習開啟:構建雲端計算的未來基石
- WebAssembly 和 Sockets: WasmEdge 上的 PHP 開發伺服器
- 使用 Docker WasmEdge 執行 WordPress | WebAssembly:無需容器的 Docker (下)
- WebAssembly:無需容器的 Docker (上)
- Shifu WasmEdge:物聯網資料輕鬆“瘦身”
- 從 WebAssembly 角度改進 WASI-NN | WASI-NN 系列文章2
- 構建基於 WasmEdge 與 WASI-NN 介面的 OpenVINO 的道路分割推理任務 | WasmEdge 0.10.1 系列解讀文章
- WasmEdge Rust SDK 釋出新版本
- WebAssembly 2022 現狀調查
- WasmEdge 0.10.0 釋出!全新的外掛擴充套件機制、LLVM 14、Socket API 增強
- WasmEdge 邀請你參加 Open Source Summit NA | 活動預告
- Rust 開發者看過來!CNCF LFX Mentorship 遠端帶薪實習機會來啦
- LFX Mentorship:從對密碼學一無所知到在 WasmEdge 中實現 wasi-crypto 提案
- LFX Mentorship:從對密碼學一無所知到在 WasmEdge 中實現 wasi-crypto 提案
- GSoC 2022 即將開始!快來申請 WasmEdge 的開源任務吧
- GSoC 2022 即將開始!快來申請 WasmEdge 的開源任務吧
- 如何用 Wasm 為資料庫增加 UDF 功能
- 她說 | women in open source
- 如何通過 Host Function 擴充套件服務端的 WebAssembly