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

Карцинизация программ Go

Иногда нам приходится встраивать библиотеку, написанную на другом языке, в нашу программу. Это общая тема среди программистов с незапамятных времен. По какой-то причине этот процесс всегда был сопряжен с опасностями, страхом, мучениями и влажными салфетками с ароматом лимона.

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

Однако основная проблема заключается в том, что когда вы используете cgo для связывания функции Rust с программой Go, вам необходимо скопировать общий объект, который генерирует Rust. Вы не можете проверить этот общий объект в своем исходном дереве (он должен быть уникальным для каждого дистрибутива ОС, для каждой ОС, для каждой архитектуры ЦП, как и обычные динамически связанные двоичные файлы). Это действительно работает, но в целом опыт разработчиков оставляет желать лучшего. Ваша сборка больше не является простой go build. Теперь вы должны не забыть запустить cargo build --release и убедиться, что полученный файл .so, .dll или .dylib находится на правильном пути для чтения динамическим компоновщиком ОС.

Это такая большая проблема, что на общем уровне именно поэтому существуют Nix и NixOS. Представьте, насколько это сложно, когда вы подключаете компоненты ОС общего назначения. 

А что, если вы услышите, что есть способ, которым мы могли бы отправить один двоичный файл из Rust, заставить его работать на каждой платформе, поддерживаемой Go, и не изменять процесс сборки, выходящий за рамки простой сборки go? Представьте, насколько это было бы проще. Легко представить, что такая вещь позволила бы пользователям даже не знать, что Rust вообще был задействован, даже если они используют пакет или программу, которая его использует.

Mastodon в проекте

Mastodon хранит инструменты в HTML и представляет этот HTML пользователям API. HTML очень удобен для отображения в браузере, но не так полезен для бота. Особенно, если ваша цель — отправить сообщения на webhook Slack.

Когда вы смотрите на такой инструмент в API:

Его содержание выглядит примерно так:

<p>test mention <span class="h-card"><a href="https://vt.social/@xe" class="u-url mention">@<span>xe</span></a></span> so I can see what HTML mastodon makes</p>

В идеале мы бы хотели, чтобы это выглядело семантически идентично в Slack, может быть, что-то вроде этого:

test mention <https://vt.social/@xe|@xe> so I can see what HTML mastodon makes

Это позволит отобразить ссылку в Slack, как и любую другую гиперссылку. По мере того, как все становится более сложным, Mastodon будет делать больше семантических странностей, таких как невидимые промежутки и другие вещи, которые делают отображение вещей в Slack раздражающим. Представьте себе разницу между этими двумя вещами:

https:// tailscale.com/blog/introducing -tailscale-funnel/

https://tailscale.com/blog/introducing-tailscale-funnel/

Один из них гораздо легче понять людям, чем другой.

Подключение к проекту wazero

Одной из основных особенностей философии UNIX является идея о том, что программы - это простые фильтры, которые хорошо выполняют одну задачу, а затем позволяют вам создавать их новыми и интересными способами. Если вы когда-либо использовали curl и jq вместе для выполнения таких действий, как чтение данных из JSONFeed, вы знаете, как это происходит на практике:

$ curl https://xeiaso.net/blog.json -qsSL | jq .items[0].title -r
The birdsong persists

Необходимо создать небольшую программу на Rust, которая использует lol_html для приема входящего HTML-кода в стиле Mastodon и создания уценки в стиле Slack. Использование простое:

$ echo '<p>test mention <span class="h-card"><a href="https://vt.social/@xe" class="u-url mention">@<span>xe</span></a></span> so I can see what HTML mastodon makes</p>' | ./testdata/mastosan.wasm
test mention <https://vt.social/@xe|@xe> so I can see what HTML mastodon makes

Эта программа принимает ввод на стандартный ввод и возвращает результат на стандартный вывод. Это не совсем соответствует потоку WebAssembly, за исключением случаев, когда вы используете WASI для преодоления разрыва. WASI предоставляет программам WebAssembly достаточную POSIX-подобную среду, чтобы могли работать самые основные вещи, но здесь действительно используются только две основные ее части: стандартный ввод и стандартный вывод.

В Go, если бы вы запускали это как обычный подпроцесс ОС, вероятно, написали бы такой код:

