本セッションの登壇者
セッション動画
よろしくお願いします。今ご紹介いただきました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で与える例を考えます。
これをRustでどうやって書くかというと、こういうコードになります。要はCに対してRustの関数を出力するFFIの実装として書いていくことになります。
書いた後でコンパイルする際、ただ素直にビルドすると環境のバイナリの上にできてしまうので、Wasmにするためにはまずcrate_type
でCのダイナミックライブラリとしてビルドする設定を入れます。
そのあと、targetアーキテクチャにwasm32-unknow-unknown
というものを足します。そのターゲットに向かってコンパイルすると、Wasmが無事にできるというしくみになっています。
実際にできたかどうかはディレクトリを見ればわかるのですが、ファイルの内容はちゃんと目で確認できます。もともとはバイナリですが、Wasmにはテキスト表現もあるので、このwasm-toolsというツールを使うとWebAssemblyをテキスト表現に翻訳して表示してくれます。
たとえばこんな感じです。「なんか見たことがある」と馴染みがある人は思うでしょうし、見たことない人は「これはS式では?」みたいな気持ちになるかもしれません。たしかにS式っぽい記法でプログラムが書かれている感じです。
注目すべきはこのimport
とexport
で、import
は作られたこのWebAssemblyが動くにあたって外部から与えられる関数です。4つ前の画面のソースコードでextern
のかたわらにシグニチャだけ定義されているような関数があったと思うんですが、その関数の実態は外部から与えられます。その与えられる関数のシグニチャはこんな感じで書いてあります。export
は外の世界からWasmの中を呼び出せる関数で、import
には参照できるシンボルの名前がリストアップされています。先ほど作ったプログラムでは、rand
という関数が外からインポートされて、add
とadd-e
という2つの関数がWasmから外の世界にエクスポートされます。
では、このエクスポートされた関数を実際に自分の書いたRustのプログラムから呼び出してみます。呼び出すためにはランタイムを用意しなければならないのですが、ここではwasmtimeというものを使おうと思います。なぜこれを選んだかというと、ランタイムそれ自身がRustで開発されていて、crateとして提供されているので自分のプログラムに組み込みやすいからです。
こんな感じのコードを書くと、最後の3行でWasmの中に実装されている関数が呼び出されます。
コードの流れとしては、最初にWasmのモジュールをファイルからロードして、Parseして、Wasmとして形式が正しいかを確認した後にモジュールをメモリ上に別途作ります。
Wasmは中で仮想マシン言語で書かれた命令のセットになっているだけなので、その後にこれを実際に実行可能な状態に変換します。インポートされて外部から与えられると期待されていた関数の実装も、ここで与えることになります。
最後にオブジェクト、つまりインスタンスからエクスポートとされている関数を取得して、ストランドで関数オブジェクトにラップしてRustの中から呼べるようにして、最後に呼び出します。
実行までの流れはこのような絵になります。
WebAssemblyとRustは相性抜群
先ほどキーだったのは、実行時、インスタンス化する際にRustで実装を書いて渡しましたが、もちろんRustのネイティブコードである必要はなくて、インポートとする関数自身がWasmで実装されていてももちろん良いわけですね。期待している通りのインターフェースに従って書かれていればという制約が付きますが、問題はありません。
たとえば、さっきRustで書いて実装を与えたrand
という関数をWasmで作って与えてみるシナリオで考えてみます。
randは適当に作ってもいいですが、何か数値を返せばよいので、とりあえず1という定数を返す定数関数にします。こんな感じで書いてビルドします。そうすると別のWasmファイルがもう1つできます。
このWasmファイル2つを読み込んで、読み込んだrand
の方をまずインスタンス化して、wasmtimeにあるLinker
という構造体に与えると、そのLinker
が依存関係を把握してつないで出してくれます。
実際に乱数を生成する時にはプラットフォームの乱数生成器を使う方が楽なのですが、その場合はプラットフォームとじかに対話しなければならないので、WASIと呼ばれるインターフェースに従った実装がrandモジュールに与えられて、それを使って実行するという感じになります。
インターフェースはCの表現で規定
Wasmは今言ったように、関数をエクスポートしてそれを外部から呼んでもらって動くのですが、エクスポートするもの以外に別の関数をインポートして受け取る場合もあります。インポートされる関数の実体は、Wasmで与えても、ほかのもので与えてもかまいません。
WebAssembly自身はほかのプログラムとやり取りするためのものなので、間にインターフェースを決める必要があります。インターフェースを決める場合には、どういうシグネチャの関数があるのかといった関数のリストはもちろんですが、それ以外に、関数でどういうデータが作られてどういうデータ構造体が返ってくるかというデータ構造を決めなければなりません。多くのAPIドキュメントでもそうなっています。
ただ、データ構造を作る際に困るのは、Wasmには実は4つの型しかないことです。文字列やユーザー定義型などを表現するには、この4つの型をうまく使って表現しなければなりません。メモリ上にどう表現するかは実は何も決まっていないです。Wasmが1つのモジュールで閉じていればいいのですが、ほかのものとやり取りすることになったとき、じゃあその表現をどうするのかはわりと問題になります。たとえば、ほかのプログラムから呼び出された際、数値だけでなく複雑なデータ構造が与えられることはよくあるので、与えられたものをどうやってもとに戻すか、逆に結果を構造体として返すときには相手にちゃんとわかる形で返さないと結果の意味がなくなってしまうので、どういうふうに表現するかを決めなければなりません。
その場合は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)の一種です。
右にあるworld
というモジュールで、import
にはインポートする関数のシグネチャが書かれています。default export
の方には、そのモジュールから外にエクスポートされる関数のシグネチャが書かれています。s32
というのはRustで言うところのi32
、符号付き32ビット整数です。たとえばadd
という関数はleft
とright
という引数を取って、どちらも符号付き32ビット整数です。関数を呼ぶと符号付き32ビット整数が返ってくることがここからわかります。
こんな感じでインターフェース定義言語でインターフェースを書きます。あとはこんな感じでWasmのwit_bindgen_guest_rust
というモジュールのgenetate
というマクロに、このインターフェース定義ファイルを渡すと、自動的にコードを生成してくれます。あとはそれに従って、中でエクスポートする関数を定義するためのトレイトが定義されるので、それを実装します。最後に実装をexport
というマクロで呼んで、外の世界に出してやります。
もちろんトレイトなので実装されていないと「この関数が実装されてません」といったエラーがコンパイラで検出されます。そんな感じで書いていきます。
書いていくとWasmのモジュールができるので、これをひとまとめにしてWasmコンポーネントという形式のファイルを作ります。
コンポーネントを作るにはいろいろなアプローチがあるんですが、wasm-toolsを使ってビルドしてモジュールを作った後に、後処理としてコンポーネントを作る方がいいような気がしています。
makeファイルを書くとこんな感じです。wasm-toolsにはcomponent
というサブコマンドがあるので、これを使うとWasmのモジュールからWasmのコンポーネントファイルが作れます。compose
の後にcomponent
というサブコマンドを使って、複数作られたコンポーネントをまとめて1個のコンポーネントにします。
この際、依存関係のデータを外から与えなければならないので、こんな感じのYAMLファイルで依存関係を書いて与えると、この依存関係のデータを含んだ形で複数のWasmファイルから1個のWasmファイルにまとまったコンポーネントができるという仕組みになっています。
Rust使いには良い環境
まとめとしては、コンポーネントモデルが提供された理由は、複数のWasmファイルを組み合わせて使うのが面倒くさいという問題をうまく解決するための手段として提起されたというふうに僕は理解しています。実際にツールはRustで書かれたツールが大半なので、Rustを使う人にはとても良い環境です。ツールはbytecode allianceが主に作っているので、そこのGitHubリポジトリを見ているといろいろなツールがあります。(最後にwasmerと書いてあるのはwasmtimeの間違いですが)いろいろなツールがありますので、ぜひ楽しく試してみていただければと思います。
僕からは以上になります。ありがとうございました。