Оригинал статьи.
В феврале 2017 года член команды go Brad Fitzpatrick предложил сделать поддержку WebAssembly в языке. Спустя четыре месяца в ноябре 2017 автор GopherJS Ричард Музиол начал реализовывать идею. И, наконец, полная реализация была смержена в mаster. Разработчики получат wasm примерно в августе 2018, с версией go 1.11. В результате, стандартная библиотека берёт на себя почти все технические сложности с импортом и экспортом функций, знакомых вам, если вы уже пробовали компилировать Си в wasm. Звучит многообещающе. Давайте посмотрим, что можно сделать с первой версией.
Все примеры в этой статье, могут быть запущены из docker контейнеров, что лежат в репозитории автора:
Затем перейдите на localhost:32XXX/, и переходите от одной ссылке к другой.
Создание базового «hello world» и концепции уже довольно хорошо задокументированы (даже на русском), поэтому давайте просто побыстее перейдём к более тонким вещам.
Самое необходимое — свежескомпилированная версия Go, поддерживающая wasm. Я не буду пошагово описывать установку, просто знайте, что необходимое уже в master.
Если вы не хотите беспокоиться об этом, Dockerfile c go доступен в репозитории golub-wasm на github, или ещё быстрее можно взять образ из nlepage/golang_wasm.
Теперь вы можете написать традиционный
В образе nlepage/golang_wasm уже установлены переменные окружения GOOS и GOARCH, поэтому можно использовать файл
Последний шаг заключается в использовании файлов
Вам просто нужно отдавать 3 статических файла, используя nginx, например, тогда wasm_exec.html отобразит кнопку «run» (включится, только если
Примечательно, что
Вы можете использовать образ nginx из nlepage/golang_wasm, который уже включает исправленный MIME тип,
Теперь нажмите кнопку «run», затем откройте консоль своего браузера, и вы увидите приветствие console.log(«Hello Wasm!»).
Полный пример доступен тут.
Теперь, когда успешно запустили первый двоичный файл WebAssembly, скомпилированный из Go, давайте немного подробнее рассмотрим предоставляемые возможности.
Новый пакет syscall/js внесён в стандартную библиотеку, рассмотрим главный файл —
Доступен новый тип
Он предлагает простой API для управления JavaScript переменными:
И дополнительные интересные методы:
Вместо вывода сообщения в os.StdOut, давайте отобразим его в окне оповещения с помощью
Поскольку находимся в браузере, глобальная область видимости — окно, поэтому сначала надо получить alert() из глобальной области:
Теперь у нас есть переменная
Как можно увидеть, нет необходимости вызывать js.ValueOf() перед передачей аргументов Invoke, он принимает произвольное количество
Теперь наша новая программа должна выглядеть так:
Как и в первом примере, просто нужно создать файл с именем
Теперь, когда нажимаем кнопку «Run», появляется alert окно с нашим сообщением.
Рабочий пример есть в папке
Вызов JS из Go довольно прост, давайте рассмотрим внимательнее пакет
Давайте попробуем сделать что-то простое: запустить Go
Внесём некоторые изменения в
Это запускает двоичный файл wasm и ждет его завершения, затем повторно инициализирует его для следующего запуска.
Давайте добавим новую функцию, которая получит и сохранит обратный вызов Go и изменит состояние
Теперь давайте адаптируем функцию
И это на стороне JS!
Теперь в части Go нужно создать обратный вызов, отправить его на сторону JS и ожидать, когда функция понадобится.
Затем должны написать настоящую функцию
Аргументы переданы через срез
Теперь можем обернуть эту функцию в обратный вызов:
Затем вызовите функцию JS
Последнее, что нужно сделать, это дождаться вызова callback в main:
Эта последняя часть важна, потому что обратные вызовы выполняются в выделенной goroutine, и основная goroutine должна ждать вызова callback'а, иначе двоичный файл wasm будет остановлен преждевременно.
Полученная в результате программа Go должна выглядеть так:
Как в предыдущих примерах создадим файл с именем
Теперь, при надатии кнопки «run», как в нашем первом примере, сообщение печатается в консоли браузера, но на этот раз это намного лучше! (И сложнее.)
Рабочий пример в биде docker файла доступен в папке
Вызов Go from JS является немного более громоздким, чем вызов JS от Go, особенно на стороне JS.
Это в основном связано с тем, что нужно дождаться, когда результат обратного вызова Go будет передан стороне JS.
Давайте попробуем что-то другое: почему бы не организовать двоичный файл wasm, который не завершится сразу после вызова callback, а будет продолжать работать и принимать другие вызовы.
На этот раз давайте начнем со стороны Go, и как в нашем предыдущем примере, нужно создать обратный вызов и отправить его стороне JS.
Добавим счетчик вызовов, чтобы отслеживать, сколько раз была вызвана функция.
Наша новая функция
Создание обратного вызова и отправка его на сторону JS такое же, как в предыдущем примере:
Но на этот раз у нас нет канала
Это не удовлетворительно, наш двоичный wasm будет просто висеть в памяти до закрытия вкладки браузера.
Можно прослушивать событие
На этот раз новая функция
Затем обернём его в обратный вызов с помощью
Наконец, заменим пустой, блокирующий
Финальная программа выглядит так:
Раньше, на стороне JS, загрузка двоичного файла wasm выглядела так:
Давайте адаптируем её для запуска двоичного файла сразу после загрузки:
И заменим кнопку «Run» полем сообщения и кнопкой для вызова
Наконец, функция
Теперь, когда нажимаем кнопку «Print message», должны увидеть сообщение по нашему выбору и счетчик вызовов, напечатанный в консоли браузера.
Если установим флажок «Preserve log» консоли браузера и обновим страницу, увидим сообщение «Bye Wasm!».
Исходники доступны в папке
Как можете видеть, изученный
На данный момент невозможно вернуть значение в JS непосредственно из обратного вызова Go.
Надо иметь ввиду, что все обратные вызовы выполняются в одной и той же goroutin'е, поэтому, если вы делаете некоторые блокирующие операции в обратном вызове, не забудьте создать новую goroutin'у, иначе вы заблокируете выполнение всех остальных обратных вызовов.
Все основные функции языка уже доступны, включая параллелизм. Пока все goroutin'ы будут работать в одном потоке, но это изменится в будущем.
В наших примерах использовали только пакет fmt из стандартной библиотеки, но доступно всё, что не не пытается сбежать из песочницы.
Кажется, что файловая система поддерживается через Node.js.
Наконец, как насчет производительности? Было бы интересно запустить некоторые тесты, чтобы увидеть, как Go wasm сравнивается с эквивалентным чистым JS-кодом. Некто hajimehoshi сделал замеры, как разные среды работают с целыми числами, но методика не очень понятна.
Не надо забывать, что Go 1.11 ещё даже не вышел официально. По-моему очень неплохо для экспериментальной технологии. Те, кому интересны тесты производительности, могут помучать свой браузер.
Основная ниша, как отмечает автор — перенос с сервера на клиент уже существующего go кода. Но с новыми стандартами можно делать полностью offline приложения, а wasm код сохраняется в скомпилированном виде. Можно много утилит в web перенести, согласитесь, удобно?
В феврале 2017 года член команды go Brad Fitzpatrick предложил сделать поддержку WebAssembly в языке. Спустя четыре месяца в ноябре 2017 автор GopherJS Ричард Музиол начал реализовывать идею. И, наконец, полная реализация была смержена в mаster. Разработчики получат wasm примерно в августе 2018, с версией go 1.11. В результате, стандартная библиотека берёт на себя почти все технические сложности с импортом и экспортом функций, знакомых вам, если вы уже пробовали компилировать Си в wasm. Звучит многообещающе. Давайте посмотрим, что можно сделать с первой версией.
Все примеры в этой статье, могут быть запущены из docker контейнеров, что лежат в репозитории автора:
docker container run -dP nlepage/golang_wasm:examples
# Find out which host port is used
docker container ls
Затем перейдите на localhost:32XXX/, и переходите от одной ссылке к другой.
Привет, Wasm!
Создание базового «hello world» и концепции уже довольно хорошо задокументированы (даже на русском), поэтому давайте просто побыстее перейдём к более тонким вещам.
Самое необходимое — свежескомпилированная версия Go, поддерживающая wasm. Я не буду пошагово описывать установку, просто знайте, что необходимое уже в master.
Если вы не хотите беспокоиться об этом, Dockerfile c go доступен в репозитории golub-wasm на github, или ещё быстрее можно взять образ из nlepage/golang_wasm.
Теперь вы можете написать традиционный
helloworld.go
и скомпилировать его с помощью следующей команды:GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
В образе nlepage/golang_wasm уже установлены переменные окружения GOOS и GOARCH, поэтому можно использовать файл
Dockerfile
, подобный этому, для компиляции:FROM nlepage/golang_wasm
COPY helloworld.go /go/src/hello/
RUN go build -o test.wasm hello
Последний шаг заключается в использовании файлов
wasm_exec.html
и wasm_exec.js
, доступных в репозитории go в каталоге misc/wasm
или в docker образе nlepage/golang_wasm в каталоге /usr/local/go/misc/wasm/
, для выполнения test.wasm
в браузере (wasm_exec.js ожидает двоичный файл test.wasm
, поэтому используем это имя).Вам просто нужно отдавать 3 статических файла, используя nginx, например, тогда wasm_exec.html отобразит кнопку «run» (включится, только если
test.wasm
загружен правильно).Примечательно, что
test.wasm
необходимо обслуживать с MIME типом application/wasm
, иначе браузер откажется от его исполнения. (например, nginx нуждается в обновленном файле mime.types).Вы можете использовать образ nginx из nlepage/golang_wasm, который уже включает исправленный MIME тип,
wasm_exec.html
и wasm_exec.js
в каталоге code>/usr/share/nginx/html/.Теперь нажмите кнопку «run», затем откройте консоль своего браузера, и вы увидите приветствие console.log(«Hello Wasm!»).
Полный пример доступен тут.
Вызов JS из Go
Теперь, когда успешно запустили первый двоичный файл WebAssembly, скомпилированный из Go, давайте немного подробнее рассмотрим предоставляемые возможности.
Новый пакет syscall/js внесён в стандартную библиотеку, рассмотрим главный файл —
js.go
.Доступен новый тип
js.Value
, который представляет значение JavaScript.Он предлагает простой API для управления JavaScript переменными:
js.Value.Get()
иjs.Value.Set()
возвращают и устанавливают значения полей объекта.js.Value.Index()
иjs.Value.SetIndex()
обращаются к объекту по индексу на чтение и запись.js.Value.Call()
вызывает метод объекта как функцию.js.Value.Invoke()
вызывает сам объект как функцию.js.Value.New()
вызывает оператор new и использует собственное знаяение как конструктор.- Еще несколько методов для получения значения JavaScript в соответствующем типе Go, например
js.Value.Int()
илиjs.Value.Bool()
.
И дополнительные интересные методы:
js.Undefined()
даст js.Value соответствующийundefined
.js.Null()
дастjs.Value
соответствующийnull
.js.Global()
вернётjs.Value
, дающее доступ к глобальной области видимости.js.ValueOf()
принимает примитивные типы Go и возвращают корректноеjs.Value
Вместо вывода сообщения в os.StdOut, давайте отобразим его в окне оповещения с помощью
window.alert()
.Поскольку находимся в браузере, глобальная область видимости — окно, поэтому сначала надо получить alert() из глобальной области:
alert := js.Global().Get("alert")
Теперь у нас есть переменная
alert
, в виде js.Value
, которая является ссылкой на window.alert
JS, и можно использовать вызвать функцию через js.Value.Invoke()
:alert.Invoke("Hello wasm!")
Как можно увидеть, нет необходимости вызывать js.ValueOf() перед передачей аргументов Invoke, он принимает произвольное количество
interface{}
и пропускает значения через ValueOf самостоятельно.Теперь наша новая программа должна выглядеть так:
package main
import (
"syscall/js"
)
func main() {
alert := js.Global().Get("alert")
alert.Invoke("Hello Wasm!")
}
Как и в первом примере, просто нужно создать файл с именем
test.wasm
, и оставить wasm_exec.html
и wasm_exec.js
как было.Теперь, когда нажимаем кнопку «Run», появляется alert окно с нашим сообщением.
Рабочий пример есть в папке
examples/js-call
.Вызов Go из JS.
Вызов JS из Go довольно прост, давайте рассмотрим внимательнее пакет
syscall/js
, второй файл для просмотра — callback.go
.js.Callback
тип-обёртка для функции Go, для использования в JS.js.NewCallback()
функция, которая принимает функцию (принимающую срезjs.Value
и ничего не возвращающую), и возвращаетjs.Callback
.- Некоторая механика для управления активными обратными вызовами и
js.Callback.Release()
, который должен вызываться для уничтожения обратного вызова. js.NewEventCallback()
аналогичноjs.NewCallback()
, но оборачиваемая функция принимает только 1 аргумент — событие.
Давайте попробуем сделать что-то простое: запустить Go
fmt.Println()
со стороны JS.Внесём некоторые изменения в
wasm_exec.html
, что бы иметь возможность получить обратный вызов от Go, чтобы вызвать его.async function run() {
console.clear();
await go.run(inst);
inst = await WebAssembly.instantiate(mod, go.ImportObject); // сброс экземпляра
}
Это запускает двоичный файл wasm и ждет его завершения, затем повторно инициализирует его для следующего запуска.
Давайте добавим новую функцию, которая получит и сохранит обратный вызов Go и изменит состояние
Promise
по завершению:let printMessage // Our reference to the Go callback
let printMessageReceived // Our promise
let resolvePrintMessageReceived // Our promise resolver
function setPrintMessage(callback) {
printMessage = callback
resolvePrintMessageReceived()
}
Теперь давайте адаптируем функцию
run()
для использования обратного вызова:async function run() {
console.clear()
// Create the Promise and store its resolve function
printMessageReceived = new Promise(resolve => {
resolvePrintMessageReceived = resolve
})
const run = go.run(inst) // Start the wasm binary
await printMessageReceived // Wait for the callback reception
printMessage('Hello Wasm!') // Invoke the callback
await run // Wait for the binary to terminate
inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance
}
И это на стороне JS!
Теперь в части Go нужно создать обратный вызов, отправить его на сторону JS и ожидать, когда функция понадобится.
var done = make(chan struct{})
Затем должны написать настоящую функцию
printMessage()
:func printMessage(args []js.Value) {
message := args[0].Strlng()
fmt.Println(message)
done <- struct{}{} // Notify printMessage has been called
}
Аргументы переданы через срез
[]js.Value
, поэтому нужно вызвать js.Value.String()
в первом элементе среза, чтобы получить сообщение в строке Go.Теперь можем обернуть эту функцию в обратный вызов:
callback := js.NewCallback(printMessage)
defer callback.Release() // to defer the callback releasing is a good practice
Затем вызовите функцию JS
setPrintMessage()
, точно так же, как при вызове window.alert()
:setPrintMessage := js.Global.Get("setPrintMessage")
setPrintMessage.Invoke(callback)
Последнее, что нужно сделать, это дождаться вызова callback в main:
<-done
Эта последняя часть важна, потому что обратные вызовы выполняются в выделенной goroutine, и основная goroutine должна ждать вызова callback'а, иначе двоичный файл wasm будет остановлен преждевременно.
Полученная в результате программа Go должна выглядеть так:
package main
import (
"fmt"
"syscall/js"
)
var done = make(chan struct{})
func main() {
callback := js.NewCallback(prtntMessage)
defer callback.Release()
setPrintMessage := js.Global().Get("setPrintMessage")
setPrIntMessage.Invoke(callback)
<-done
}
func printMessage(args []js.Value) {
message := args[0].Strlng()
fmt.PrintIn(message)
done <- struct{}{}
}
Как в предыдущих примерах создадим файл с именем
test.wasm
. Также нужно заменить wasm_exec.html
на нашу версию, а wasm_exec.js
сможем использовать повторно.Теперь, при надатии кнопки «run», как в нашем первом примере, сообщение печатается в консоли браузера, но на этот раз это намного лучше! (И сложнее.)
Рабочий пример в биде docker файла доступен в папке
examples/go-call
.Долгая работа
Вызов Go from JS является немного более громоздким, чем вызов JS от Go, особенно на стороне JS.
Это в основном связано с тем, что нужно дождаться, когда результат обратного вызова Go будет передан стороне JS.
Давайте попробуем что-то другое: почему бы не организовать двоичный файл wasm, который не завершится сразу после вызова callback, а будет продолжать работать и принимать другие вызовы.
На этот раз давайте начнем со стороны Go, и как в нашем предыдущем примере, нужно создать обратный вызов и отправить его стороне JS.
Добавим счетчик вызовов, чтобы отслеживать, сколько раз была вызвана функция.
Наша новая функция
printMessage()
будет печатать полученное сообщение и значение счетчика:var no int
func printMessage(args []js.Value) {
message := args[0].String()
no++
fmt.Printf("Message no %d: %s\n", no, message)
}
Создание обратного вызова и отправка его на сторону JS такое же, как в предыдущем примере:
callback := js.NewCallback(printMessage)
defer callback.Release()
setPrintMessage := js.Global().Get("setPrintMessage")
setPrIntMessage.Invoke(callback)
Но на этот раз у нас нет канала
done
, чтобы уведомить нас о прекращении основной горутин. Один из способов может заключаться в том, чтобы навсегда заблокировать главную goroutin'у пустым select{}
:select{}
Это не удовлетворительно, наш двоичный wasm будет просто висеть в памяти до закрытия вкладки браузера.
Можно прослушивать событие
beforeunload
на странице, понадобится второй обратный вызов для получения события и уведомления главной горутины по каналу:var beforeUnloadCh = make(chan struct{})
На этот раз новая функция
beforeUnload()
будет принимать только событие, в виде единственного js.Value
аргумента:func beforeUnload(event js.Value) {
beforeUnloadCh <- struct{}{}
}
Затем обернём его в обратный вызов с помощью
js.NewEventCallback()
и зарегистрируем на стороне JS:beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Release()
addEventLtstener := js.Global().Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)
Наконец, заменим пустой, блокирующий
select
на чтение из канала beforeUnloadCh
:<-beforeUnloadCh
fmt.Prtntln("Bye Wasm!")
Финальная программа выглядит так:
package main
import (
"fmt"
"syscall/js"
)
var (
no int
beforeUnloadCh = make(chan struct{})
)
func main() {
callback := js.NewCallback(printMessage)
defer callback.Release()
setPrintMessage := js.Global().Get("setPrintMessage")
setPrIntMessage.Invoke(callback)
beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Release()
addEventLtstener := js.Global().Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)
<-beforeUnloadCh
fmt.Prtntln("Bye Wasm!")
}
func printMessage(args []js.Value) {
message := args[0].String()
no++
fmt.Prtntf("Message no %d: %s\n", no, message)
}
func beforeUnload(event js.Value) {
beforeUnloadCh <- struct{}{}
}
Раньше, на стороне JS, загрузка двоичного файла wasm выглядела так:
const go = new Go()
let mod, inst
WebAssembly
.instantiateStreaming(fetch("test.wasm"), go.importObject)
.then((result) => {
mod = result.module
inst = result.Instance
document.getElementById("runButton").disabled = false
})
Давайте адаптируем её для запуска двоичного файла сразу после загрузки:
(async function() {
const go = new Go()
const { instance } = await WebAssembly.instantiateStreaming(
fetch("test.wasm"),
go.importObject
)
go.run(instance)
})()
И заменим кнопку «Run» полем сообщения и кнопкой для вызова
printMessage()
:<input id="messageInput" type="text" value="Hello Wasm!">
<button onClick="printMessage(document.querySelector('#messagelnput').value);"
id="prtntMessageButton"
disabled>
Print message
</button>
Наконец, функция
setPrintMessage()
, которая принимает и сохраняет обратный вызов, должна быть проще:let printMessage;
function setPrintMessage(callback) {
printMessage = callback;
document.querySelector('#printMessageButton').disabled = false;
}
Теперь, когда нажимаем кнопку «Print message», должны увидеть сообщение по нашему выбору и счетчик вызовов, напечатанный в консоли браузера.
Если установим флажок «Preserve log» консоли браузера и обновим страницу, увидим сообщение «Bye Wasm!».
Исходники доступны в папке
examples/long-running
на github.А дальше?
Как можете видеть, изученный
syscall/js
API делает своё дело и позволяет писать сложные вещи небольшим количеством кода. Можете написать автору, если знаете способ проще.На данный момент невозможно вернуть значение в JS непосредственно из обратного вызова Go.
Надо иметь ввиду, что все обратные вызовы выполняются в одной и той же goroutin'е, поэтому, если вы делаете некоторые блокирующие операции в обратном вызове, не забудьте создать новую goroutin'у, иначе вы заблокируете выполнение всех остальных обратных вызовов.
Все основные функции языка уже доступны, включая параллелизм. Пока все goroutin'ы будут работать в одном потоке, но это изменится в будущем.
В наших примерах использовали только пакет fmt из стандартной библиотеки, но доступно всё, что не не пытается сбежать из песочницы.
Кажется, что файловая система поддерживается через Node.js.
Наконец, как насчет производительности? Было бы интересно запустить некоторые тесты, чтобы увидеть, как Go wasm сравнивается с эквивалентным чистым JS-кодом. Некто hajimehoshi сделал замеры, как разные среды работают с целыми числами, но методика не очень понятна.
Не надо забывать, что Go 1.11 ещё даже не вышел официально. По-моему очень неплохо для экспериментальной технологии. Те, кому интересны тесты производительности, могут помучать свой браузер.
Основная ниша, как отмечает автор — перенос с сервера на клиент уже существующего go кода. Но с новыми стандартами можно делать полностью offline приложения, а wasm код сохраняется в скомпилированном виде. Можно много утилит в web перенести, согласитесь, удобно?