30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Deno Deploy で WebAssembly を動かす

Deno Deploy や Rust の練習として、Rust から出力した WebAssembly を Deno Deploy を動かしてみた。
その手順をまとめておく。

ローカルでの動作確認は以下の環境で行った。

  • rustc 1.50.0
  • cargo 1.50.0
  • Deno 1.9.2
  • deployctl 0.3.0

使用しているクレートのバージョンは以下。

  • brotli@3.3.0
  • js-sys@0.3.50
  • wasm-bindgen@0.2.73

Deno Deploy

Deno Deploy は、Edge Server で JavaScript や TypeScript、WebAssembly を動かせるサービス。
公式ドキュメントによればExtremely fastとのこと。

以下のHello World!スクリプトを見れば分かるように、fetchイベントを使ってリクエストを制御するという、Service Worker と同様の書き方ができる。

addEventListener("fetch", (event) => {
  const response = new Response("Hello World!", {
    headers: { "content-type": "text/plain" },
  });
  event.respondWith(response);
});

ローカルでの開発にはdeployctlという開発ツールを使う。
インストール方法や使い方は公式ドキュメントに分かりやすくまとまっている。

以下の内容のmod.jsを書き、$ deployctl run --watch mod.jsで動かしてみる。

const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Deno Deploy</title>
</head>
<body>
  <a href="/json">Show JSON</a>
</body>
</html>
`;

addEventListener("fetch", (event) => {
  const { pathname } = new URL(event.request.url);
  if (pathname === "/json") {
    return event.respondWith(
      new Response(JSON.stringify({ a: 1, b: 2 }), {
        headers: { "content-type": "application/json; charset=UTF-8" },
      })
    );
  }

  event.respondWith(
    new Response(html, {
      headers: { "content-type": "text/html" },
    })
  );
});

この状態でhttp://localhost:8080/にアクセスするとShow JSONが表示され、それをクリックすると JSON が表示される。

このようにdeployctlを使うことでローカルで開発できるのだが、後述するようにdeployctlで動いたからといって本番環境でも動くとは限らないので、注意する。

Rust から WebAssembly を出力する

次に、Rust でコードを書いてそれを WebAssembly で出力する。
Rust で書くことや WebAssembly を動かすことそれ自体が目的なので中身は何でもよいのだが、brotliによる圧縮を行うことにする。

?src=http://example.comのようにsrcクエリで URL を指定して、そのリソースをbrotliで圧縮してクライアントに返す。
実用性は一切考えていない。

以下の内容のsrc/lib.rsを書いた。

use brotli::CompressorReader;
use js_sys::Uint8Array;
use std::io::Read;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub extern "C" fn compress_by_brotli(text: &str) -> Uint8Array {
    let bytes = text.as_bytes();

    let mut compressor = CompressorReader::new(bytes, 4096, 6, 20);
    let mut compressed = Vec::new();
    compressor.read_to_end(&mut compressed).unwrap();

    js_sys::Uint8Array::from(&compressed[..])
}

これを WebAssembly に変換すると、JavaScript からcompress_by_brotliを呼び出せるようになる。
この関数に文字列を渡すと、brotliで圧縮されUint8Array形式で返ってくる。

変換にはwasm-packを使うので、インストールする。

そして$ wasm-pack build --target webを実行すると、/pkgディレクトリに WebAssembly が出力される。

WebAssembly を読み込む

続いて、出力した WebAssembly を Deno で読み込む。

以下のコードで動く。
ファイル名のcompressの部分はCargo.tomlで指定した[package]nameによって決まるので、適宜置き換える。

import init, { compress_by_brotli } from "./pkg/compress.js";

await init(Deno.readFile("./pkg/compress_bg.wasm"));

const text = "abc";
console.log(compress_by_brotli(text));
$ deno run --allow-read mod.js
Uint8Array(7) [
   7,  1, 128, 97,
  98, 99,   3
]

Uint8Arrayが返ってきている。
new Responseの第一引数にはUint8Arrayをそのまま渡せるので、HTTP レスポンスとして返すのも難しくない。

WebAssembly の読み込みと利用が成功したのであとは JavaScript を書いていくだけなのだが、ひとつだけ注意点がある。
実はDeno.readFileは Deno Deploy には存在しないので、使おうとするとエラーになる。
Edge Server なのだからreadFileがないのは当然のような気がするが、deployctlでは動いていしまうので見落としていた。

本番環境では GitHub に置いたファイルを読み込むようにして、解決した。

if (Deno.env.get("ENVIRONMENT") === "production") {
  const res = await fetch(
    "https://raw.githubusercontent.com/numb86/brotli-compression/main/pkg/compress_bg.wasm"
  );
  await init(await res.arrayBuffer());
} else {
  await init(Deno.readFile("./pkg/compress_bg.wasm"));
}

Deno Deploy では環境変数を設定できるので、それで処理を分けている。

コードの全文は以下に置いてある。

github.com

デプロイ

案内に従ってデプロイする。上述の環境変数の設定も行っておく。

最後に動作確認。
はてなブックマークの新着記事で試してみる。

まずオリジナルのリソース。

https://b.hatena.ne.jp/site/numb86-tech.hatenablog.com/?mode=rss

f:id:numb_86:20210428211540p:plain

f:id:numb_86:20210428211612p:plain

gzipでエンコーディングされており、ファイルサイズは21.8kB

次に、Deno Deploy 経由でリソースを取得する。

https://brotli-compression.deno.dev/?src=https://b.hatena.ne.jp/site/numb86-tech.hatenablog.com/?mode=rss

f:id:numb_86:20210428211638p:plain

f:id:numb_86:20210428211624p:plain

brotliでエンコーディングされており、ファイルサイズが14.1kBになっている。