package foo

import (
    "bytes"
    "os/exec"
    "strings"
)

func HTML2Slackdown(input string) (string, error) {
    loc, err := exec.LookPath("mastosan")
    if err != nil {
        return "", err
    }
    
    fout := &bytes.Buffer{}
    cmd := exec.Command(loc)
    cmd.Stdin = bytes.NewBufferString(input)
    cmd.Stdout = fout
    if err := cmd.Run(); err != nil {
        return "", err
    }
    
    return strings.TrimSpace(fout.String()), nil
}

Однако это по-прежнему зависит от программы, скомпилированной для вашей родной ОС и дистрибутива, а также от наличия в папке в вашем $PATH. Это работает, но это не идеально ни в малейшей степени.

Rust позволяет вам создать двоичный файл, ориентированный на WASI, с помощью этого флага компилятора:

cargo build --target wasm32-wasi --release --bin mastosan

Это создаст двоичный файл размером несколько мегабайт в ./target/wasm32-wasi/release/mastosan.wasm. Когда вы запустите его, он будет делать то, что вы хотите.

Теперь вам нужно использовать его из Go. Для этого есть много вариантов, но в нашем примере решим использовать wazero. Общий процесс использования похож на использование подпроцесса с os/exec, но немного отличается, потому что мы внедряем WebAssembly. Это будет выглядеть так:


//go:embed testdata/mastosan.wasm
var mastosanWasm []byte

func HTML2Slackdown(ctx context.Context, text string) (string, error) {
    // create wazero runtime
	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)
    
    // load wasi environment into runtime
	wasi_snapshot_preview1.MustInstantiate(ctx, r)

    // set up standard output and standard input
	fout := &bytes.Buffer{}
	fin := bytes.NewBufferString(text)

    // create runtime configuration
	config := wazero.NewModuleConfig().WithStdout(fout).WithStdin(fin).WithArgs("mastosan")

    // compile the WASM module
	code, err := r.CompileModule(ctx, mastosanWasm)
	if err != nil {
		log.Panicln(err)
	}

    // run the WASM module
	if _, err = r.InstantiateModule(ctx, code, config); err != nil {
		return "", err
	}

	return strings.TrimSpace(fout.String()), nil
}

Это в основном то же самое. Вы настраиваете среду, загружаете модуль WASM и затем запускаете его. Основное отличие состоит в том, что вместо того, чтобы загружать двоичный файл как машинный код с диска, я использую go:embed для встраивания предварительно скомпилированного модуля WebAssembly в двоичный файл. Это означает, что получившаяся программа Go будет просто работать до тех пор, пока модуль WebAssembly присутствует в ожидаемом месте.

Увеличиваем скорость с Моар

Одним из основных недостатков этой реализации является то, что она немного медленная. Он должен компилировать модуль WebAssembly каждый раз, когда вызывается функция.

Среду выполнения wazero и скомпилированный код модуля WebAssembly можно перенести в переменную уровня пакета, как в этом патче. Главное преимущество, которое это дает, — скорость. После этого патча модуль WebAssembly компилируется только один раз при загрузке приложения. До этого патча каждый вызов занимал около 0,2 секунды. Вот результаты тестов после этого патча:

BenchmarkHTML2Slackdown             1221            938774 ns/op
BenchmarkHTML2Slackdown-2           2293            488032 ns/op
BenchmarkHTML2Slackdown-6           3555            305505 ns/op
BenchmarkHTML2Slackdown-12          3897            297974 ns/op

Оно сократилось с 0,2 секунды в лучшем случае до 0,3 миллисекунды в лучшем случае. Это по крайней мере 1000-кратное увеличение производительности, при этом большая часть времени, вероятно, тратится на синтаксический анализатор HTML, а не на что-либо еще.

Это более чем удовлетворит наши потребности как лично, так и на работе. Вы можете сами попробовать это еще немного со случайными сообщениями Mastodon, чтобы увидеть, делает ли это то, что мы хотим. Здорово иметь возможность объединить два несовместимых мира вместе, и не терпится увидеть, что мы можем сделать с этим в будущем.

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

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

В этом месте могла бы быть ваша реклама

Разместить рекламу