DevGang
Авторизоваться

Основы: Wasm в Golang - это фантастика 

В этой стате мы увидим, как сделать ваши первые шаги в Wasm, попробуем передать данные из Golang в JavaScript и некоторые рецепты, которые помогут вам продвинуться дальше.

Начнем

Что ж, в названии сказано: «Wasm in Golang - это фантастика», но что такое «Wasm» в двух словах?

На домашней странице WebAssembly говорится: «WebAssembly (сокращенно Wasm) - это двоичный формат инструкций для виртуальной машины на основе стека. Wasm разработан как переносимая цель компиляции для языков программирования, позволяющая развертывать в Интернете клиентские и серверные приложения».

Я бы сказал:

  • «Wasm - это переносимый формат (например, Java или .Net), и вы можете выполнять его везде, где у вас есть хост, способный к этому. Первоначально основным хостом был JavaScript с браузером».

Теперь вы можете запускать Wasm с помощью JavaScript и NodeJS, и недавно мы увидели рождение сред выполнения Wasm, таких как проект Wasmer, позволяющих запускать Wasm повсюду.

Я люблю говорить это "wasm-файл похож на образ контейнера, но меньше по размеру и без операционной системы".

Wasm полиглот, но...

Вы можете скомпилировать файл Wasm с несколькими языками: C / C++, Rust, GoLang, Swift, ... И мы даже видели появление языков, посвященных сборке Wasm, таких как AssemblyScript или многообещающий Grain.

Этим летом я решил начать с Wasm. Кажется, что для этого есть тенденция использовать Rust, но я быстро понял, что мои детские шаги будут сложными. Сложность не обязательно связана с самим языком. Самой утомительной и сложной частью были все инструменты, необходимые для запуска простого "Hello World" в браузере (1). После некоторых поисков я обнаружил, что Golang обеспечивает довольно простую поддержку Wasm (намного проще, чем с Rust). Итак, моя домашняя работа на каникулах была сделана с Go.

Поддержка Wasm с Golang просто фантастическая. Обычно WebAssembly имеет четыре типа данных (32- и 64-разрядное целое число, 32- и 64-разрядное число с плавающей запятой), и использование функций со строковыми параметрами (или даже объектов JSON) может вызвать затруднения. К счастью, Go предоставляет файл wasm_exec.js, который упрощает взаимодействие с JavaScript API.

(1): Через месяц я покопался более серьезно, и думаю, что это в основном документация и примеры, которые не подходят для новичков. «Первые шаги с Wasm in Rust» могут стать темой будущей статьи.

Предпосылки

Чтобы запустить примеры из этой статьи, вам понадобятся:

  1. Golang 1.16
  2. TinyGo 0.19.0 (примечание: TinyGo 0.19.0 не работает с GoLang 1.17)
  3. HTTP-сервер для обслуживания ваших веб-страниц

Кстати, для обслуживания своих страниц я использую Fastify с этим кодом:

index.js
const fastify = require('fastify')({ logger: true })
const path = require('path')

// Serve the static assets
fastify.register(require('fastify-static'), {
  root: path.join(__dirname, ''),
  prefix: '/'
})

const start = async () => {
  try {
    await fastify.listen(8080, "0.0.0.0")
    fastify.log.info(`server listening on ${fastify.server.address().port}`)

  } catch (error) {
    fastify.log.error(error)
  }
}
start()

и я использую этот файл package.json для установки Fastify:

package.json
{
    "dependencies": {
        "fastify": "^3.6.0",
        "fastify-static": "^3.2.1"
    }
}

Для таких ленивых, как я, я создал здесь проект https://gitlab.com/k33g_org/suborbital-demo и если вы откроете его с помощью GitPod, вы получите готовую к использованию среду разработки, и вам не нужно ничего устанавливать.

Важнейшее слово "hello world!"

Создать быстрый и грязный проект

