Работа с WebAssembly в Golang

WebAssembly позволяет запускать в браузере код низкоуровневых языков, таких как C, C++, Rust и Go. Мы компилируем Go-код в байт-код, и когда инстанцируем его в теге html-скрипта или JS-коде, среда выполнения браузера создает виртуальную машину для выполнения кода WebAssembly (wasm). Для распараллеливания выполнения WASM с основным потоком может использоваться веб-воркер.

Виртуальная машина задействует компилятор JUST IN TIME, который преобразует байт-код в машинный, а также может производить оптимизацию в зависимости от аппаратного обеспечения. WASM безопасен, поскольку работает в среде “песочницы” и не может получить доступ к аппаратному обеспечению базовой системы.

Когда мы загружаем или инстанцируем wasm, мы получаем модуль, который является пакетом Go-кода. При инстанцировании создается экземпляр wasm, являющийся отдельным контекстом выполнения модуля.

Хватит теории  —  перейдем к коду.

Создание проекта

Сначала нужно создать проект Golang. Для этого выполним следующую команду.

go mod init wasmgo

Go-код

Добавление функции

Создайте файл main.go со следующим содержимым:

package main

import (
"fmt"
"syscall/js"
)

// создание json-функции, совместимой с WebAssembly
func add() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 2 {
return "Invalid number of argument"
}

// Примечание: конвертация типов важна, поскольку операторы не перегружены для js.Value
// Вы получите сообщение "invalid operation: operator + not defined on arg1 (variable of type js.Value)".
arg1 := args[0].Int()
arg2 := args[1].Int()

return arg1 + arg2
})
}

// регистрация обратных вызовов, совместимых с js
func registerCallbacks() {
// вызов функции и задание ей имени, которое будет использоваться в js
js.Global().Set("add", add())
}

func main() {
fmt.Println("running go as webassembly")
registerCallbacks()

// нужно добавить канал и вызвать его, чтобы программа Golang не закрылась, иначе получим следующую ошибку:
// Uncaught (in promise) Error: Go program has already exited
<-make(chan bool)
}

Мы создали функцию add, которая будет возвращать JS-функцию, используя JS-объект, импортированный из syscall/js. Цель  —  создать функцию, которая будет принимать аргументы из JS-кода и использовать их в Go, затем выполнять операцию и возвращать результат, который может быть использован в JS.

Мы определяем функцию и ожидаем массив []js.Value в качестве аргументов. Нам нужно преобразовать их в соответствующий тип Golang, так как JS не чувствителен к типам. Это можно сделать следующим образом: args[0].Int().

Теперь можем выполнить добавление и вернуть результат.

Регистрация

Нам также необходимо зарегистрировать функцию, чтобы ее можно было вызывать из JS. Даем имя функции и настраиваем ее для использования в браузере.

js.Global().Set("add", add())

Функция main

Осталось только вызвать функцию register в main, но нам также необходимо убедиться, что Go-код не закроется после запуска. Поэтому воспользуемся пустым каналом. Теперь Go-код готов.

func main() {
fmt.Println("running go as webassembly")
registerCallbacks()

// нужно добавить канал и вызвать его, чтобы программа Golang не закрылась, иначе получим следующую ошибку:
// Uncaught (in promise) Error: Go program has already exited
<-make(chan bool)
}

Сборка

Для сборки Go-кода необходимо указать архитектуру и операционную систему, для которой производится сборка, а затем создать сборку с расширением wasm.

GOARCH=wasm GOOS=js go build -o main.wasm main.go

Фронтенд

WebAssembly

Нам необходимо инициализировать WebAssembly с помощью файла main.wasm. После этого запускаем Go-код в этом экземпляре. Можем написать следующий код в отдельном js-файле.

// создание экземпляра Go
const go = new Go();

// запуск экземляра WebAssembly
WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject)
.then(
result => {
// запуск экземпляра Go wasm
go.run(result.instance)
// видим результат в консоли браузера
console.log("Summition happening in Golang")
}
)

HTML

Нужно создать фронтенд приложения, чтобы лучше его представить, но на самом деле реальная работа происходит в тегах script html-файла. Добавьте путь к JS-файлу, который мы только что написали на html  —  в данном случае это go.js.

<!DOCTYPE html>
<html>

<head>
<title>Web Assembly</title>

// перейти к js -> предоставляется самим Golang
<script src="wasm_exec.js"></script>
// инициализация wasm
<script src="go.js"></script>
<script>
let summision = () => {
let resultElement = document.querySelector("#result")
let input1 = document.querySelector("#arg1").value
let input2 = document.querySelector("#arg2").value

console.log("input1: ", input1)
console.log("input2: ", input2)

resultElement.innerHTML = add(Number(input1), Number(input2))
}
</script>
</head>

<body>
<input id="arg1" />
<input id="arg2" />
<button onclick="summision()">add</button>
<h1>Sumission is: <span id="result"></span></h1>
</body>

</html>

Переход к JS

У Golang есть два пути, которые настраиваются при его установке: один содержит все проекты, которые мы устанавливаем, а другой  —  путь, по которому установлен Golang.

// место установки Golang
export PATH=$PATH:/usr/local/go/bin

// сюда попадают все загруженные проекты, это домашний каталог
export GOPATH=$(go env GOPATH)

Cкопируем файл wasm_exec.js из /usr/local/go/misc/wasm в домашний каталог проекта и укажем его путь в теге script.

<script src="wasm_exec.js"></script>

Результат

Откройте localhost:8000, введите несколько чисел, и Golang добавит их за вас.

Общие ошибки

1. Если импорт “syscall/js” остается красным, это означает, что проблема не в коде, а в расширении.

2. Если запускать код не от сервера, то можно получить ошибку следующего вида:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///home/raotalha/Code/TestCode/WebAssembly/main.wasm. (Reason: CORS request not http).

Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.

3. Преобразование типов важно, потому что операторы не перегружены для js.Value, иначе вы получите следующее: invalid operation: operator + not defined on arg1 (variable of type js.Value).

4. Вызовите функцию и задайте ей имя, которое будет использоваться в js, иначе получите ошибку js.Global().Set(“add”, add()).

5. Чтобы Golang-программа не закрылась, нужно добавить канал и вызвать его, иначе получим Uncaught (in promise) Error: Go program has already exited.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Rao Talha: Web Assembly in Golang

Предыдущая статьяSQL: комплексный анализ оттока клиентов
Следующая статьяПереходная анимация: практическое пособие