本セッションの登壇者
セッション動画
Rust Tokyoの運営をしている@chikoskiと言います。WebAssemblyのコミュニティもやっていて、TechfeedにはWebAssemblyのエキスパートとして関わらせていただいています。
最近、いろいろなところでWebAssemblyの名前を耳にします。今日はRustでWebAssembly向けに開発する方法や、今のところのベストプラクティスをお話できればと思います。よろしくお願いします。
まとめを先に言いますと、今はcargo-componentというツールを使って開発をするのが一番楽です。普通のcargoと同じ感じでWasmのコンポーネントを作るためのプロジェクトを作ったり、それをビルドしたり、コンポーネントレポジトリというレポジトリにパブリッシュしたりできます。
また、他のプログラムから使われるライブラリなどを作る場合は、インターフェース定義言語でインターフェースを定義すると楽になります。コードジェネレーションも使えるのでぜひ使ってみてください。
ランタイムはいろいろあって甲乙つけがたいですが、個人的にはwasmtimeが一番いいと思っています。その理由の1つは、Wasmの今後のスタンダードになるであろうコンポーネントモデルに対応しているからです。
最後はwa.devというコンポーネントリポジトリがあるので、そこでパッケージを公開すると良いでしょうというお話です。
cargo-component を使用した開発とは
そもそもRustにおけるWebAssemblyの位置付けは、コンパイラターゲットなんですね。
Rustはクロスコンパイルが得意な言語です。つまり、自分がプログラムを書いているのと違う環境向けにビルドする、たとえばMacでプログラムを書いてWindows向けにビルドする、ArmのLinux向けにビルドするなどが得意で、それと同じような枠組みでクロスコンパイルのターゲットとしてWebAssemblyを扱っています。
今、対応可能なWebAssemblyのstableバージョンは5つあります。Webブラウザ用のwasm-32-unknown-unkownから始まっていて、ファイルシステムとやり取りするインターフェースを定義したwasm32-wasi は今はまだ使えますが、発展的に解消してwasm32-wasip1になる予定です。こうした標準の枠組みの中で使用されます。
cargo-componentを使って開発するには、インストールした後に、cargoと同様に new
コマンドでプロジェクトを作成して、main.rs
を実装します。
作成直後にビルドしてwasmtimeで実行した例です。cargo-componentを使ってビルドしているところ以外は、普通のバイナリークレートと同様に実装できます。
もちろん、Rustのクレートも使えます。これは clap::Parser
を使用してコマンドライン引数を処理する例です。基本的にほとんどのクレートは使えますが、unsafeなコードを多用しているものや、たとえば明確にwasm32をターゲットにしているビルドでビルドを失敗させるコードが書かれているものなどは使えません。async
のコードもビルドして実行することができます。
ライブラリもほぼ同じ
ライブラリ、つまり他のプログラムからロードされてその一部として動作するwasm componentを作る時も同じ感じです。
普通のライブラリクレートを作る時と唯一違うのは、WITというインターフェース定義言語を使って、ライブラリが実装する関数や関数の中で使うデータ構造を定義して、インターフェース定義を記述するステップが増えることです。面倒くさいと思うかもしれませんが、ライブラリの中で使うのに適したデータ構造や、関数を定義したトレイトを作成すると自動でコードを生成してくれるので、あとはそれを実装するだけで済むという非常に良い側面があります。
WebAsenblyを作る側としては、普通にクレートを作るような形で作成します。
インターフェース定義言語でインターフェースを書きます。Rustに似た文法なので、Rustを知っている人は違和感なく使えると思います。
気をつける点は、キーワードが異なることです。Rustでは「struct」と呼ぶものをWITでは「record」「レコード」と呼ぶ他、RustのenumはWITだと2種類に分かれています。Rustでは値を持つenumを作れますが、これをWITでは「variant」と呼び、値を持たないものを「enum」と呼びます。また、Result型が標準に用意されているところもRustっぽさがありますね。Option型もあるはずです。
このようにconfigというデータ構造を定義して、エラーを定義するenumを作って、typeのエイリアスを作り、返す関数のインターフェースを書きます。
コード生成を走らせると、インターフェース定義から構造体が定義されていることが分かります。このGuest
というトレイトで、先ほど定義した内容が定められているので、このトレイトを構造体に実装すると、作成したライブラリの機能を実装したWasmコンポーネントができます。
その Wasmコンポーネントを普通にビルドしてあげると良いという感じですね。
WITのコード生成で楽に安全に
作成したインターフェース定義に従って作られたWasmの関数を呼び出す側のコードはどうするかというと、そこもWITのコード生成の力で自動的に作ってくれます。
画面上の .let list = my_grep
の下の2行がコード生成されたコードです。この時点でWebAssemblyのプログラムをラップした関数が作成されているので、文字列型やデータを与えて呼び出すことができます。ラップされた状態だと普通のRustの関数と区別なく、データ型も等価的に扱えるメリットがあります。
先に処理系を初期化するとか、処理系サンドボックスの中でWasmのコードを起こすために共有ディレクトリを指定するなどが必要になりますが、それを除けば簡単に使えるようになります。
コード生成はとても強力で、例えばプラグインシステムが作りやすくなるんですね。プラグインを作成する際は、ある種のインターフェースを決めて実装していく必要がありますが、そのインターフェースをWITで書くことで、自動でコードが生成されて必要なものが出てくるし、必要なものが実装されているかどうかをRustの型システムがチェックしてくれるので非常に良いと思います。
使う側にとっても、インターフェースに従って実装されていることが保証されていますし、初期化やコードの不具合もコンパイルエラーで検知されるので安全です。
作成したらパッケージレポジトリーに登録
最後に、パッケージレポジトリーという、Rustのcrates.ioみたいなものがWebAssemblyの世界にもできつつあります。仕様はほぼ固まっていて、もう実装されてサイトで動いているサービスもあります。
wa.devにRustで書かれたCLIプログラムがあるので、そこに自分で作ったインターフェース定義を登録して、そのURLを渡せば、あとはそのインターフェースを参照する形でcargo-componentがプロジェクトを作るので、それに合わせて作っていくこともできます。
たとえば、先ほどのインターフェースはこんな感じで登録されています。
このインターフェース定義に従ったプログラムを作る時には、cargo componentの引数のtargetに実装するインターフェースを定義すると、そのインターフェースを実装するプロジェクトを作れます。
実装するとインターフェースに合わせてコードが生成されるので、あとは穴埋めするだけです。
作ったコンポーネントをコンポーネントレポジトリーに登録できるので、登録したコンポーネントを指定してプログラムを書くこともできます。
このように指定して作ると、自動的にコードが生成されて使えるようになるという感じです。
結果はこのようになります。
まとめです。まず、Rustでwasmをターゲットしたプログラムを作るならcargo-componentを使うと良いでしょう。作る時はWITでインターフェースを定義しておくと、コード生成の恩恵を非常に受けられるので良いと思います。
インターフェースを作ったら積極的にパッケージレポジトリーのwa.devに登録すると、後々のプロジェクトの進行が楽になっていくと思います。wa.devに登録したパッケージはプライベートに設定することもできるので、機密プロジェクトも可能です。
コード生成をうまく使って、より書き味がどんどん良くなってきているので、Wasmを始めるなら今だと思います。ありがとうございました。