Сначала создайте каталог hello-world, а затем внутри этого каталога создайте 2 файла:

  1. main.go
  2. index.html

со следующим исходным кодом:

main.go
package main

import (
    "fmt"
)

func main() {
  fmt.Println("👋 Hello World 🌍")
    // Prevent the function from returning, which is required in a wasm module
    <-make(chan bool)
}
index.html
<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // This is a polyfill for FireFox and Safari
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) => {
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

            // Promise to load the wasm file
           function loadWasm(path) {
             const go = new Go()

             return new Promise((resolve, reject) => {
               WebAssembly.instantiateStreaming(fetch(path), go.importObject)
               .then(result => {
                 go.run(result.instance)
                 resolve(result.instance)
               })
               .catch(error => {
                 reject(error)
               })
             })
           }

         // Load the wasm file
         loadWasm("main.wasm").then(wasm => {
             console.log("main.wasm is loaded 👋")
         }).catch(error => {
             console.log("ouch", error)
         }) 

        </script>
    </body>
</html>

Замечание: самые важные части:

  • Эта строка <script src="wasm_exec.js"></script>
  • И эта WebAssembly.instantiateStreaming, API JavaScript, который позволяет загрузить файл wasm.

Вам также необходимо создать файл go.mod, используя эту команду:

go mod init hello-world

У вас должен получиться такое:

go.mod
module hello-world

go 1.16

Создайте свой первый модуль Wasm

Перед сборкой модуля Wasm вам необходимо получить файл wasm_exec.js, после чего вы сможете запустить компиляцию:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
GOOS=js GOARCH=wasm go build -o main.wasm

А теперь обслуживайте свою html-страницу с помощью команды node index.js чтобы запустить http-сервер Fastify и перейти к http://localhost: 8080 в вашем любимом браузере и откройте инструменты разработчика консоли:

Итак, начать довольно просто, но если вы посмотрите на размер main.wasm, вы обнаружите, что размер сгенерированного файла составляет около 2,1МБ!!! и честно говоря, я считаю это неприемлемым. К счастью, у нас есть удобное решение сTinyGo. Посмотрим на это.

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/01-hello-world

Важнейшее слово "hello world!" с TinyGo

Во-первых, что такое TinyGo? TinyGo позволяет компилировать исходный код Golang для микроконтроллеров, а также может компилировать код Go в Wasm. TinyGo - компилятор, предназначенный для использования в «маленьких местах», поэтому сгенерированные файлы значительно меньше.

Скопируйте ваш проект hello-world в новый каталог hello-world-tinygo и измените содержание файла go.mod:

module hello-world-tinygo

go 1.16

Прежде чем создавать файл Wasm, на этот раз вам необходимо получить wasm_exec.js совместно с TinyGo, и тогда вы сможете запустить компиляцию:

wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.19.0/targets/wasm_exec.js
tinygo build -o main.wasm -target wasm ./main.go

Если вы запустите свою html-страницу, вы получите тот же результат, что и в предыдущем примере. Но посмотрите на размер main.wasm. Теперь размер составляет 223K, и это намного лучше.

Имейте в виду, что TinyGo поддерживает подмножество языка Go, поэтому еще не все доступно (tinygo.org/docs/reference/lang-support). Для моих экспериментов этого было достаточно; в противном случае продолжайте с "чистым" Go.

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/02-hello-world-tinygo

Моя маленькая кулинарная книга

Я видел слишком много длинных учебных пособий, которые в конце концов остановились на этом простом "hello world", не продвигаясь дальше. Они даже не объясняют, как изменять параметры функций. Часто это всего лишь украшение "getting started", никогда не продвигаясь дальше.

Поэтому сегодня я даю вам все маленькие рецепты, которые позволили мне пойти немного дальше.

Вот различные взаимодействия между Wasm и браузером, о которых я расскажу сегодня:

  1. Взаимодействие с DOM
  2. Получите строку, вызвав функцию Golang со строкой в качестве параметра
  3. Как вернуть объект, "читаемый" с помощью JavaScript?
  4. Как использовать объект JSON в качестве параметра?
  5. Как использовать массив в качестве параметра?
  6. Как вернуть массив?

Взаимодействие с DOM

Мы будем использовать "syscall/js" пакет Golang для добавления дочерних тегов в объектную модель html-документа из кода Go. Согласно документации: "Пакет js предоставляет доступ к среде хоста WebAssembly при использовании архитектуры js/wasm. Его API основан на семантике JavaScript". Этот пакет предоставляет небольшой набор функций: тип Value (представление данных Go JavaScript) и способ запроса Go от хоста JavaScript.

  1. Создайте новый каталог, скопировав предыдущий, и назовите его dom
  2. Обновите файл go.mod:
module dom

go 1.16

Просто измените код main.go:

package main

import (
    "syscall/js"
)

func main() {

  message := "👋 Hello World 🌍"

    document := js.Global().Get("document")
    h2 := document.Call("createElement", "h2")
    h2.Set("innerHTML", message)
    document.Get("body").Call("appendChild", h2)

    <-make(chan bool)
}

Создайте код tinygo build -o main.wasm -target wasm ./main.go и обслуживайте html-страницу node index.js, затем перейдите к http://localhost:8080

  1. У нас есть ссылка на DOM с js.Global().Get("document")
  2. Мы создали элемент <h2></h2> с document.Call("createElement", "h2")
  3. Мы устанавливаем значение innerHTML с h2.Set("innerHTML", message)
  4. И, наконец, добавьте элемент в тело с помощью document.Get("body").Call("appendChild", h2)

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/03-dom

Теперь давайте посмотрим, как сделать вызываемую функцию Go, которую мы будем использовать на нашей html-странице.

Вызов функции Go

На этот раз нам нужно "экспортировать" функцию в глобальный контекст (т.е. window в браузере, global в NodeJS). Снова GoLang пакет "syscall/js" предоставляет для этого необходимые функции.

Как обычно, создайте новый каталог first-function (используйте предыдущий пример) и обновите файл go.mod путем изменения значения модуля: module first-function.

Это исходный код main.go:

package main

import (
    "syscall/js"
)

func Hello(this js.Value, args []js.Value) interface{} {
    message := args[0].String() // get the parameters
    return "Hello " + message
}

func main() {
    js.Global().Set("Hello", js.FuncOf(Hello))

    <-make(chan bool)
}
  1. Чтобы экспортировать функцию в глобальный контекст, мы использовали функцию: js.Global().Set("Hello", js.FuncOf(Hello)). Функция FuncOf используется для создания типа Func.
  2. Функция Hello принимает два параметра и возвращает тип interface{}. Функция будет вызываться синхронно из Javascript. Первый параметр (this) относится к JavaScript объекту global. Второй параметр - это срез []js.Value представление аргументов, переданных вызову функции Javascript.

Нам нужно изменить файл index.html для вызова Go функции Hello:

index.html
<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // polyfill
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) => {
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

      function loadWasm(path) {
        const go = new Go()
        return new Promise((resolve, reject) => {
          WebAssembly.instantiateStreaming(fetch(path), go.importObject)
          .then(result => {
            go.run(result.instance)
            resolve(result.instance)
          })
          .catch(error => {
            reject(error)
          })
        })
      }

    loadWasm("main.wasm").then(wasm => {
        console.log("main.wasm is loaded 👋")
        console.log(Hello("Bob Morane"))
        document.querySelector("h1").innerHTML = Hello("Bob Morane")
            }).catch(error => {
        console.log("ouch", error)
      }) 

        </script>
    </body>
</html>

Что изменилось? только эти 2 строки:

  1. console.log(Hello("Bob Morane")): вызов Go функции Hello с "Bob Morane" в качестве параметра и отображение результата в консоли браузера.
  2. вызов document.querySelector("h1").innerHTML = Hello("Bob Morane") в Go функции Hello с "Bob Morane"в качестве параметра и изменение значения тега h1.

Так

  1. Создайте файл Wasm: tinygo build -o main.wasm -target wasm ./main.go
  2. Запустите html-страницу: node index.js

Вы можете видеть, что содержимое страницы обновлено, но у нас есть некоторые сообщения об ошибках в консоли. Не беспокойтесь, это легко исправить; это известная ошибка https://github.com/tinygo-org/tinygo/issues/1140, но обходной путь прост:

function loadWasm(path) {
    const go = new Go()
    //remove the message: syscall/js.finalizeRef not implemented
    go.importObject.env["syscall/js.finalizeRef"] = () => {}

    return new Promise((resolve, reject) => {
        WebAssembly.instantiateStreaming(fetch(path), go.importObject)
        .then(result => {
            go.run(result.instance)
            resolve(result.instance)
        })
        .catch(error => {
            reject(error)
        })
    })
}
  • Я только добавил эту строку go.importObject.env["syscall/js.finalizeRef"] = () => {} чтобы избежать сообщения об ошибке.

Обновите страницу, больше никаких проблем!:

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

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/04-first-function

Другие мои рецепты

Как вернуть "readable" объект с помощью JavaScript?

На этот раз мы передаем 2 строковых параметра в функцию Hello (firstName и lastName), и для возврата объекта json мы используем тип map[string]interface{}:

Функция GoLang:

func Hello(this js.Value, args []js.Value) interface{} {

    firstName := args[0].String()
    lastName := args[1].String()

    return map[string]interface{}{
        "message": "👋 Hello " + firstName + " " + lastName,
        "author":  "@k33g_org",
    }

}

Вызов функции Hello из JavaScript прост:

loadWasm("main.wasm").then(wasm => {
    let jsonData = Hello("Bob", "Morane")
    console.log(jsonData)
    document.querySelector("h1").innerHTML = JSON.stringify(jsonData)

}).catch(error => {
    console.log("ouch", error)
})

Запустите свою страницу node index.js:

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/05-return-object

Как использовать объект Json в качестве параметра при вызове Hello?

Если я хочу использовать объект Json в качестве параметра в JavaScript, например:

let jsonData = Hello({firstName: "Bob", lastName: "Morane"})

Я напишу свою функцию GoLang следующим образом

func Hello(this js.Value, args []js.Value) interface{} {

    // get an object
    human := args[0]
    // get members of an object
    firstName := human.Get("firstName")
    lastName := human.Get("lastName")

    return map[string]interface{}{
        "message": "👋 Hello " + firstName.String() + " " + lastName.String(),
        "author":  "@k33g_org",
    }

}
  1. args[0] содержит объект Json
  2. Используйте способ извлечения значения полей Get(field_name)

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/06-json-as-parameter

Как использовать массив в качестве параметра при вызове Hello?

Вызов JavaScript:

let jsonData = Hello(["Bob", "Morane", 42, 1.80])

Функция Golang:

func Hello(this js.Value, args []js.Value) interface{} {

    // get members of an array
    firstName := args[0].Index(0)
    lastName := args[0].Index(1)
    age := args[0].Index(2)
    size := args[0].Index(3)

    return map[string]interface{}{
        "message":   "👋 Hello",
        "firstName": firstName.String(),
        "lastName":  lastName.String(),
        "age":       age.Int(),
        "size":      size.Float(),
        "author":    "@k33g_org",
    }
}

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/07-array-as-parameter

Как вернуть массив?

Функция Golang:

func GiveMeNumbers(_ js.Value, args []js.Value) interface{} {
    return []interface{} {1, 2, 3, 4, 5}
}

исходный код: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/08-return-an-array

Итак, на этот раз все. Я все еще изучаю Wasm и пакет Js Golang, но я уже получил серьезное удовольствие от всего этого.

#JavaScript #Golang #Wasm
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить