aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--blog/carcinization-golang.markdown232
-rw-r--r--data/toots/167beb2854e7772f70d2239d1860926991e54b0386c39cc2338dac54b754a0a5.json41
-rw-r--r--templates/contact.rs.html1
3 files changed, 273 insertions, 1 deletions
diff --git a/blog/carcinization-golang.markdown b/blog/carcinization-golang.markdown
new file mode 100644
index 0000000..7da607a
--- /dev/null
+++ b/blog/carcinization-golang.markdown
@@ -0,0 +1,232 @@
+---
+title: "The carcinization of Go programs"
+date: 2022-11-22
+author: Heartmender
+tags:
+ - cursed
+ - wasm
+ - go
+ - rust
+---
+
+<xeblog-hero ai="Waifu Diffusion v1.3 (float16)" file="crab-invasion" prompt="crabs, invasion, beach, palm trees, green hill zone, studio ghibli, xenoblade chronicles 2, pokemon, ken sugimori, thick outlines, ink"></xeblog-hero>
+
+Sometimes you just need to embed this one library written in another language
+into your program. This is a common thread amongst programmers time immemorial.
+This has always been a process fraught with peril, fear, torment, and
+lemon-scented moist towelettes for some reason.
+
+Normally if you want to call a Rust function from Go, you have to go through
+some middleman like [cgo](https://pkg.go.dev/cmd/cgo). This works and is
+somewhat elegant for how utterly terrible a hack cgo is.
+
+However, the main problem is that when you use cgo to link a Rust function into
+a Go program, you need to copy around the shared object that Rust generates. You
+can't check this shared object into your source tree (it needs to be unique per
+OS distribution per OS per CPU architecture, much like normal dynamically linked
+binaries are). It does work, but overall the developer experience is poor. Your
+build is no longer one simple `go build`. Now you have to remember to run `cargo
+build --release` and ensure that the resulting `.so`, `.dll`, or `.dylib` is in
+the right path for the OS' dynamic linker to read from. It's a mess.
+
+<xeblog-conv name="Mara" mood="hacker">This is such a big problem that at a
+generic level that this is why [Nix and NixOS](https://nixos.org) exist. Imagine
+how complicated this is when you get general-purpose OS components into the mix.
+It's astounding that anything works at all.</xeblog-conv>
+
+So what if I told you there was a way that we could ship _one_ binary from Rust,
+have that work on _every_ platform Go supports, and not have to modify the build
+process beyond a simple `go build`? Imagine how much easier that would be. It's
+easy to imagine that such a thing would let users _not even know that Rust was
+involved at all_, even if they consume a package or program that uses it.
+
+I've done this with a package I call
+[mastosan](https://github.com/Xe/x/tree/master/web/mastosan) and here's why it
+exists as well as how I made it.
+
+## Why
+
+Mastodon stores toots in HTML and presents that HTML to API consumers. HTML is
+very nice for a browser to display, but this is not as useful for a bot.
+Especially if your goal is to send toots to a Slack webhook.
+
+When you look at a toot such as this in the API:
+
+<xeblog-toot url="https://pony.social/@cadey/109388957068451434"></xeblog-toot>
+
+Its content looks something like this:
+
+```html
+<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>
+```
+
+Ideally we'd like it to look semantically identical in Slack, maybe something
+like this:
+
+```
+test mention <https://vt.social/@xe|@xe> so I can see what HTML mastodon makes
+```
+
+This will display the link in Slack like any other hyperlink. As things get more
+elaborate, Mastodon will do more semantic weirdness like invisible spans and
+other things that make displaying things on Slack annoying. Imagine the
+difference between these two things:
+
+```
+https:// tailscale.com/blog/introducing -tailscale-funnel/
+
+https://tailscale.com/blog/introducing-tailscale-funnel/
+```
+
+One is much more easy to understand for humans than the other.
+
+## How
+
+One of the core features of the UNIX philosophy is the idea that programs are
+simple filters that do _one thing well_ and then allow you to compose them into
+new and interesting ways. If you've ever used `curl` and `jq` together to do
+things like read data from a JSONFeed, you know how this is in practice:
+
+```console
+$ curl https://xeiaso.net/blog.json -qsSL | jq .items[0].title -r
+The birdsong persists
+```
+
+I made a little program in Rust that uses
+[lol_html](https://docs.rs/lol_html/latest/lol_html/) to take incoming
+Mastodon-flavored HTML and emit slack-flavored markdown. Usage is simple:
+
+```
+$ 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
+```
+
+That's it. It takes input on standard input and returns the result on standard
+output. This doesn't cleanly map to the WebAssembly flow, except if you use
+[WASI](https://wasi.dev/) to bridge the gap. WASI gives WebAssembly programs
+enough of a POSIX-like environment that most basic things can work, but here we
+are only really using two major parts of it: standard input and standard output.
+
+In Go, if you were running this as a normal OS subprocess, you'd probably write
+some code like this:
+
+```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
+}
+```
+
+However this still depends on the program being compiled for your native OS and
+distribution as well as present in a folder in your `$PATH`. This works, but
+this is not ideal in the slightest.
+
+Rust lets you build a binary that targets WASI with this compiler flag:
+
+```
+cargo build --target wasm32-wasi --release --bin mastosan
+```
+
+This will emit a several megabyte binary file in
+`./target/wasm32-wasi/release/mastosan.wasm`. When you run it, it will do what
+you want.
+
+Now you need to use it from Go. There's many choices for this, but I chose to
+use [wazero](https://wazero.io/). The overall flow of using this is similar to
+using a subprocess with `os/exec`, but slightly different because we're
+embedding WebAssembly. It will look like this:
+
+```go
+//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
+}
+```
+
+This is mostly the same thing. You set up the environment, load the WASM module
+and then run it. The main difference is that instead of loading the binary as
+machine code from the disk, I use
+[go:embed](https://pkg.go.dev/embed#hdr-Strings_and_Bytes) to embed the
+precompiled WebAssembly module into the binary. This means that the resulting Go
+program will Just Work as long as the WebAssembly module is present in the place
+it expects.
+
+## Moar faster
+
+One main drawback to this implementation is that it's a bit slow. It has to
+compile the WebAssembly module _every time_ the function is called.
+
+The wazero runtime and compiled WebAssembly module code can be lifted into a
+package-level variable, like with [this
+patch](https://github.com/Xe/x/commit/b61b59318be6544632ac1f64b1237bb17b2e7a32).
+The main advantage this gives you is _speed_. After this patch, the WebAssembly
+module is only ever compiled _once_, on application boot. Before this patch,
+each invocation took about 0.2 seconds per run. Here's the benchmark results
+after this patch:
+
+```
+BenchmarkHTML2Slackdown 1221 938774 ns/op
+BenchmarkHTML2Slackdown-2 2293 488032 ns/op
+BenchmarkHTML2Slackdown-6 3555 305505 ns/op
+BenchmarkHTML2Slackdown-12 3897 297974 ns/op
+```
+
+It's gone down from 0.2 seconds in the best case to _0.3 milliseconds_ in the
+best case. This is at least a 1000x increase in performance, with most of the
+time probably being spent in the HTML parser rather than being spent in anything
+else.
+
+I think this is going to more than meet my needs both personally and at work.
+I'm going to have to try this a bit more against random Mastodon messages to see
+if it does what I want. It's cool to be able to merge two incompatible worlds
+together and I'm excited to see what I can do in the future with this.
+
+<xeblog-conv name="Cadey" mood="coffee">Darn you shitposting coworkers nerd
+sniping the poor DevRel on their well-earned week off!</xeblog-conv>
diff --git a/data/toots/167beb2854e7772f70d2239d1860926991e54b0386c39cc2338dac54b754a0a5.json b/data/toots/167beb2854e7772f70d2239d1860926991e54b0386c39cc2338dac54b754a0a5.json
new file mode 100644
index 0000000..e832f04
--- /dev/null
+++ b/data/toots/167beb2854e7772f70d2239d1860926991e54b0386c39cc2338dac54b754a0a5.json
@@ -0,0 +1,41 @@
+{
+ "id": "https://pony.social/users/cadey/statuses/109388957068451434",
+ "type": "Note",
+ "inReplyTo": null,
+ "published": "2022-11-22T18:50:21Z",
+ "url": "https://pony.social/@cadey/109388957068451434",
+ "attributedTo": "https://pony.social/users/cadey",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://pony.social/users/cadey/followers",
+ "https://vt.social/users/xe"
+ ],
+ "sensitive": false,
+ "conversation": "tag:pony.social,2022-11-22:objectId=6283328:objectType=Conversation",
+ "summary": null,
+ "content": "<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>",
+ "contentMap": {
+ "en": "<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>"
+ },
+ "attachment": [],
+ "tag": [
+ {
+ "type": "Mention",
+ "href": "https://vt.social/users/xe",
+ "name": "@xe@vt.social"
+ }
+ ],
+ "replies": {
+ "id": "https://pony.social/users/cadey/statuses/109388957068451434/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://pony.social/users/cadey/statuses/109388957068451434/replies?only_other_accounts=true&page=true",
+ "partOf": "https://pony.social/users/cadey/statuses/109388957068451434/replies",
+ "items": []
+ }
+ },
+ "source": null
+} \ No newline at end of file
diff --git a/templates/contact.rs.html b/templates/contact.rs.html
index 05191f2..1538429 100644
--- a/templates/contact.rs.html
+++ b/templates/contact.rs.html
@@ -13,7 +13,6 @@
<h3>Social Media</h3>
<ul>
<li><a href="https://github.com/Xe">Github</a></li>
- <li><a href="https://twitter.com/theprincessxena">Twitter</a></li>
<li><a href="https://keybase.io/xena">Keybase</a></li>
<li><a href="https://www.patreon.com/cadey">Patreon</a></li>
<li><a href="https://twitch.tv/princessxen">Twitch</a></li>