如何通過 Host Function 擴充套件服務端的 WebAssembly

語言: CN / TW / HK

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 中網頁的 url
  • mem.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、邊緣計算、汽車等領域。