Rust + Go 雙劍合璧:WebAssembly 領(lǐng)域應(yīng)用
Go 語(yǔ)言是一種易于使用且安全的編程語(yǔ)言,可編譯為高性能的原生應(yīng)用程序。Golang 是編寫軟件基礎(chǔ)設(shè)施和框架的非常流行的選擇。
軟件框架的一個(gè)關(guān)鍵要求是,用戶能夠使用自己的代碼對(duì)其進(jìn)行擴(kuò)展和定制。但是,在 Golang 中,向現(xiàn)有應(yīng)用程序添加用戶定義的函數(shù)或擴(kuò)展并不容易。通常,需要通過(guò)組合框架的源代碼和用戶定義的函數(shù)在源代碼級(jí)別進(jìn)行集成。雖然可以使用 Golang 創(chuàng)建動(dòng)態(tài)共享模塊,但這些廣泛用于邊緣計(jì)算的基于 ARM 的系統(tǒng),缺乏對(duì)共享模塊的支持。此外,源代碼集成和動(dòng)態(tài)模塊都沒(méi)有為用戶定義的函數(shù)提供隔離。擴(kuò)展可能會(huì)干擾框架本身,并且集成多方的用戶定義函數(shù)會(huì)不安全。因此,Golang 作為“云原生”的語(yǔ)言,需要更好的擴(kuò)展機(jī)制。
WebAssembly 提供了一種強(qiáng)大、靈活、安全且簡(jiǎn)單的擴(kuò)展機(jī)制,可以將用戶定義的函數(shù)嵌入到 Golang 應(yīng)用程序中。最初為 Web 瀏覽器而發(fā)明,但越來(lái)越多地用于獨(dú)立和服務(wù)器端應(yīng)用程序,WebAssembly 是其字節(jié)碼應(yīng)用程序的輕量級(jí)軟件容器。WebAssembly 是高性能、可移植的,并支持多種編程語(yǔ)言。
在本教程中,我們將討論如何從 Golang 應(yīng)用程序運(yùn)行 WebAssembly 函數(shù)。WebAssembly 函數(shù)是用 Rust 編寫的。它們與 Golang 主機(jī)應(yīng)用程序有著很好的隔離,同時(shí)函數(shù)之間彼此也進(jìn)行了隔離。
GitHub 代碼:https://github.com/second-state/WasmEdge-go
準(zhǔn)備工作
顯然,我們需要安裝 Golang,這里假設(shè)你已經(jīng)安裝了。
Golang 版本應(yīng)該高于 1.15,我們的示例才能工作。
下一步,請(qǐng)安裝 WasmEdge 共享庫(kù)。WasmEdge 是由 CNCF 托管的領(lǐng)先的 WebAssembly runtime 。我們將使用 WasmEdge 在 Golang 應(yīng)用程序中嵌入和運(yùn)行 WebAssembly 程序。
$ wget https://github.com/second-state/WasmEdge-go/releases/download/v0.8.1/install_wasmedge.sh
$ chmod +x ./install_wasmedge.sh
$ sudo ./install_wasmedge.sh /usr/local
最后,由于我們的 demo WebAssembly 函數(shù)是用 Rust 編寫的,因此您還需要安裝 Rust 編譯器和 rustwasmc 工具鏈。
嵌入一個(gè)函數(shù)
目前,我們需要 Rust 編譯器版本 1.50 或更低版本才能讓 WebAssembly 函數(shù)與 WasmEdge 的 Golang API 一起使用。一旦 interface type 規(guī)范最終確定并得到支持,我們會(huì)趕上最新的Rust 編譯器版本 。
此示例中,我們將演示如何從 Golang 應(yīng)用程序調(diào)用一些簡(jiǎn)單的 WebAssembly 函數(shù)。這些函數(shù)是用 Rust 編寫的,需要復(fù)雜的調(diào)用參數(shù)和返回值。編譯器工具需要 #[wasm_bindgen]宏來(lái)自動(dòng)生成正確的代碼以將調(diào)用參數(shù)從 Golang 傳到 WebAssembly。
WebAssembly 規(guī)范僅支持一些開(kāi)箱即用的簡(jiǎn)單數(shù)據(jù)類型。Wasm 不支持例如字符串和數(shù)組的類型。為了將 Golang 中的豐富類型傳到 WebAssembly,編譯器需要將它們轉(zhuǎn)換為簡(jiǎn)單的整數(shù)。例如,它將字符串轉(zhuǎn)換為整數(shù)內(nèi)存地址和整數(shù)長(zhǎng)度。嵌入在 rustwasmc 中的 wasm_bindgen 工具會(huì)自動(dòng)進(jìn)行這種轉(zhuǎn)換。
use wasm_bindgen::prelude::*;
use num_integer::lcm;
use sha3::{Digest, Sha3_256, Keccak256};
#[wasm_bindgen]
pub fn say(s: &str) -> String {
let r = String::from("hello ");
return r + s;
}
#[wasm_bindgen]
pub fn obfusticate(s: String) -> String {
(&s).chars().map(|c| {
match c {
'A' ..= 'M' | 'a' ..= 'm' => ((c as u8) + 13) as char,
'N' ..= 'Z' | 'n' ..= 'z' => ((c as u8) - 13) as char,
_ => c
}
}).collect()
}
#[wasm_bindgen]
pub fn lowest_common_multiple(a: i32, b: i32) -> i32 {
let r = lcm(a, b);
return r;
}
#[wasm_bindgen]
pub fn sha3_digest(v: Vec<u8>) -> Vec<u8> {
return Sha3_256::digest(&v).as_slice().to_vec();
}
#[wasm_bindgen]
pub fn keccak_digest(s: &[u8]) -> Vec<u8> {
return Keccak256::digest(s).as_slice().to_vec();
}
首先,我們使用 rustwasmc 工具將 Rust 源代碼編譯為 WebAssembly 字節(jié)碼函數(shù)。請(qǐng)使用 Rust 1.50 或者更低版本。
$ rustup default 1.50.0
$ cd rust_bindgen_funcs
$ rustwasmc build
# The output WASM will be pkg/rust_bindgen_funcs_lib_bg.wasm
Golang 源代碼 運(yùn)行在 WasmEdge 中的 WebAssembly 函數(shù)的示例如下。 ExecuteBindgen() 函數(shù)調(diào)用 WebAssembly 函數(shù)并使用 #[wasm_bindgen]傳入?yún)?shù)。
package main
import (
"fmt"
"os"
"github.com/second-state/WasmEdge-go/wasmedge"
)
func main() {
/// Expected Args[0]: program name (./bindgen_funcs)
/// Expected Args[1]: wasm or wasm-so file (rust_bindgen_funcs_lib_bg.wasm))
wasmedge.SetLogErrorLevel()
var conf = wasmedge.NewConfigure(wasmedge.WASI)
var vm = wasmedge.NewVMWithConfig(conf)
var wasi = vm.GetImportObject(wasmedge.WASI)
wasi.InitWasi(
os.Args[1:], /// The args
os.Environ(), /// The envs
[]string{".:."}, /// The mapping directories
[]string{}, /// The preopens will be empty
)
/// Instantiate wasm
vm.LoadWasmFile(os.Args[1])
vm.Validate()
vm.Instantiate()
/// Run bindgen functions
var res interface{}
var err error
res, err = vm.ExecuteBindgen("say", wasmedge.Bindgen_return_array, []byte("bindgen funcs test"))
if err == nil {
fmt.Println("Run bindgen -- say:", string(res.([]byte)))
}
res, err = vm.ExecuteBindgen("obfusticate", wasmedge.Bindgen_return_array, []byte("A quick brown fox jumps over the lazy dog"))
if err == nil {
fmt.Println("Run bindgen -- obfusticate:", string(res.([]byte)))
}
res, err = vm.ExecuteBindgen("lowest_common_multiple", wasmedge.Bindgen_return_i32, int32(123), int32(2))
if err == nil {
fmt.Println("Run bindgen -- lowest_common_multiple:", res.(int32))
}
res, err = vm.ExecuteBindgen("sha3_digest", wasmedge.Bindgen_return_array, []byte("This is an important message"))
if err == nil {
fmt.Println("Run bindgen -- sha3_digest:", res.([]byte))
}
res, err = vm.ExecuteBindgen("keccak_digest", wasmedge.Bindgen_return_array, []byte("This is an important message"))
if err == nil {
fmt.Println("Run bindgen -- keccak_digest:", res.([]byte))
}
vm.Delete()
conf.Delete()
}
接下來(lái),讓我們使用 WasmEdge Golang SDK 構(gòu)建 Golang 應(yīng)用程序。
$ go get -u github.com/second-state/WasmEdge-go/wasmedge
$ go build
運(yùn)行 Golang 應(yīng)用程序,它將運(yùn)行嵌入在 WasmEdge Runtime 中的 WebAssembly 函數(shù)。
$ ./bindgen_funcs rust_bindgen_funcs/pkg/rust_bindgen_funcs_lib_bg.wasm
Run bindgen -- say: hello bindgen funcs test
Run bindgen -- obfusticate: N dhvpx oebja sbk whzcf bire gur ynml qbt
Run bindgen -- lowest_common_multiple: 246
Run bindgen -- sha3_digest: [87 27 231 209 189 105 251 49 159 10 211 250 15 159 154 181 43 218 26 141 56 199 25 45 60 10 20 163 54 211 195 203]
Run bindgen -- keccak_digest: [126 194 241 200 151 116 227 33 216 99 159 22 107 3 177 169 216 191 114 156 174 193 32 159 246 228 245 133 52 75 55 27]
嵌入一整個(gè)程序
你可以使用最新的 Rust 編譯器和
main.rs創(chuàng)建一個(gè)單獨(dú)的 WasmEdge 應(yīng)用,然后將其嵌入一個(gè) Golang 應(yīng)用中。
除了函數(shù), WasmEdge Golang SDK 也可以嵌入獨(dú)立的 WebAssembly 應(yīng)用程序,即將一個(gè)帶有 main() 函數(shù)的 Rust 應(yīng)用編譯為 WebAssembly。
我們的demo Rust 應(yīng)用程序 從一個(gè)文件中讀取。注意這里不需要 #{wasm_bindgen] ,因?yàn)?WebAssembly 程序的輸入和輸出數(shù)據(jù)現(xiàn)在由 STDIN 和 STDOUT 傳遞。
use std::env;
use std::fs::File;
use std::io::{self, BufRead};
fn main() {
// Get the argv.
let args: Vec<String> = env::args().collect();
if args.len() <= 1 {
println!("Rust: ERROR - No input file name.");
return;
}
// Open the file.
println!("Rust: Opening input file \"{}\"...", args[1]);
let file = match File::open(&args[1]) {
Err(why) => {
println!("Rust: ERROR - Open file \"{}\" failed: {}", args[1], why);
return;
},
Ok(file) => file,
};
// Read lines.
let reader = io::BufReader::new(file);
let mut texts:Vec<String> = Vec::new();
for line in reader.lines() {
if let Ok(text) = line {
texts.push(text);
}
}
println!("Rust: Read input file \"{}\" succeeded.", args[1]);
// Get stdin to print lines.
println!("Rust: Please input the line number to print the line of file.");
let stdin = io::stdin();
for line in stdin.lock().lines() {
let input = line.unwrap();
match input.parse::<usize>() {
Ok(n) => if n > 0 && n <= texts.len() {
println!("{}", texts[n - 1]);
} else {
println!("Rust: ERROR - Line \"{}\" is out of range.", n);
},
Err(e) => println!("Rust: ERROR - Input \"{}\" is not an integer: {}", input, e),
}
}
println!("Rust: Process end.");
}
使用 rustwasmc 工具將應(yīng)用程序編譯為 WebAssembly。
$ cd rust_readfile
$ rustwasmc build
# The output file will be pkg/rust_readfile.wasm
Golang 源代碼運(yùn)行在 WasmEdge 中 WebAssembly 函數(shù),如下:
package main
import (
"os"
"github.com/second-state/WasmEdge-go/wasmedge"
)
func main() {
wasmedge.SetLogErrorLevel()
var conf = wasmedge.NewConfigure(wasmedge.REFERENCE_TYPES)
conf.AddConfig(wasmedge.WASI)
var vm = wasmedge.NewVMWithConfig(conf)
var wasi = vm.GetImportObject(wasmedge.WASI)
wasi.InitWasi(
os.Args[1:], /// The args
os.Environ(), /// The envs
[]string{".:."}, /// The mapping directories
[]string{}, /// The preopens will be empty
)
/// Instantiate wasm. _start refers to the main() function
vm.RunWasmFile(os.Args[1], "_start")
vm.Delete()
conf.Delete()
}
接下來(lái),讓我們使用 WasmEdge Golang SDK 構(gòu)建 Golang 應(yīng)用程序。
$ go get -u github.com/second-state/WasmEdge-go
$ go build
運(yùn)行 Golang 應(yīng)用。
$ ./read_file rust_readfile/pkg/rust_readfile.wasm file.txt
Rust: Opening input file "file.txt"...
Rust: Read input file "file.txt" succeeded.
Rust: Please input the line number to print the line of file.
# Input "5" and press Enter.
5
# The output will be the 5th line of `file.txt`:
abcDEF___!@#$%^
# To terminate the program, send the EOF (Ctrl + D).
^D
# The output will print the terminate message:
Rust: Process end.
接下來(lái)
本文中,我們展示了在 Golang 應(yīng)用程序中嵌入 WebAssembly 函數(shù)的兩種方法:嵌入一個(gè) WebAssembly 函數(shù)以及嵌入一個(gè)完整的程序。更多示例可以參考 WasmEdge-go-examples GitHub repo。
GitHub 鏈接:https://github.com/second-state/WasmEdge-go-examples
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開(kāi)源圖書《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio
