TechFeed Experts Night Pick up

Rust使いは要注目! WebAssemblyのコンポーネントモデルを知る ~TechFeed Experts Night#9講演より

本記事は、2022年11月に開催された「TechFeed Experts Night#9 〜 Rust/WebAssemblyの「いま」を探る」のセッション書き起こし記事「Rust使いは要注目! WebAssemblyのコンポーネントモデルを知る by chikoski@」を転載したものです。オリジナルはTechFeedをご覧ください。

よろしくお願いします。今ご紹介いただきましたchikoski@です。

Rustにはコミュニティ的な関わり方が多くて、Rust.Tokyoというカンファレンスの運営をやっています。WebAssemblyは出たときからずっと仕様を追っていて、コロナの始まる前はWebイベントもやっていたのですが、最近はできていません。Techfeedの公認エキスパートもさせていただいています。

WebAssemblyの気に入っているところは、Semanticsを含めた仕様が決まっていてフォーマル セマンティクスがあるところです。テキストフォーマットがあって、Webらしくバイナリではなくテキストでも読めるというのもいいなと思っています。

自己紹介

今日は、WebAssemblyで最近コンポーネントモデルというものが定義されつつあるのでその話をします。ディスカッションはもう1年以上前から始まっていて、どこかでコンポーネントモデルの話を1年前ぐらいにしたときは海のものとも山のものともわからないものだったのですが、現在はだいぶ出来上がってきたのでご紹介しようと思っています。どうしてこういうのが必要とされてきたのか、背景の部分をRustのツールを使いながらご紹介できればと思っています。よろしくお願いします。

仕様に従ったどんなコンテンツもローカルで安全に動く世界

WebAssemblyの使われ方にはいろいろあって、最初はゲームやソフトウェアを丸ごとWebブラウザの上で動かすみたいな使われ方が主だったと思うんですが、時が経つにつれて変わってきました。WebAssemblyがどういうものかというと、WebとAssemblyという名前が付いているのでよく冷やかしで「Webでもアセンブリでもないじゃん」みたいに言われたりもするのですが、けっこうWebっぽいところもあると僕は思っています。

きちんと仕様が定められたオープンスタンダードのフォーマットがあって、そのフォーマットに従って作られた、誰が作ったかもわからない、出所も知らないコンテンツが自分のローカルな環境にやってきて動くという、そういった仕組みがWebAssemblyが目指している世界なんですが、その世界で大事なのは、どこの誰でも作れる、どんなツールを使っても作れるけど、それが安全に動かなければいけません。

その特徴をうまく使ってブラウザの上でプログラムを動かす使い方もあるんですけど、それよりもプラグインやエクステンション、ECサイトのビジネスロジックを書くためのアプリケーションの実装、エッジコンピューティングのエッジの処理などをより柔軟に開発者に書いてもらうための手段としてWasmを使うことが増えています。Wasmにすると、Cで書こうが、Rust、Go、Rubyで書こうがWebAssemblyはWebAssemblyなので、書かれたプログラムをアプリならアプリ、エッジならエッジの処理の仕様に従って書いたものをアップロードすればそれが動きます。そのようにして柔軟に好きなツールを使って他人の書いたプログラムが安全に動く仕組みを作るためのものとして使われています。

Wasmにすると柔軟に好きなツールを使って他人の書いたプログラムが安全に動く

たとえばこういう、足し算の処理をWasmで与える例を考えます。

足し算の処理をWasmで与える例

これをRustでどうやって書くかというと、こういうコードになります。要はCに対してRustの関数を出力するFFIの実装として書いていくことになります。

足し算の処理をWasmで与える例:Rustコード

書いた後でコンパイルする際、ただ素直にビルドすると環境のバイナリの上にできてしまうので、Wasmにするためにはまずcrate_typeでCのダイナミックライブラリとしてビルドする設定を入れます。

crate_typeでCのダイナミックライブラリとしてビルドする設定を入れる

そのあと、targetアーキテクチャにwasm32-unknow-unknownというものを足します。そのターゲットに向かってコンパイルすると、Wasmが無事にできるというしくみになっています。

