Extend golang application through wasmedge embedded webassembly function

Time:2021-9-20

Go programming language (golang) is an easy-to-use and secure programming language that can be compiled into high-performance native applications. Golang is a very popular choice for writing software infrastructure and frameworks.

A key requirement of software framework is that users can extend and customize it with their own code. However, in golang, it is not easy to add user-defined functions or extensions to existing applications. In general, you need to integrate at the source level through the source code of the composite framework and user-defined functions. Although golang can be used to create dynamic shared modules, these arm based systems widely used in edge computing lack support for shared modules. In addition, neither source code integration nor dynamic modules provide isolation for user-defined functions. Extensions can interfere with the framework itself, and integrating multi-party user-defined functions can be unsafe. Therefore, as a “cloud native” language, golang needs a better extension mechanism.

Web assembly provides a powerful, flexible, secure and simple extension mechanism to embed user-defined functions into golang applications. Originally invented for web browsers, but increasingly used for stand-alone and server-side applications, web assembly is a lightweight software container for its bytecode applications. Web assembly is high-performance, portable, and supports multiple programming languages.

In this tutorial, we will discuss how to run the webassembly function from a golang application. The webassembly function is written in rust. They are well isolated from golang host applications, and functions are isolated from each other.

preparation

Obviously, we need to install golang. It is assumed that you have already installed it.

The version of golang should be higher than 1.15 for our example to work.

Next, install wasmedge shared library. Wasmedge is a leading web assembly runtime hosted by CNCF. We will use wasmedge to embed and run the web assembly program in the golang application.

$ 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

Finally, since our demo webassembly function is written in rust, you also need to install the rust compiler and the rustwasmc toolchain.

Embed a function

At present, we need rust compiler version 1.50 or lower to use the webassembly function with wasmedge’s golang API. Once the interface type specification is finalized and supported, we will catch up with the latest rust compiler version.

In this example, we will demonstrate how to call some simple web assembly functions from a golang application. These functions are written in rust and require complex call parameters and return values. Compiler tools need#[wasm_bindgen] Macro to automatically generate the correct code to pass the call parameters from golang to webassembly.

The web assembly specification only supports some simple data types out of the box. Wasm does not support types such as strings and arrays. In order to pass the rich types in golang to the web assembly, the compiler needs to convert them to simple integers. For example, it converts a string to an integer memory address and an integer length. Wasm embedded in rustwasmc_ The binden tool automatically performs this conversion.

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();
}

First, we use the rustwasmc tool to compile the rust source code into a web assembly bytecode function. Please use rust 1.50 or lower.

$ rustup default 1.50.0
$ cd rust_bindgen_funcs
$ rustwasmc build
# The output WASM will be pkg/rust_bindgen_funcs_lib_bg.wasm

An example of the web assembly function that golang source code runs in wasmedge is as follows.ExecuteBindgen()Function calls the webassembly function and uses the#[wasm_bindgen] Pass in parameters.

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()
}

Next, let’s build a golang application using the wasmedge golang SDK.

$ go get -u github.com/second-state/WasmEdge-go/wasmedge
$ go build

Run the golang application, which will run the webassembly function embedded in the wasmedge runtime.

$ ./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]

Embed a whole program

You can use the latest rust compiler andmain.rsCreate a separate wasmedge application and embed it in a golang application.

In addition to functions, the wasmedge golang SDK can also embed independent webassembly applications, that is, amain()The rust application of the function is compiled into a webassembly.

Our demo rust application reads from a file. Note that there is no need here#{wasm_bindgen], because the input and output data of the web assembly program is nowSTDINandSTDOUTPass.

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.");
}

Compile the application into a web assembly using the rustwasmc tool.

$ cd rust_readfile
$ rustwasmc build
# The output file will be pkg/rust_readfile.wasm

The golang source code runs the webassembly function in wasmedge, as follows:

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()
}

Next, let’s build a golang application using the wasmedge golang SDK.

$ go get -u github.com/second-state/WasmEdge-go
$ go build

Run the golang application.

$ ./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`:
[email protected]#$%^
# To terminate the program, send the EOF (Ctrl + D).
^D
# The output will print the terminate message:
Rust: Process end.

next

In this article, we show two ways to embed webassembly functions in golang applications: embedding a webassembly function and embedding a complete program. For more examples, refer to wasmedge go examples GitHub repo.

In the next article, we will study a complete example of embedding AI reasoning (image recognition) functions into a real-time stream data processing framework based on golang. This has practical applications in intelligent factories and automobiles.