なんとなく察しはついてるけど、いちおう確かめておこうかと。
詳細はこのリポジトリに。
Rustで書くには
ドキュメントなどあらゆる情報は、Cloudflare公式のこのリポジトリにある内容がすべて。
GitHub - cloudflare/workers-rs: Write Cloudflare Workers in 100% Rust via WebAssembly
Workerグローバルのコードがそれ用のcrateになってて、それを使ってRustでコードを書く。RequestやらKVやらだけでなく、いわゆるRouterやちょっとした便利関数まで実装されてた。
READMEにあるコード例をそのまま貼るとこんな雰囲気で。
use worker::*; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> { // Create an instance of the Router, which can use paramaters (/user/:name) or wildcard values // (/file/*pathname). Alternatively, use `Router::with_data(D)` and pass in arbitrary data for // routes to access and share using the `ctx.data()` method. let router = Router::new(); // useful for JSON APIs #[derive(Deserialize, Serialize)] struct Account { id: u64, // ... } router .get_async("/account/:id", |_req, ctx| async move { if let Some(id) = ctx.param("id") { let accounts = ctx.kv("ACCOUNTS")?; return match accounts.get(id).json::<Account>().await? { Some(account) => Response::from_json(&account), None => Response::error("Not found", 404), }; } Response::error("Bad Request", 400) }) // handle files and fields from multipart/form-data requests .post_async("/upload", |mut req, _ctx| async move { let form = req.form_data().await?; if let Some(entry) = form.get("file") { match entry { FormEntry::File(file) => { let bytes = file.bytes().await?; } FormEntry::Field(_) => return Response::error("Bad Request", 400), } // ... if let Some(permissions) = form.get("permissions") { // permissions == "a,b,c,d" } // or call `form.get_all("permissions")` if using multiple entries per field } Response::error("Bad Request", 400) }) // read/write binary data .post_async("/echo-bytes", |mut req, _ctx| async move { let data = req.bytes().await?; if data.len() < 1024 { return Response::error("Bad Request", 400); } Response::from_bytes(data) }) .run(req, env) .await }
だいたいイメージどおりだった。非同期なコードを書くけど、`tokio`とか`async-std`とかのそれではない。
実行時は`wasm32-unknown-unknown`でコンパイルするので、そこで動かないものは動かないとのこと。
気になる比較結果
- リクエストにとりあえずレスポンスするだけのコードを書いて
- JavaScriptとRust(WASM)でそれぞれデプロイして
- そのビルドされたサイズを見比べて
- 速度を`autocannon`で計測した
のが冒頭のリポジトリであるコレ。
結果としてはまぁ大方の予想通り。
- 速度: 大差ないけど、Rust(WASM)のほうが微妙に遅い
- サイズ: Rust(WASM)のほうが明らかにデカい
もっと遅くなるかと思ってたけど、意外にこんなもんで済むのか〜ってなったのは収穫だった。
サイズに関してはもうどうしようもなさそう・・・?このガイドに従ってもう少し頑張れるって書いてあった。
`opt-level`を`s`から`z`にしたら、16KBくらい減ったけど誤差っぽい。`gzip`するとか`brotli`するとかはやってない。
というわけで
もう少し踏み込んだ処理をする内容だったらば、パフォーマンスに差が出てきたりするんか・・・?って思いつつ、それはでも結局JSでやるべきか vs WASMでやるべきかの話でしかなく、WorkerのコードとしてRustを選ぶ理由にはならんかな・・?
そもそも、Rustで書いたプロジェクトもビルドするとこういう構成になる。
build ├── README.md ├── index.js ├── package.json └── worker ├── index_bg.mjs ├── index_bg.wasm └── shim.mjs
つまり実行環境としては処理の大部分がWASMに寄るってだけで、実質はJavaScriptのそれなのでは・・。
わざわざ全部Rustで書きたい強い気持ちがないなら、JavaScript(TypeScript)でいいし、WASMのモジュールが必要ならそのときに`import`すればいいはず。
あとRustで書くとき、ファイル開いてからCOC経由で`rust-analyzer`が仕事するようになるまで30秒くらい待つのが地味に不便で、在りし日のTSCの遅さを思い出した。