ビルド

実際にできたかどうかはディレクトリを見ればわかるのですが、ファイルの内容はちゃんと目で確認できます。もともとはバイナリですが、Wasmにはテキスト表現もあるので、このwasm-toolsというツールを使うとWebAssemblyをテキスト表現に翻訳して表示してくれます。

wasm-tools

たとえばこんな感じです。⁠なんか見たことがある」と馴染みがある人は思うでしょうし、見たことない人は「これはS式では?」みたいな気持ちになるかもしれません。たしかにS式っぽい記法でプログラムが書かれている感じです。

wasm-toolsによる表示

注目すべきはこのimportとexportで、importは作られたこのWebAssemblyが動くにあたって外部から与えられる関数です。4つ前の画面のソースコードでexternのかたわらにシグニチャだけ定義されているような関数があったと思うんですが、その関数の実態は外部から与えられます。その与えられる関数のシグニチャはこんな感じで書いてあります。exportは外の世界からWasmの中を呼び出せる関数で、importには参照できるシンボルの名前がリストアップされています。先ほど作ったプログラムでは、randという関数が外からインポートされて、addとadd-eという2つの関数がWasmから外の世界にエクスポートされます。

importとexport

では、このエクスポートされた関数を実際に自分の書いたRustのプログラムから呼び出してみます。呼び出すためにはランタイムを用意しなければならないのですが、ここではwasmtimeというものを使おうと思います。なぜこれを選んだかというと、ランタイムそれ自身がRustで開発されていて、crateとして提供されているので自分のプログラムに組み込みやすいからです。

wasmtime

こんな感じのコードを書くと、最後の3行でWasmの中に実装されている関数が呼び出されます。

Wasmの中に実装されている関数

コードの流れとしては、最初にWasmのモジュールをファイルからロードして、Parseして、Wasmとして形式が正しいかを確認した後にモジュールをメモリ上に別途作ります。

Wasmのモジュールをファイルからロード

Wasmは中で仮想マシン言語で書かれた命令のセットになっているだけなので、その後にこれを実際に実行可能な状態に変換します。インポートされて外部から与えられると期待されていた関数の実装も、ここで与えることになります。

実行可能な状態に変換

最後にオブジェクト、つまりインスタンスからエクスポートとされている関数を取得して、ストランドで関数オブジェクトにラップしてRustの中から呼べるようにして、最後に呼び出します。

関数オブジェクトにラップ

実行までの流れはこのような絵になります。

実行までの流れ

WebAssemblyとRustは相性抜群

先ほどキーだったのは、実行時、インスタンス化する際にRustで実装を書いて渡しましたが、もちろんRustのネイティブコードである必要はなくて、インポートとする関数自身がWasmで実装されていてももちろん良いわけですね。期待している通りのインターフェースに従って書かれていればという制約が付きますが、問題はありません。

インポートとする関数自身がWasmで実装されても良い

たとえば、さっきRustで書いて実装を与えたrandという関数をWasmで作って与えてみるシナリオで考えてみます。

例:randという関数をWasmで作って与えてみる

randは適当に作ってもいいですが、何か数値を返せばよいので、とりあえず1という定数を返す定数関数にします。こんな感じで書いてビルドします。そうすると別のWasmファイルがもう1つできます。

例:rand

このWasmファイル2つを読み込んで、読み込んだrandの方をまずインスタンス化して、wasmtimeにあるLinkerという構造体に与えると、そのLinkerが依存関係を把握してつないで出してくれます。

ずインスタンス化してLinker構造体に与える

実際に乱数を生成する時にはプラットフォームの乱数生成器を使う方が楽なのですが、その場合はプラットフォームとじかに対話しなければならないので、WASIと呼ばれるインターフェースに従った実装がrandモジュールに与えられて、それを使って実行するという感じになります。

WASI

インターフェースはCの表現で規定

Wasmは今言ったように、関数をエクスポートしてそれを外部から呼んでもらって動くのですが、エクスポートするもの以外に別の関数をインポートして受け取る場合もあります。インポートされる関数の実体は、Wasmで与えても、ほかのもので与えてもかまいません。

