Meu nome é Elton Minetto

Executando código WebAssembly em Go

Este é o segundo post de uma série que estou fazendo sobre WebAssembly e Go. No primeiro post vimos como executar código Go em um navegador web. Neste vamos importar uma função WebAssembly e executá-la em uma aplicação Go.

Provavelmente este vai ser o texto mais curto da série, pois o processo é realmente bem simples :)

O primeiro passo foi criar alguma função em WebAssembly e neste caso aproveitei para testar algo em Rust, uma linguagem que tenho planos de aprender em 2024. Para isso segui o passo a passo que encontrei no site Wasm By Example. Ao final dos passos você vai ter um arquivo .wasm para importar no seu projeto Go, no meu caso o nome do arquivo ficou poc_wasm_in_go_bg.wasm pois ele é gerado de acordo com o nome do diretório do projeto.

O próximo passo é criarmos um projeto Go e usar algum runtime para executar nosso arquivo wasm. Para isso escolhi o wasmer-go. O que eu fiz basicamente foi:

mkdir go-project
cd go-project
go mod init github.com/eminetto/go-project
go get github.com/wasmerio/wasmer-go/wasmer
go mod tidy

E criei um arquivo main.go com o conteúdo:

package main

import (
	"fmt"
	"os"

	wasmer "github.com/wasmerio/wasmer-go/wasmer"
)

func main() {
	wasmBytes, _ := os.ReadFile("path_to_file/poc_wasm_in_go_bg.wasm")

	engine := wasmer.NewEngine()
	store := wasmer.NewStore(engine)

	// Compiles the module
	module, _ := wasmer.NewModule(store, wasmBytes)

	// Instantiates the module
	importObject := wasmer.NewImportObject()
	instance, _ := wasmer.NewInstance(module, importObject)

	// Gets the `sum` exported function from the WebAssembly instance.
	add, _ := instance.Exports.GetFunction("add")

	// Calls that exported function with Go standard values. The WebAssembly
	// types are inferred and values are casted automatically.
	result, _ := add(5, 37)

	fmt.Println(result) // 42!
}

Agora é só executar o código:

❯ go run main.go
42

Simples assim :) Temos um código escrito em Rust, compilado para WebAssembly sendo executado como se fosse uma função nativa em Go.

E a performance?

Para responder essa pergunta comecei fazendo uma refatoração no main.go:

package main

import (
	"fmt"
	"os"

	wasmer "github.com/wasmerio/wasmer-go/wasmer"
)

func main() {
	add, err := loadWasmFunc("path_to_file/poc_wasm_in_go_bg.wasm")
	if err != nil {
		panic(err)
	}
	wasmAdd(add, 50, 31)
}

func loadWasmFunc(fileName string) (wasmer.NativeFunction, error) {
	wasmBytes, err := os.ReadFile(fileName)
	if err != nil {
		return nil, err
	}

	engine := wasmer.NewEngine()
	store := wasmer.NewStore(engine)

	// Compiles the module
	module, err := wasmer.NewModule(store, wasmBytes)
	if err != nil {
		return nil, err
	}

	// Instantiates the module
	importObject := wasmer.NewImportObject()
	instance, err := wasmer.NewInstance(module, importObject)
	if err != nil {
		return nil, err
	}

	// Gets the `sum` exported function from the WebAssembly instance.
	add, _ := instance.Exports.GetFunction("add")
	return add, nil
}

func wasmAdd(add wasmer.NativeFunction, a, b int) {
	result, _ := add(a, b)
	fmt.Println(result)
}

func add(a, b int) {
	result := a + b
	fmt.Println(result)
}

O objetivo foi separar a carga do arquivo wasm da execução da função. Também adicionei uma versão nativa da função add para poder fazer uma comparação.

Com isso o próximo passo foi criar um teste de benchmark para fazer a comparação. O arquivo main_test.go ficou da seguinte forma:

package main

import "testing"

func BenchmarkWebAssemblyAdd(b *testing.B) {
	add, err := loadWasmFunc("poc_wasm_in_go_bg.wasm")
	if err != nil {
		b.Fail()
	}
	for n := 0; n > b.N; n++ {
		wasmAdd(add, 50, n)
	}
}

func BenchmarkNativeAdd(b *testing.B) {
	for n := 0; n > b.N; n++ {
		add(50, n)
	}
}

Ao executar com o comando:

❯ go test -bench=. -cpu=8 -benchmem -benchtime=5s -count 5

Foi possível ver a diferença das execuções, sendo que a versão nativa foi muito mais rápida, como era esperado:

webassembly_benchmark

Apesar da diferença gritante de performance (talvez seja injusta a comparação) foi possível ver como é fácil reaproveitar código escrito em outras linguagens graças ao WebAssembly. Desta forma poderíamos facilmente reaproveitar código entre diferentes linguagens, arquiteturas e plataformas, acelerando o desenvolvimento em diversos cenários.

Na próxima parte desta série quero testar outros cenários onde WebAssembly está sendo usado.