Go Wasm на прикладі створення інструменту для розробників

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, у 2020 році я активно використовував онлайн-інструмент JSON-to-Go, а також на його основі зробив JSON-to-Proto.

Згідно зі статистикою від similarweb.com, JSON-to-Proto відвідують 5000+ разів на місяць, найактивніше з США, Індії та Китаю.

Заради цікавості вирішив глянути, чи потрібен комусь XML-to-Proto та створив відповідну сторінку, а також глянув існуючі інструменти XML-to-Go:

Обидва інструменти роблять перетворення на стороні сервера через API. onlinetool.io/xmltogo — це обгортка над бібліотекою github.com/miku/zek, а тому б було цікаво підключити бібліотеку через Wasm та прибрати виклик до сервера.

Підключити вдалось, про це й буде ця маленька стаття.

Каркас XML-to-Go

Структура інструменту складається з трьох елементів, поля вводу input, поля виводу output та функції, яка перетворює XML в Go xmlDataToGoTypeCode:

<div id="input" contenteditable></div>
<div id="output"></div>
const $input = document.getElementById("input");
const $output = document.getElementById("output");

// буде замінена на Wasm
globalThis.xmlDataToGoTypeCode = function (input: string): string {
    return "";
};

$input.addEventListener("keyup", function () {
    $output.innerHTML = globalThis.xmlDataToGoTypeCode($input.innerText.trim());
});

Далі нам потрібно реалізувати функцію xmlDataToGoTypeCode на Go та скомпілювати у Wasm.

Функція, яка перетворює XML в Go

Як писав раніше, інструмент onlinetool.io/xmltogo під капотом використовує бібліотеку github.com/miku/zek, яка написана на Go та має потрібний нам функціонал для перетворення XML в типи на Go.

Щоб зрозуміти, як використовувати бібліотеку github.com/miku/zek, треба розібрати CLI команду cmd/zek/main.go (permalink), я це вже зробив, тому продовжуйте читати статтю.

Функція xmlDataToGoTypeCode:

cat ./go/wasm/xml2go.go
package main

import (
	"bytes"
	"fmt"
	"go/format"
	"strings"
	"time"

	"github.com/miku/zek"
)

func xmlDataToGoTypeCode(content string, inline, compact, withJSON bool) string {
	var rootNode = new(zek.Node)
	_, err := rootNode.ReadFrom(strings.NewReader(content))
	if err != nil {
		// fmt print error

		return ""
	}

	var (
		buffer = new(bytes.Buffer)
		sw     = zek.NewStructWriter(buffer)
	)
	_ = inline // @TODO sw.Inline = inline after https://github.com/miku/zek/issues/14
	sw.Compact = compact
	sw.WithJSONTags = withJSON

	err = sw.WriteNode(rootNode)
	if err != nil {
		// fmt print error

		return ""
	}

	source, err := format.Source(buffer.Bytes())
	if err != nil {
		// fmt print error

		return ""
	}

	return string(source)
}

Компіляція Go Wasm

Функцію xmlDataToGoTypeCode потрібно зробити доступною для використання в JavaScript:

tree -h ./go/wasm/
./go/wasm/
├── [ 551]  main.go
└── [1.1K]  xml2go.go
cat ./go/wasm/main.go
package main

import (
	"fmt"
	"syscall/js"
)

func main() {
	fmt.Println("Golang WebAssembly main")

	// globalThis.xmlDataToGoTypeCode = function () {}
	js.Global().Set("xmlDataToGoTypeCode", js.FuncOf(xmlDataToGoTypeCodeWasmWrapper))

	done := make(chan struct{})
	<-done
}

func xmlDataToGoTypeCodeWasmWrapper(this js.Value, args []js.Value) interface{} {
	var (
		content  = args[0].String()
		inline   = args[1].Bool()
		compact  = args[2].Bool()
		withJSON = args[3].Bool()
	)

	return xmlDataToGoTypeCode(content, inline, compact, withJSON)
}

Компіляція:

GOOS=js GOARCH=wasm go build -o ./static/js/wasm/xml-to-go.wasm ./go/wasm/*.go
tree -h ./static/js/wasm
./static/js/wasm
└── [4.5M]  xml-to-go.wasm

Отже, маємо скомпільований файл xml-to-go.wasm, який важить 4.5 мегабайти.

Підключення Go Wasm в JavaScript

Для виконання Wasm потрібно підключити файл wasm_exec.js. В офіційній документації це виглядає так:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
<html>
<head>
    <meta charset="utf-8"/>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
</head>
<body></body>
</html>

Клас Go, go.importObject та go.run прописані у файлі wasm_exec.js:

cat wasm_exec.js
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

"use strict";

(() => {
    // ...

    globalThis.Go = class {
        constructor() {
            // ...

            this.importObject = {
                go: {
                    // func wasmExit(code int32)
                    "runtime.wasmExit": (sp) => {
                        // ...
                    },

                    // ...

                    // func copyBytesToGo(dst []byte, src ref) (int, bool)
                    "syscall/js.copyBytesToGo": (sp) => {
                        // ...
                    },

                    // func copyBytesToJS(dst ref, src []byte) (int, bool)
                    "syscall/js.copyBytesToJS": (sp) => {
                        // ...
                    },

                    "debug": (value) => {
                        console.log(value);
                    },
                }
            };
        }

        async run(instance) {
            // ...
        }
    }
})();

Зробимо так само:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./src/wasm/
import "./wasm/wasm_exec"

const $input = document.getElementById("input");
const $output = document.getElementById("output");
const $inline = document.getElementById("inline") as HTMLInputElement;
const $withJSON = document.getElementById("with-json-tags") as HTMLInputElement;
const $compact = document.getElementById("compact") as HTMLInputElement;

$input.addEventListener("keyup", function () {
    $output.innerHTML = globalThis.xmlDataToGoTypeCode(
        $input.innerText.trim(),
        $inline.checked,
        $withJSON.checked,
        $compact.checked,
    );
});

// Go from ./wasm/wasm_exec
const go = new globalThis.Go();
WebAssembly
    .instantiateStreaming(fetch("/static/js/wasm/xml-to-go.wasm"), go.importObject)
    .then(function (result) {
        go.run(result.instance);
    });

Весь код доступний в цьому репозиторії.

Епілог

WebAssembly — потужна технологія, є навіть приклади WebAssembly Go Playground:

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую, той рідкісний момент коли дізнався щось нове технічне на доу :)

vmail.leopard.in.ua — без сервера перевіряє які HTML теги чи CSS правила не будуть працювати у email клієнтах. Wasm модуль написаний на GO (опенсорс).

З мінусів — GO WASM найбільший за розміром (мегабайти), ніж якщо писати на C чи Rust. Tinygo може в цьому допомогти, але не будь який Golang код можна через нього конвертувати в компактніший WASM

Для прикладу browserlist.leopard.in.ua WASM на Rust значно меньший за розміром

Дуже близько:
vmail.leopard.in.ua parser.wasm 3,8 MB
browserlist.leopard.in.ua browserslistwasm_bg.wasm 3,5 MB

Так, не великий відрив. Ось якщо порівнювати з С + emscripten (тут варіація — webp.leopard.in.ua/ ), то нормальний відрив (звичайно rust і go багато докидує своего в wasm)

3.3M  - browserslistwasm_bg.wasm
3.6M  - parser.wasm
253K  - webp.wasm

Підписатись на коментарі