WebAssembly自身はほかのプログラムとやり取りするためのものなので、間にインターフェースを決める必要があります。インターフェースを決める場合には、どういうシグネチャの関数があるのかといった関数のリストはもちろんですが、それ以外に、関数でどういうデータが作られてどういうデータ構造体が返ってくるかというデータ構造を決めなければなりません。多くのAPIドキュメントでもそうなっています。

ただ、データ構造を作る際に困るのは、Wasmには実は4つの型しかないことです。文字列やユーザー定義型などを表現するには、この4つの型をうまく使って表現しなければなりません。メモリ上にどう表現するかは実は何も決まっていないです。Wasmが1つのモジュールで閉じていればいいのですが、ほかのものとやり取りすることになったとき、じゃあその表現をどうするのかはわりと問題になります。たとえば、ほかのプログラムから呼び出された際、数値だけでなく複雑なデータ構造が与えられることはよくあるので、与えられたものをどうやってもとに戻すか、逆に結果を構造体として返すときには相手にちゃんとわかる形で返さないと結果の意味がなくなってしまうので、どういうふうに表現するかを決めなければなりません。

Wasmとのインタフェースを決めるには

その場合はCの表現として返すということになっています。こんな感じのコードを書いていくのですが、問題はこれは全部グルーコードで、ビジネスロジックなどプログラムの本質とは何の関係もないことです。ほかから呼ばれるために界面をくっつけるだけのコードなので、書いてて楽しいものではないですね。

全部グルーコード

これは呼び出し側も一緒で、さっきWasmのモジュールを読んでLinkerに入れて依存関係を解決してもらうというコードがありましたが、それも全部グルーコードなので書いてて楽しくない。

呼び出し側もグルーコード

実際に僕も書いてみて思ったんですけど、グルーコードを書くのは面倒くさいです。あとはインターフェースで、Rustというのは型検査がしっかりしていて、コンパイラのチェックを通れば動くというのが強みのはずなのに、インターフェースの定義がちゃんとRustフレンドリーな形はなく、人間が読める文章でしかなかったりすると、正しくインターフェースに従っているのかどうかとても不安になります。また、Wasmを個別に読むのが面倒だったり、データ表現がちゃんと期待に沿っているかどうかなどけっこう不安になったりします。

実際に書いてみて

まとめると、Wasmを複数組み合わせて使うシナリオを考えたときに面倒くさい点は、ほかのWasmのコードを読むためのグルーコードを書くのが面倒くさいし、データ表現を揃えなければいけないし、そもそも読み込む側からするとWasmを個別に読むのはとっても面倒くさいですね。たとえばランタイムが複数のファイルになっていてZIPで固めて送るみたいなことがあると、ZIPを全部ひも解いて、名前や依存関係を苦労して解決して読むみたいなことを自分で書かなければならなくなってとても面倒くさいので、そこの部分はちゃんと標準として用意して、環境ごとに勝手な仕様を決めるのではなく標準化して、デベロッパの開発者体験をぐっと上げよう、みたいなモチベーションがあるんじゃないかなと思っています。

コンポーネントモデルでより扱いやすく

そういう問題点に対して解決策を提供したのが、コンポーネントモデルだというのが僕の理解です。コンポーネントモデルがどのようにこういった問題を解決しているかというと、グルーコードを書くのが面倒だという問題はInterface Definition Language(IDL)でインターフェースを定義してもらって、そのインターフェースの定義からグルーコードを自動生成するというアプローチになっています。ABIも定義されています。データ表現も標準化されています。Wasmを個別に読むのは面倒だという問題は、別のファイルフォーマットを決めて、そこに複数のWasmモジュールを入れられるようになっています。依存関係も書けるようになっているので、そのファイルを読めば依存関係も解決できるし、必要なコンポーネントモジュールが全部入っています。シングルバイナリを作るみたいなノリでWasmのモジュールファイルをたくさん持つWasmのファイルが作れるというアプローチで問題を解決しようとするのがコンポーネントモデルであるというのが僕の理解です。

