阅读 759

通过 WasmEdge 嵌入WebAssembly 函数扩展 Golang 应用

GO 编程语言(Golang)是一种易于使用且安全的编程语言,可编译为高性能的原生应用程序。Golang 是编写软件基础设施和框架的非常流行的选择。

软件框架的一个关键要求是,用户能够使用自己的代码对其进行扩展和定制。但是,在 Golang 中,向现有应用程序添加用户定义的函数或扩展并不容易。通常,需要通过组合框架的源代码和用户定义的函数在源代码级别进行集成。虽然可以使用 Golang 创建动态共享模块,但这些广泛用于边缘计算的基于 ARM 的系统,缺乏对共享模块的支持。此外,源代码集成和动态模块都没有为用户定义的函数提供隔离。扩展可能会干扰框架本身,并且集成多方的用户定义函数会不安全。因此,Golang 作为“云原生”的语言,需要更好的扩展机制。

WebAssembly 提供了一种强大、灵活、安全且简单的扩展机制,可以将用户定义的函数嵌入到 Golang 应用程序中。最初为 Web 浏览器而发明,但越来越多地用于独立和服务器端应用程序,WebAssembly 是其字节码应用程序的轻量级软件容器。WebAssembly 是高性能、可移植的,并支持多种编程语言。

在本教程中,我们将讨论如何从 Golang 应用程序运行 WebAssembly 函数。 WebAssembly 函数是用 Rust 编写的。它们与 Golang 主机应用程序有着很好的隔离,同时函数之间彼此也进行了隔离。

wasm-go-rust-wasmedge.jpg

准备工作

显然,我们需要安装 Golang,这里假设你已经安装了。

Golang 版本应该高于 1.15,我们的示例才能工作。

下一步,请安装 WasmEdge 共享库。 WasmEdge 是一个由 CNCF 托管的领先的 WebAssembly runtime 。我们将使用 WasmEdge 在 Golang 应用程序中嵌入和运行 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 函数是用 Rust 编写的,因此您还需要安装 Rust 编译器和 rustwasmc 工具链

嵌入一个函数

目前,我们需要 Rust 编译器版本 1.50 或更低版本才能让 WebAssembly 函数与 WasmEdge 的 Golang API 一起使用。一旦 interface type 规范最终确定并得到支持,我们会赶上最新的Rust 编译器版本

示例中,我们将演示如何从 Golang 应用程序调用一些简单的 WebAssembly 函数。这些函数是用 Rust 编写的,需要复杂的调用参数和返回值。编译器工具需要 #[wasm_bindgen] 宏来自动生成正确的代码以将调用参数从 Golang 传到 WebAssembly。

WebAssembly 规范仅支持一些开箱即用的简单数据类型。 Wasm 不支持例如字符串和数组的类型。 为了将 Golang 中的丰富类型传到 WebAssembly,编译器需要将它们转换为简单的整数。 例如,它将字符串转换为整数内存地址和整数长度。 嵌入在 rustwasmc 中的 wasm_bindgen 工具会自动进行这种转换。

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 字节码函数。请使用 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 源代码 运行在 WasmEdge 中的 WebAssembly 函数的示例如下。 ExecuteBindgen() 函数调用 WebAssembly 函数并使用 #[wasm_bindgen] 传入参数。

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()
}
复制代码

接下来,让我们使用 WasmEdge Golang SDK 构建 Golang 应用程序。

$ go get -u github.com/second-state/WasmEdge-go/wasmedge
$ go build
复制代码

运行 Golang 应用程序,它将运行嵌入在 WasmEdge Runtime 中的 WebAssembly 函数。

$ ./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]
复制代码

嵌入一整个程序

你可以使用最新的 Rust 编译器和 main.rs 创建一个单独的 WasmEdge 应用,然后将其嵌入一个 Golang 应用中。

除了函数, WasmEdge Golang SDK 也可以嵌入独立的 WebAssembly 应用程序,即将一个带有 main() 函数的 Rust 应用编译为 WebAssembly。

我们的demo Rust 应用程序 从一个文件中读取。注意这里不需要 #{wasm_bindgen] ,因为 WebAssembly 程序的输入和输出数据现在由 STDINSTDOUT 传递。

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 工具将应用程序编译为 WebAssembly。

$ cd rust_readfile
$ rustwasmc build
# The output file will be pkg/rust_readfile.wasm
复制代码

Golang 源代码运行在 WasmEdge 中 WebAssembly 函数,如下:

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()
}
复制代码

接下来,让我们使用 WasmEdge Golang SDK 构建 Golang 应用程序。

$ go get -u github.com/second-state/WasmEdge-go
$ go build
复制代码

运行 Golang 应用。

$ ./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.
复制代码

接下来

本文中,我们展示了在 Golang 应用程序中嵌入 WebAssembly 函数的两种方法:嵌入一个 WebAssembly 函数以及嵌入一个完整的程序。 更多示例可以参考 WasmEdge-go-examples GitHub repo

下一篇文章,我们将研究将 AI 推理(图像识别)函数嵌入到基于 Golang 的实时流数据处理框架的完整示例。这在智能工厂和汽车中有实际应用。

文章分类
后端