非同期 Rust で WASI を使って Web と通信したかった

# TL;DR

**できません**.

# 動機

ある日思い出しました. そういえば Docker って WASM 動かせたよな, と.

https://docs.docker.com/desktop/features/wasm/

そして偶々手元には作りたいアプリケーション[^1] が！
[^1]: ただの Discord Bot である.

よしそれを WASM/WASI で実行してやろうじゃあないか, と.

# Rust における target について

(恐らく)以前までは `wasm32-wasi` みたいな名前だった記憶がありますが, 執筆時現在では:

- `wasm32-wasip1`
- `wasm32-wasip2`

とおおまかには[^2] 分かれています.
[^2]: 他にも `wasm32-wasip1-threads` とかあるけどね？

これは WASI の仕様に Preview 1 と Preview 2 が存在するからですね. しかも単に機能追加があったとかではなく, 構造的にいろいろ変わっています. 次世代版なのは確かですが.

https://github.com/WebAssembly/WASI

ここでは一旦 `wasm32-wasip1` で動かすものと考えます. 何故なら Preview 2 のバイナリを実行できるランタイムは少ない[^3] からです.
[^3]: 執筆時現在では Wasmtime でしか動かない, はず.

# コンテナを構築して実行する

難しいことはありません. 但し, 規定では使えない機能を使うので, 環境設定を行う必要があります. [Wasm workloads (Beta)](https://docs.docker.com/desktop/features/wasm/) に載ってるのでその通りにやって下さい. ここでは説明は割愛します.

Cargo を使ってバイナリを吐き出すことを考えるので, まずお好きなように crate を準備します. その上で, 次のような手順を踏みます.

```makefile:Makefile
DOCKER_IMAGE ?= "example"

build-wasm:
	cargo build --release --target wasm32-wasip1

build-container: build-wasm
	docker buildx build --platform wasi/wasm --tag $(DOCKER_IMAGE) .

build: build-container

run-container: build
	docker run --rm --platform wasi/wasm --runtime io.containerd.wasmedge.v1 $(DOCKER_IMAGE)

run: run-container
```

```dockerfile:Dockerfile
# syntax=docker/dockerfile:1
# check=error=true
FROM scratch
COPY ./target/wasm32-wasip1/release/example.wasm /main.wasm
ENTRYPOINT [ "/main.wasm" ]
```

コンテナの対象プラットフォームは `wasi/wasm` となります. 実行時にはこれにコンテナランタイムを指定してあげて動かす形になります. 本当だったら Docker Compose とか使っても良いんですが, そこまで凝らなくても良いので簡易的に記述しています.

"Hello world" でも書いておけばきちんと出力されるはずです.

# 同期で通信をする

```rs:src/main.rs
fn main() {
    std::net::TcpStream::connect("1.1.1.1").unwrap();
}
```

```:/dev/stderr
thread 'main' panicked at src/main.rs:2:45:
called `Result::unwrap()` on an `Err` value: Error { kind: Unsupported, message: "operation not supported on this platform" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
[2024-11-16 06:33:29.736] [error] execution failed: unreachable, Code: 0x89
[2024-11-16 06:33:29.736] [error]     In instruction: unreachable (0x00) , Bytecode offset: 0x00003d42
```

そういえば Preview 1 ではサポートされてないんだった. ウケる. Preview 1 でもネット通信がしたい場合はこの記事を参照しよう！(ダイマ)

https://qiita.com/Nanai10a/items/0bbd60bf8215c006f3e4

という訳で次のように書き換えて `wasm32-wasip2` を使うようにする.

```diff:Makefile
 build-wasm:
-	cargo build --release --target wasm32-wasip1
+	cargo build --release --target wasm32-wasip2
```

```diff:Dockerfile
-COPY ./target/wasm32-wasip1/release/example.wasm /main.wasm
+COPY ./target/wasm32-wasip2/release/example.wasm /main.wasm
```

```:/dev/stderr
```

あれ, 何も言わずに `$? = 137` だけ吐いて死んじゃったぞ？

https://github.com/WasmEdge/WasmEdge/issues/2939

と思ったら執筆時現在では WasmEdge は Preview 1 までしか対応してなかったらしい. 残念. という訳で Wasmtime を使います[^4].
[^4]: ちなみにコンテナランタイムとして指定できるランタイムは[ここ](https://docs.docker.com/desktop/features/wasm/#turn-on-wasm-workloads)に列挙されているのですが, 特に何もせずに動いたのが Wasmtime だけだったのでこれを使うことにしています.

```diff:Makefile
 run-container: build
-	docker run --rm --platform wasi/wasm --runtime io.containerd.wasmedge.v1 $(DOCKER_IMAGE)
+	docker run --rm --platform wasi/wasm --runtime io.containerd.wasmtime.v1 $(DOCKER_IMAGE)
```

```:/dev/stderr
thread 'main' panicked at src/main.rs:2:45:
called `Result::unwrap()` on an `Err` value: Error { kind: InvalidInput, message: "invalid socket address" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

あっ間違えた (凡ミス)

```diff:src/main.rs
-    std::net::TcpStream::connect("1.1.1.1").unwrap();
+    std::net::TcpStream::connect("1.1.1.1:80").unwrap();
```

```:/dev/stderr
thread 'main' panicked at src/main.rs:2:48:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: PermissionDenied, message: "Permission denied" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

おっと？きっとこれはランタイムの機能でしょう. **という訳で Docker 上で動かすのは一旦諦めて, `wasmtime` を直接使うことにしましょう.** 頑張れば設定出来るでしょうが, 未来の自分に任せましょう.

という訳で実行手順を変更.

```makefile:Makefile
build-wasm:
	cargo build --release --target wasm32-wasip2

build: build-wasm

run-wasm: build
	wasmtime target/wasm32-wasip2/release/example.wasm

run: run-wasm
```

ここで,

```diff:Makefile
-	wasmtime target/wasm32-wasip2/release/example.wasm
+	wasmtime --wasi inherit-network=y target/wasm32-wasip2/release/example.wasm
```

```:/dev/stderr
```

やっと `$? = 0` になりましたね. 接続が出来ているようです. じゃあ HTTP 通信もしちゃいましょう.

```rs:src/main.rs
use std::io::{Read, Write};

fn main() {
    let mut stream = std::net::TcpStream::connect("1.1.1.1:80").unwrap();

    writeln!(&mut stream, "GET / HTTP/1.1").unwrap();
    writeln!(&mut stream, "HOST: 1.1.1.1").unwrap();
    writeln!(&mut stream, "User-Agent: unknown").unwrap();
    writeln!(&mut stream, "Accept: */*").unwrap();
    writeln!(&mut stream, "Connection: close").unwrap();
    writeln!(&mut stream, "").unwrap();

    stream.flush().unwrap();

    let mut buf = Vec::new();
    stream.read_to_end(&mut buf).unwrap();

    println!("{}", String::from_utf8_lossy(&buf));
}
```

```:/dev/stdout
HTTP/1.1 301 Moved Permanently
Server: cloudflare
Date: Sat, 16 Nov 2024 07:14:43 GMT
Content-Type: text/html
Content-Length: 167
Connection: close
Location: https://1.1.1.1/
CF-RAY: 8e35af906f4ad41b-KIX

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>cloudflare</center>
</body>
</html>

```

応答が帰ってきていますね. 問題無さそうです.

# 非同期で通信をする

さて本題はここからです. まず `tokio` を導入しましょう.

```toml:Cargo.toml
# --- snip ---
[dependencies.tokio]
version = "1.41.1"
features = ["sync", "macros", "io-util", "rt", "time"]
```

`tokio` は WASM 向けには使用出来る機能がかなり制限されています. 安定しているのが上に挙げたもの達ですね. まずはこれで非同期ランタイムが起動するか確認しましょう.

```diff:src/main.rs
-fn main() {
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
```

```:/dev/stderr
```

無事実行されていますね.

しかしこれでは通信部分が同期になってしまっています. という訳で不安定な機能に手を出しましょう.

```toml:.cargo/config.toml
[build]
rustflags = ["--cfg", "tokio_unstable"]
```

この状態で `net` 機能を有効化します.

```diff:Cargo.toml
-features = ["sync", "macros", "io-util", "rt", "time"]
+features = ["sync", "macros", "io-util", "rt", "time", "net"]
```

```:/dev/stderr
   Compiling tokio v1.41.1
error[E0658]: use of unstable library feature `wasip2`
   --> /Users/nanai/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.41.1/src/net/tcp/listener.rs:421:13
    |
421 |         use std::os::wasi::prelude::*;
    |             ^^^^^^^^^^^^^^^^^^^^^^
    |
    = help: add `#![feature(wasip2)]` to the crate attributes to enable
    = note: this compiler was built on 2024-11-14; consider upgrading it if it is out of date

error[E0658]: use of unstable library feature `wasip2`
    --> /Users/nanai/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.41.1/src/net/tcp/stream.rs:1411:9
     |
1411 |     use std::os::wasi::prelude::*;
     |         ^^^^^^^^^^^^^^^^^^^^^^
     |
     = help: add `#![feature(wasip2)]` to the crate attributes to enable
     = note: this compiler was built on 2024-11-14; consider upgrading it if it is out of date

For more information about this error, try `rustc --explain E0658`.
error: could not compile `tokio` (lib) due to 2 previous errors
```

後述しますが Tokio の `wasm32-wasip2` 向けサポートはまだ未完成の様です. ので, 一旦バイパスするようにパッチを当てましょう.

```
git clone https://github.com/tokio-rs/tokio
```

```diff:Cargo.toml
-version = "1.41.1"
+path = "tokio/tokio"
```

```diff:tokio/tokio/src/lib.rs
+#![feature(wasip2)]
 #![allow(unknown_lints, unexpected_cfgs)]
```

```:/dev/stderr
```

実行に成功しますね. では, 書き換えてみましょう.

```rs:src/main.rs
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let mut stream = tokio::net::TcpStream::connect("1.1.1.1:80").await.unwrap();

    stream.write_all(b"GET / HTTP/1.1\nHOST: 1.1.1.1\nUser-Agent: unknown\nAccept: */*\nConnection: close\n\n").await.unwrap();

    stream.flush().await.unwrap();

    let mut buf = Vec::new();
    stream.read_to_end(&mut buf).await.unwrap();

    println!("{}", String::from_utf8_lossy(&buf));
}
```

```:/dev/stderr
error[E0599]: no function or associated item named `connect` found for struct `tokio::net::TcpStream` in the current scope
   --> src/main.rs:5:45
    |
5   |     let mut stream = tokio::net::TcpStream::connect("1.1.1.1:80").await.unwrap();
    |                                             ^^^^^^^ function or associated item not found in `TcpStream`
    |
note: if you're trying to build a new `tokio::net::TcpStream`, consider using `tokio::net::TcpStream::from_std` which returns `Result<tokio::net::TcpStream, std::io::Error>`
   --> /private/tmp/ex/example/tokio/tokio/src/net/tcp/stream.rs:202:5
    |
202 |     pub fn from_std(stream: std::net::TcpStream) -> io::Result<TcpStream> {
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0599`.
error: could not compile `example` (bin "example") due to 1 previous error
```

ありゃ. 直接接続することは出来ない様です.

```diff:src/main.rs
-    let mut stream = tokio::net::TcpStream::connect("1.1.1.1:80").await.unwrap();
+    let stream = std::net::TcpStream::connect("1.1.1.1:80").unwrap();
+    let mut stream = tokio::net::TcpStream::from_std(stream).unwrap();
```

```:/dev/stderr
thread 'main' panicked at tokio/tokio/src/runtime/io/driver.rs:157:23:
unexpected error when polling the I/O driver: Os { code: 8, kind: Uncategorized, message: "Bad file descriptor" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error: failed to run main module `target/wasm32-wasip2/release/example.wasm`

Caused by:
    0: failed to invoke `run` function
    1: error while executing at wasm backtrace:
           0: 0x18a1a - example-da4e39d1571ed28c.wasm!__rust_start_panic
           1: 0x185f9 - example-da4e39d1571ed28c.wasm!rust_panic
           2: 0x185cc - example-da4e39d1571ed28c.wasm!std::panicking::rust_panic_with_hook::h31050d650e4c6e7f
           3: 0x17800 - example-da4e39d1571ed28c.wasm!std::panicking::begin_panic_handler::{{closure}}::h469d1b355ae53b73
           4: 0x17761 - example-da4e39d1571ed28c.wasm!std::sys::backtrace::__rust_end_short_backtrace::h08324fb8cc8e0e55
           5: 0x17f01 - example-da4e39d1571ed28c.wasm!rust_begin_unwind
           6: 0x211ec - example-da4e39d1571ed28c.wasm!core::panicking::panic_fmt::h31f2b8f84b02f226
           7: 0x10aa0 - example-da4e39d1571ed28c.wasm!tokio::runtime::io::driver::Driver::turn::hd21f216e585d8f7d
           8: 0x76fe - example-da4e39d1571ed28c.wasm!tokio::runtime::time::Driver::park_internal::h7de4bd81c96bc1e9
           9: 0xc1f1 - example-da4e39d1571ed28c.wasm!tokio::runtime::scheduler::current_thread::Context::park::hb8092a3585144521
          10: 0x3e0c - example-da4e39d1571ed28c.wasm!tokio::runtime::context::scoped::Scoped<T>::set::he9ad78af44797ea0
          11: 0x5d67 - example-da4e39d1571ed28c.wasm!tokio::runtime::scheduler::current_thread::CoreGuard::block_on::h51b67a5350c6d900
          12: 0x2d89 - example-da4e39d1571ed28c.wasm!tokio::runtime::context::runtime::enter_runtime::h980e03ad6178d511
          13: 0x358c - example-da4e39d1571ed28c.wasm!example::main::hb0c7a862b608cdd6
          14: 0x2571 - example-da4e39d1571ed28c.wasm!std::sys::backtrace::__rust_begin_short_backtrace::ha352f805040ead73
          15: 0x2564 - example-da4e39d1571ed28c.wasm!std::rt::lang_start::{{closure}}::hf3fc10f70a0f148c
          16: 0x1440e - example-da4e39d1571ed28c.wasm!std::rt::lang_start_internal::h95b0528984df5faa
          17: 0x38a1 - example-da4e39d1571ed28c.wasm!__main_void
          18: 0x253c - example-da4e39d1571ed28c.wasm!_start
          19: 0x37d41 - wit-component:adapter:wasi_snapshot_preview1!wasi:cli/run@0.2.0#run
    2: wasm trap: wasm `unreachable` instruction executed
```

*あーなんか無理そう. "Bad file descriptor" はどう考えても軽く修正しただけじゃ治らないやつだ…*

**という感じでこの記事はおしまいです.**

# 対応状況

よくよく探したら対応が進められているみたいですね. ただ…早くは無さそう.

https://github.com/tokio-rs/mio/pull/1836

https://github.com/tokio-rs/tokio/pull/6893

もっと言うと unstable feature の `wasip2` も…問題というか, 議論がある様です.

https://github.com/rust-lang/rust/issues/130323

この記事の内容とは外れますが, 通信する上では暗号化, 暗号化するには… `ring`, が Rust では標準的ですよね. これは対応が進んでいる様ですが…少し古いのかもしれません.

https://github.com/briansmith/ring/issues/1921

<details>

```:/dev/stderr
   Compiling ring v0.17.8
warning: ring@0.17.8: error: unable to create target: 'No available targets are compatible with triple "wasm32-unknown-wasip2"'
warning: ring@0.17.8: 1 error generated.
error: failed to run custom build command for `ring v0.17.8`

Caused by:
  process didn't exit successfully: `/private/tmp/ex/example/target/release/build/ring-9263422c07f84357/build-script-build` (exit status: 1)
  --- stdout
  cargo:rerun-if-env-changed=RING_PREGENERATE_ASM
  cargo:rustc-env=RING_CORE_PREFIX=ring_core_0_17_8_
  OPT_LEVEL = Some(3)
  OUT_DIR = Some(/private/tmp/ex/example/target/wasm32-wasip2/release/build/ring-6ad973c36bee25aa/out)
  TARGET = Some(wasm32-wasip2)
  HOST = Some(aarch64-apple-darwin)
  cargo:rerun-if-env-changed=CC_wasm32-wasip2
  CC_wasm32-wasip2 = None
  cargo:rerun-if-env-changed=CC_wasm32_wasip2
  CC_wasm32_wasip2 = None
  cargo:rerun-if-env-changed=TARGET_CC
  TARGET_CC = None
  cargo:rerun-if-env-changed=CC
  CC = None
  cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT
  RUSTC_WRAPPER = None
  cargo:rerun-if-env-changed=CRATE_CC_NO_DEFAULTS
  CRATE_CC_NO_DEFAULTS = None
  cargo:rerun-if-env-changed=WASI_SYSROOT
  WASI_SYSROOT = None
  DEBUG = Some(false)
  cargo:rerun-if-env-changed=CFLAGS_wasm32-wasip2
  CFLAGS_wasm32-wasip2 = None
  cargo:rerun-if-env-changed=CFLAGS_wasm32_wasip2
  CFLAGS_wasm32_wasip2 = None
  cargo:rerun-if-env-changed=TARGET_CFLAGS
  TARGET_CFLAGS = None
  cargo:rerun-if-env-changed=CFLAGS
  CFLAGS = None
  cargo:warning=error: unable to create target: 'No available targets are compatible with triple "wasm32-unknown-wasip2"'
  cargo:warning=1 error generated.

  --- stderr


  error occurred: Command "clang" "-O3" "-ffunction-sections" "-fdata-sections" "-fno-exceptions" "--target=wasm32-wasip2" "-I" "include" "-I" "/private/tmp/ex/example/target/wasm32-wasip2/release/build/ring-6ad973c36bee25aa/out" "-Wall" "-Wextra" "-fvisibility=hidden" "-std=c1x" "-Wall" "-Wbad-function-cast" "-Wcast-align" "-Wcast-qual" "-Wconversion" "-Wmissing-field-initializers" "-Wmissing-include-dirs" "-Wnested-externs" "-Wredundant-decls" "-Wshadow" "-Wsign-compare" "-Wsign-conversion" "-Wstrict-prototypes" "-Wundef" "-Wuninitialized" "-g3" "-nostdlibinc" "-DNDEBUG" "-DRING_CORE_NOSTDLIBINC=1" "-o" "/private/tmp/ex/example/target/wasm32-wasip2/release/build/ring-6ad973c36bee25aa/out/fad98b632b8ce3cc-curve25519.o" "-c" "crypto/curve25519/curve25519.c" with args clang did not execute successfully (status code exit status: 1).

  
```
</details>

# 最後に

WASM/WASI の道は険しく長い.