まずはこの中で、プログラムを書いたり、モジュールやコンポーネントを作る側からすると一番目に触れる部分 - 一番上のグルーコードを書くのが面倒だという問題が解決するのが良いと思うので、そこの話からしますね。

コンポーネントモデルのアプローチ

Wasmではコードジェネレーションというか、外部からWasmの関数を使ったり使われたりするための仕組みとしてFFIというものがあって、それを使うときは同じようなグルーコードをたくさん書くのが面倒くさいという問題が起きます。そのため、bindingとかbindgenとかその世界では呼ぶのですが、バインディングを作るコード生成器がいくつか用意されています。WebAssemblyの世界でよく名前を聞くのは、この最初のwasm-bindgenです。これはJavaScript向けのバインディングを生成するためのツールで、これを使うとJavaScriptからWasmの関数を適切に呼べたり、JavaScriptのオブジェクトをWasmから使えるようになります。

バインディングを作るコード生成器

今回はそちらではなくて、下のwit-bindgenという新しい方の話をしようと思います。このwitというのはさっき言ったインターフェース定義言語(IDL)の一種です。

wit-bindgen

右にあるworldというモジュールで、importにはインポートする関数のシグネチャが書かれています。default exportの方には、そのモジュールから外にエクスポートされる関数のシグネチャが書かれています。s32というのはRustで言うところのi32、符号付き32ビット整数です。たとえばaddという関数はleftとrightという引数を取って、どちらも符号付き32ビット整数です。関数を呼ぶと符号付き32ビット整数が返ってくることがここからわかります。

wit-bindgenのインターフェース定義

こんな感じでインターフェース定義言語でインターフェースを書きます。あとはこんな感じでWasmのwit_bindgen_guest_rustというモジュールのgenetateというマクロに、このインターフェース定義ファイルを渡すと、自動的にコードを生成してくれます。あとはそれに従って、中でエクスポートする関数を定義するためのトレイトが定義されるので、それを実装します。最後に実装をexportというマクロで呼んで、外の世界に出してやります。

wit-bindgenが自動的にコードを生成

もちろんトレイトなので実装されていないと「この関数が実装されてません」といったエラーがコンパイラで検出されます。そんな感じで書いていきます。

コンパイルエラー

書いていくとWasmのモジュールができるので、これをひとまとめにしてWasmコンポーネントという形式のファイルを作ります。

Wasmコンポーネント

コンポーネントを作るにはいろいろなアプローチがあるんですが、wasm-toolsを使ってビルドしてモジュールを作った後に、後処理としてコンポーネントを作る方がいいような気がしています。

Wasmコンポーネントを作るには

makeファイルを書くとこんな感じです。wasm-toolsにはcomponentというサブコマンドがあるので、これを使うとWasmのモジュールからWasmのコンポーネントファイルが作れます。composeの後にcomponentというサブコマンドを使って、複数作られたコンポーネントをまとめて1個のコンポーネントにします。

makeファイル

この際、依存関係のデータを外から与えなければならないので、こんな感じのYAMLファイルで依存関係を書いて与えると、この依存関係のデータを含んだ形で複数のWasmファイルから1個のWasmファイルにまとまったコンポーネントができるという仕組みになっています。

依存関係のデータ

Rust使いには良い環境

まとめとしては、コンポーネントモデルが提供された理由は、複数のWasmファイルを組み合わせて使うのが面倒くさいという問題をうまく解決するための手段として提起されたというふうに僕は理解しています。実際にツールはRustで書かれたツールが大半なので、Rustを使う人にはとても良い環境です。ツールはbytecode allianceが主に作っているので、そこのGitHubリポジトリを見ているといろいろなツールがあります。(最後にwasmerと書いてあるのはwasmtimeの間違いですが)いろいろなツールがありますので、ぜひ楽しく試してみていただければと思います。

まとめ

僕からは以上になります。ありがとうございました。

おすすめ記事

記事・ニュース一覧