TL;DR
できません.
動機
ある日思い出しました. そういえば Docker って WASM 動かせたよな, と.
そして偶々手元には作りたいアプリケーション1 が!
よしそれを WASM/WASI で実行してやろうじゃあないか, と.
Rust における target について
(恐らく)以前までは wasm32-wasi
みたいな名前だった記憶がありますが, 執筆時現在では:
wasm32-wasip1
wasm32-wasip2
とおおまかには2 分かれています.
これは WASI の仕様に Preview 1 と Preview 2 が存在するからですね. しかも単に機能追加があったとかではなく, 構造的にいろいろ変わっています. 次世代版なのは確かですが.
ここでは一旦 wasm32-wasip1
で動かすものと考えます. 何故なら Preview 2 のバイナリを実行できるランタイムは少ない3 からです.
コンテナを構築して実行する
難しいことはありません. 但し, 規定では使えない機能を使うので, 環境設定を行う必要があります. Wasm workloads (Beta) に載ってるのでその通りにやって下さい. ここでは説明は割愛します.
Cargo を使ってバイナリを吐き出すことを考えるので, まずお好きなように crate を準備します. その上で, 次のような手順を踏みます.
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
# 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" でも書いておけばきちんと出力されるはずです.
同期で通信をする
fn main() {
std::net::TcpStream::connect("1.1.1.1").unwrap();
}
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 でもネット通信がしたい場合はこの記事を参照しよう!(ダイマ)
という訳で次のように書き換えて wasm32-wasip2
を使うようにする.
build-wasm:
- cargo build --release --target wasm32-wasip1
+ cargo build --release --target wasm32-wasip2
-COPY ./target/wasm32-wasip1/release/example.wasm /main.wasm
+COPY ./target/wasm32-wasip2/release/example.wasm /main.wasm
あれ, 何も言わずに $? = 137
だけ吐いて死んじゃったぞ?
と思ったら執筆時現在では WasmEdge は Preview 1 までしか対応してなかったらしい. 残念. という訳で Wasmtime を使います4.
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)
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
あっ間違えた (凡ミス)
- std::net::TcpStream::connect("1.1.1.1").unwrap();
+ std::net::TcpStream::connect("1.1.1.1:80").unwrap();
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
を直接使うことにしましょう. 頑張れば設定出来るでしょうが, 未来の自分に任せましょう.
という訳で実行手順を変更.
build-wasm:
cargo build --release --target wasm32-wasip2
build: build-wasm
run-wasm: build
wasmtime target/wasm32-wasip2/release/example.wasm
run: run-wasm
ここで,
- wasmtime target/wasm32-wasip2/release/example.wasm
+ wasmtime --wasi inherit-network=y target/wasm32-wasip2/release/example.wasm
やっと $? = 0
になりましたね. 接続が出来ているようです. じゃあ HTTP 通信もしちゃいましょう.
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));
}
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
を導入しましょう.
# --- snip ---
[dependencies.tokio]
version = "1.41.1"
features = ["sync", "macros", "io-util", "rt", "time"]
tokio
は WASM 向けには使用出来る機能がかなり制限されています. 安定しているのが上に挙げたもの達ですね. まずはこれで非同期ランタイムが起動するか確認しましょう.
-fn main() {
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
無事実行されていますね.
しかしこれでは通信部分が同期になってしまっています. という訳で不安定な機能に手を出しましょう.
[build]
rustflags = ["--cfg", "tokio_unstable"]
この状態で net
機能を有効化します.
-features = ["sync", "macros", "io-util", "rt", "time"]
+features = ["sync", "macros", "io-util", "rt", "time", "net"]
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
-version = "1.41.1"
+path = "tokio/tokio"
+#![feature(wasip2)]
#![allow(unknown_lints, unexpected_cfgs)]
実行に成功しますね. では, 書き換えてみましょう.
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));
}
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
ありゃ. 直接接続することは出来ない様です.
- 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();
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" はどう考えても軽く修正しただけじゃ治らないやつだ…
という感じでこの記事はおしまいです.
対応状況
よくよく探したら対応が進められているみたいですね. ただ…早くは無さそう.
もっと言うと unstable feature の wasip2
も…問題というか, 議論がある様です.
この記事の内容とは外れますが, 通信する上では暗号化, 暗号化するには… ring
, が Rust では標準的ですよね. これは対応が進んでいる様ですが…少し古いのかもしれません.
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).
最後に
WASM/WASI の道は険しく長い.