WebAssembly入門 ─ Webフロントエンドの現実的なユースケースを知り、Wasm製アプリケーションを体験してみよう!

WebAssembly(Wasm)は、ブラウザー上で直接動くプログラミング言語として、JavaScriptを置き換える期待が寄せられますが、実際にWasmが果たすべき役割や適したユースケースとはどういったものでしょうか? Wasmの動作を体験するチュートリアルとあわせて、山本悠滋(igrep)さんが解説します。

WebAssembly入門 ─ Webフロントエンドの現実的なユースケースを知り、Wasm製アプリケーションを体験してみよう!

WebAssembly(以下、略称の「Wasm」と呼びます。「キャズム」や「~イズム」などからの類推なのか「ワズム」と発音するようです)とは、ブラウザーをはじめとするさまざまな環境で動作する、仮想マシンに対する命令セットです。さまざまなプログラミング言語からコンパイルできることを目指し、特定のハードウェアやプログラミング言語への依存を極力排除するほか、実行速度やファイルサイズ、セキュリティーを重視して設計されています。

WebAssembly

本稿では、そんなWasmの歴史から始めて、Webフロントエンドにおけるユースケースや、Wasm製のアプリケーションの使い方を学びます。今後、皆さんがWebフロントエンドでWasmをどう活用するかを考えるヒントになれば幸いです。

WebAssembly(Wasm)はどのように生まれたのか?

Wasmの直系の親にあたる言語に、2014年ごろ仕様が検討されたasm.jsがあります。asm.jsは、コンパイラーの生成対象(機械語やWasmのように、人間が書くよりコンピューターが処理しやすいように設計されたプログラミング言語)として利用できるくらいに機能を限定したJavaScriptです。

例えば「ビット論理和演算子|を適用した値は必ず32ビット整数の範囲に収まる」という仕様を応用して、「x|0といった式の型を32ビット整数と見なす」といった具合に、すでにあるJavaScriptの仕様の範囲内でどうにか機械語に近い最適化ができるよう定義されました。

こうした最適化は以前からブラウザー自身が行っていたこともあって広く受け入れられ、各種ブラウザーに加えて、後述するEmscriptenのようなコンパイルツールもasm.jsをサポートしました。

別の言語からJavaScriptを生成することの問題

しかし、本来は人間が書くはずのJavaScriptを強引にコンパイラーの生成対象とした結果、次のような問題が生じました。

  • ファイルサイズが大きくなりやすい
  • 構文解析(パース)に時間がかかる

一般に、コンパイラーの生成対象となる言語は、人間が書く言語よりも1つ1つの命令がプリミティブで小さい傾向にあります。そうした命令セットを、命令1つ当たりの容量が大きいJavaScriptで表現すると、ファイルサイズが大きくなってしまいます。

また、JavaScriptのような人間が手書きする言語は、コンパイラーの生成対象となる言語に比べて構文が複雑で、解析に時間がかかる傾向にあります。人間が手で書く程度の量であればあまり問題にならないのですが、ファイルサイズが大きくなりやすいという問題と重なったため、構文解析にかかる時間が無視できなくなってしまいました。

Wasmの誕生と現在までの簡単な歴史

Wasmは2015年に発表され、2017年に策定された最初のバージョンでは、まさにasm.jsを参考にしつつ、asm.jsの問題を解決するよう作られました。バイナリーフォーマットにすることでasm.jsよりサイズが小さく、構文解析が簡単になりました。

しかも安全性のため、JavaScriptの環境から分離させるという仕様も加えられました。Wasmには、いわゆる「組み込みで呼べる関数」というものがなく、JavaScript製のアプリケーションをはじめとする「ホスト」(Wasmのコードを実行するプログラム)が明示的に許可した関数しか呼び出すことができません。こうすることによって、悪意のあるWasmのコードを実行しても、Wasmのコードを動かしている環境への予期せぬ影響を抑えることができるのです。

2019年12月には、JavaScriptを除けばブラウザー上で直接動く唯一のプログラミング言語として、W3Cにより標準化されました。今や公認のWeb標準なのです。

2022年4月には、多値や参照型、SIMDといった新しい仕様を追加したWasm 2.0 Working Draftが公開されました。もっとも追加された仕様は、このドラフト以前から多くのブラウザーで実装されており、2.0によって直ちに機能が追加されたり、既存のWasm処理系が時代遅れになることはありません。あくまで最初のバージョン以降に合意された仕様をまとめただけのものと考えてください。

いずれにせよJavaScriptやCSSと同様に、Wasmはバージョン番号と関係なく、今後も多くの仕様が考案されることでしょう。GitHubのWebAssembly Organizationには、各種のproposalを議論する多くのリポジトリーが作られています。ぜひのぞいてみてください!

Wasmに期待されたものと現実のギャップ

そんな背景の下で生まれたWasmには、当初から「JavaScriptより高速に動作し、今後JavaScriptを置き換えるもの」という期待が寄せられました。特に「JavaScriptより高速に動作」という点については、盛んに宣伝されてきました。

そのためWasmについてTwitterで検索すると、時折「これからフロントエンドエンジニアは(Wasmへの変換元として現状で好んで用いられる)Rustも学ぶべきだ」といった、JavaScriptの代わりにWasmが使われるようになることを前提とした言説が見られます。

しかしながら、現状は言うまでもなく今後のWasmの発展をもってしても、JavaScriptを過去のものにするようなことはないでしょう。このセクションでは、そうした期待にそぐわない事実について説明します。

そもそも「JavaScriptを置き換える」ものではない

まず、Wasmの公式サイトのFAQにそのものズバリな質問があります。冒頭を引用してみましょう。

Is WebAssembly trying to replace JavaScript?

No! WebAssembly is designed to be a complement to, not replacement of, JavaScript. While WebAssembly will, over time, allow many languages to be compiled to the Web, JavaScript has an incredible amount of momentum and will remain the single, privileged (as described above) dynamic language of the Web. (以下略)

以下は筆者による翻訳です。

WebAssemblyはJavaScriptを置き換えようとしているのですか?

そんなことはありません! WebAssemblyはJavaScriptを置き換えるものではなく、補完するものとして設計されています。WebAssemblyは今後、多くの言語をWeb向けにコンパイルできるようになる見込みですが、一方のJavaScriptには信じられないほど強い勢いがあり、今後もWebにおけるただ1つの、(上述のような)特権を得た動的言語であり続けることでしょう。

このようにそもそもの設計方針として、WasmはJavaScriptを補完することをゴールとしている点にご注意ください。

後でも触れますが「JavaScript以外のプログラミング言語をブラウザー上で動かせるようにする」のは、あくまでも他のプログラミング言語による資産を再利用しやすくするためであって、始めからJavaScript以外の言語でWebアプリケーションを作るためではないのです。

期待したほどパフォーマンスが良くなかった実例

JavaScriptは、各ブラウザーベンダーによる速度競争が長年行われた結果、いわゆる「スクリプト言語」とは思えないほど高速に実行されるようになりました1。それは、うまく最適化されればWasmの実行速度に比肩し、「Wasmで書けば何でも速くなる」なんて楽観をことごとく打ち砕くほどのものです。

実際にJavaScriptとWasmで同じ処理を書いてみたけど、それほど速くできなかった例を1つ挙げましょう。

Zaplib post-mortem - Zaplib docs

Zaplibは、JavaScript製のアプリケーションを少しずつRust(からコンパイルしたWasm)で書き換えることを支援するライブラリーです。このドキュメントは、試しに数人のユーザーに自身のアプリケーションを移植してもらった結果の分析(post-mortem)で、実際にRustで書き換えた方が速かったアプリケーションは想定よりはるかに少なく、しかも期待していた10倍の高速化には遠く及ばなかったそうです。

Zaplibはこの結果をもって、開発を終了すると発表しました。

なお、このWasm対JavaScriptに限らず「○○言語は××言語より速い」なんて意見に流され、安易に別の言語で書き直すのは考えものです。ボトルネックをよく見極め、そもそもアプリケーションが原因なのか、アプリケーションだとしてアルゴリズムを改善できないかなど、その前に考えるべきことはたくさんあります。

移植したWasmアプリケーションのパフォーマンスはなぜ上がらないのか

なぜ、Zaplibのような結果になってしまったのでしょう? 先ほども触れたように、JavaScriptが十分に高速化されていることがまずあります。それに加えて、Wasm・JavaScript間における関数呼び出しコストの高さも影響していると考えられます。

JavaScriptからWasmの関数を呼んだり、逆にWasmからJavaScriptの関数を呼ぶコストは、JavaScriptがJavaScriptの関数を呼んだり、Wasmが(同じWasmモジュールに定義した)Wasmの関数を呼ぶのに比べて、どうしてもオーバーヘッドがかることになります。さらに、ブラウザーで動くアプリケーションを作る際に使用するAPIは、全てJavaScript向けの関数です。そのためWasmからそうしたAPIを呼ぶと、やはりJavaScriptから呼ぶよりもオーバーヘッドが大きくなってしまうのです。

「そうしたAPI」には、私たちがブラウザー向けのアプリケーションを作る際に、息を吸うように使っている関数、例えばDOMオブジェクトを操作したり、サーバーにHTTPリクエストを送ったりするものが、全て含まれています。皆さんが作るたいていのWebアプリケーションは、こうした処理が実行時間の大半を占めるのではないでしょうか?

そうです。そのためZaplibが推進したような、普通のJavaScript製アプリケーションをWasmに移植することは、少なくともパフォーマンスの面に関しては悪手なのです。

ファイルサイズの問題もまだ残っている

実行速度以前に、ファイルサイズの問題もまだあります。asm.jsが抱えていた「ファイルサイズが大きくなりやすい」という問題を解決するために生まれたWasmですが、残念ながら一般的なJavaScript製のアプリケーションほど小さくすることはできませんでした。

この多くは、他のプログラミング言語からWasmを作ったときに同梱される「ランタイムライブラリー」が占めています。例えば、現在人気のプログラミング言語はたいていガーベージコレクター(GC)を備えていますが、これもサイズが大きくなりやすい機能の1つです。私たちがメモリーの確保・解放を意識しないでプログラムを書けているのも、大きなランタイムライブラリーのおかげなのです2

たとえGCがなく、ランタイムライブラリーが比較的小さいCやC++、Rustであっても、(例えば内部で参照するマスターテーブルがあるなどの事情で)少し大きなライブラリーに依存すると、出来上がるWasmのサイズはあっという間に膨れ上がります。

対するJavaScriptは、ガーベージコレクターも含めて、必要なライブラリーの大半をブラウザーが組み込みで提供してくれます。ファイルサイズについては、その分でWasmに大きく水をあけているのです。

付記・実行速度と最適化に関するより複雑な観点

ただし、Wasm対JavaScriptのパフォーマンス合戦には、単純なベンチマークでは見えない、より複雑な観点があることにも触れておきましょう。藤吾郎@__gfx__さんが、とても興味深い実験をしてくれました。

AssemblyScriptでライブラリコードの高速化をしてみる - Speaker Deck

この資料の第二部(スライド31枚目以降)では、「ウェブページの初回読み込み時から操作可能になるまでの間は、最適化がまだ効いてないかもしれない」という仮説のもと、V83の最適化を有効にした場合と無効にした場合を比較しています。その結果として「最適化が効かないときは、かなり小さいデータサイズでもWasm版の方が速い」ことを発見しました。

「JavaScriptも最適化によって速くなる」ことは一面の真実であるものの、それが期待通りに機能するかどうかは思いのほか予測しづらく、不透明なことが知られています。ここまで説明したとおり、期待されていたほど実行速度でJavaScriptに差を付けられていないWasmですが、最適化の有無にかかわらず安定した速度が出せることは、メリットなのかもしれません。

現実的なWasmのユースケースを考える

ここまでで、WasmはJavaScriptよりも高速に実行できると期待されたものの、実際のところそれほどではないし、今後も既存のアプリケーションを直ちに書き換えるほどでもないことを説明しました。

それでも、Wasmは誕生以降、それなりの数のWebアプリケーションで採用されてきました。ここでは、そうしたWasmの具体的なユースケースについてまとめてみましょう。

既存のアプリケーションを移植する

WebのフロントエンドにおけるWasmの最もポピュラーな利用目的は、JavaScript以外で書かれた既存のソフトウェア(アプリケーションやライブラリー)の活用でしょう。実例をいくつか挙げてみます。

まず、既存のアプリケーションをブラウザー上で実行する例です。リンク先に解説があります。

このうちGoogle Earthでは、以前はNative ClientというChromeでしか使えない技術で、C++のコードを利用していました。これをクロスブラウザーで動くよう、Wasmで作り直しました。

既存のライブラリーを再利用する

次に、既存のライブラリーの再利用です。

まずGoogle Meetの背景ぼかし機能では、「MediaPipe」というC++で書かれた機械学習フレームワークを活用しています。

Background Features in Google Meet, Powered by Web ML - Google AI Blog

また、t-kihiraさんが開発した「Block Pong」というゲームでは、各ブラウザーだけでなくiOSやAndroidのネイティブアプリでも浮動小数点数の演算における一貫性を確保するため、FDLIBMというライブラリーをWasmで採用しました。

Web ベースのカジュアルゲーム「block pong」の実装とビジネス - slideshare

上記のアプリケーションやライブラリーはいずれも既存の(JavaScript以外で書かれた)コードが先にあり、それをブラウザーに移植した例です。仮にこれをJavaScriptで書き直したり、asm.jsにコンパイルしたりしても、おそらくWasmの方がパフォーマンスは高いでしょう(そもそも膨大な工数ゆえ誰もやりたくないでしょうが)

こうした例を見るに、Wasmの本来の目的は達成できているのではないでしょうか。同じ目的だったasm.jsより優れている時点で、Wasmを設計した人たちの意図通りなのです。ただその「速い」という部分が強調されるあまり、ありふれたJavaScriptによるアプリケーションの開発者達の期待が高まり過ぎてしまったのです。

応用・ブラウザー外でWasmを利用する

もともとブラウザー上で動かすものとして設計されたWasmですが、マシンアーキテクチャーやプログラミング言語からの中立性や、ホストが実行を許可する関数を制御できるという安全性から、ブラウザー外での利用も数多く試みられています。

フロントエンドの話からそれてしまうので詳細は割愛しますが、Shopifyがサーバーサイドの拡張機構としての採用を検討[動画]したり、Zellijというターミナルマルチプレクサーがプラグインシステムに採用したりしています。サードパーティーのコードを安全に実行できる、いわば「手軽な任意コード実行」を提供する仕組みとしてうってつけなのです。

WasmそのものをWATで体験してみよう

ここからは実際にWasmを使って、学んでみましょう。まずこのセクションで、Wasmそのものがどういった言語なのかを軽く紹介します。続いて次のセクションでは、既存のアプリケーションをビルドし、ビルドしたものを使ってみることで、Wasmでできることを「体験」してもらいたいと思います。

ただし、Wasm製のアプリケーションを開発することにはあまり踏み込みません。ここまで解説したように、Wasmは一義的にはコンパイラーの生成対象なのです。つまり人間が直接読み書きする言語ではなく、究極にはWasmのことなんて一切意識しなくても、好きな言語を使える世界であるべきなのです。

なので、RustなどでWasm製のアプリケーションを書くことは、フロントエンドエンジニアに必須の仕事ではないと私は考えています。興味のある方は、おなじみMDNの簡単なチュートリアルなどを調べてみるのがよいでしょう。

Wasm「ほぼそのもの」を軽く学習する

ここではWAT(WebAssembly Text format)という言語を書いてみることで、Wasmで書かれたプログラムの中身を見てみましょう。WATは、もともとバイナリーであるWasmファイルを、デバッグなどで読みやすいように、テキストファイルとして表現した形式です。機械語に対するアセンブリーのWasm版、と言えばピンとくる方も多いのではないでしょうか。

WABT(WebAssembly Binary Toolkit)に含まれるwat2wasmは、そんなWATをWasmに変換できるコマンドです(アセンブリーに対するアセンブラーに相当しますね)。上記GitHubリポジトリーのReleasesページからWABTをダウンロードすれば利用できますが、ここでは簡単に下記のデモページで試してみましょう。

wat2wasm demo

このリンクを開くと、次のようなウェブページが表示されるはずです。

wat2wasm demoを開いたときの画面

簡単なWATのコードを入力してみる

画面上部左に「WAT」という見出しがついたテキストエリアがあります。ここにWATのコードを入力すると、直ちにwat2wasmが実行され、変換されたWasmが右側のペインに注釈付きの16進数で表示されます4

wat2wasmのデモページを開くと最初から簡単なWATのコードが書かれていると思いますが、今回はこのコードはさておいて、代わりに以下のコードをコピーアンドペーストしてください。

(module
  (import "jsImports" "consoleLog" (func  $jsFunc (param i32)))
  (func (export "addTwo") (param i32 i32)
    (call $jsFunc
      (i32.add
        (local.get 0)
        (local.get 1)))))

このWATで表されるWasmモジュールは、addTwoという名前の関数をエクスポートします。addTwoは引数として整数を2つ受け取って、インポートした$jsFuncという関数を呼びます。

詳しい解説は後にして、とりあえず動かしてみましょう。そのためにはJavaScriptでWasmモジュールを初期化し、エクスポートした関数を呼び出す必要があります。

WATのコードを呼び出すJavaScriptを入力する

wasm2watのデモページにおける「WAT」欄の下に、「JS」という見出しのテキストエリアがあります。ここにはWasmモジュールで定義した関数を呼び出すコードを書けます。やはりJavaScriptのコードが入力済みかと思いますが、いったん全て削除して次のコードに書き換えてください。

const imports = {
  jsImports: {
    consoleLog: console.log
  }
};
const wasmInstance =
      new WebAssembly.Instance(wasmModule, imports);
const { addTwo } = wasmInstance.exports;
addTwo(17, 25);

すると「JS」のテキストエリアの右にある「JS LOG」というエリアに、addTwo関数を実行した結果、ここでは42が出力されているでしょう。これで無事に、WATで書いた関数を実行することができました!

それでは、入力したコードを解説していきましょう。まず上記のJavaScriptです。最初のconst imports = {で始まる文は、Wasmモジュールにインポートさせる関数の設定です。jsImportsというモジュールを定義して、その中のconsoleLog関数としてconsole.logを設定しています。

次のconst wasmInstance =で始まる文では、Wasmモジュールの実行可能なインスタンスを生成します。Wasmモジュールはコンパイルしただけでは実行できる状態にはならず、インスタンス化という手順を経る必要があります。インスタンス化の際は、Wasmモジュールがインポートしている関数が、ホストにおけるどの関数を差しているのか指定しなければなりません。

ですのでこの文では、インスタンス化されたWasmモジュールを作るWebAssembly.Instanceのコンストラクターに、コンパイルしたWasmモジュール(wasmModule(wat2wasmのデモページが自動的に定義してくれる変数)と、前の文で定義したインポートの設定(imports)を渡します。

続く2つの文では、wasmInstanceexportsというプロパティーを通じて、定義したaddTwo関数を取得し、引数として1725を渡して実行します。以上が、Wasmを実行するJavaScriptコードの解説でした。

WATのコードの説明

続いてWATのコードを説明します。前述のWATを再掲します。

(module
  (import "jsImports" "consoleLog" (func  $jsFunc (param i32)))
  (func (export "addTwo") (param i32 i32)
    (call $jsFunc
      (i32.add
        (local.get 0)
        (local.get 1)))))

改めて眺めてみると、妙にカッコ()が多いですね。WATの構文はLispでおなじみのS式をベースとしており5(定義の種類 ...定義の中身...)という、カッコを使って定義の種類と範囲を示す構文になっています。

例えば、一番最初に出てくる(module ...)という宣言はWasmモジュール全体の定義、それから(import ...)はインポートする関数の定義、(func ...)はWasmモジュール自身における関数の定義を表します。

1つ1つの定義を改めて詳説しましょう。まず(import ...)は、Wasmモジュール内で使う、ホストの関数についての定義です。先ほどJavaScriptを書いた際、Wasmモジュールにインポートさせるモジュールと、その関数を具体的に設定しました。この、Wasmモジュール側で定義する(import ...)では、ホスト側に提供して欲しい関数の名前や、引数と戻り値の型を宣言します。

上記の例における(import "jsImports" "consoleLog" (func (param i32)))では、次の情報を指定しています。

  1. インポートするモジュールの名前: jsImports
  2. そのモジュールに含まれる関数の名前: consoleLog
  3. WATファイル内で参照する際の名前: $jsFunc
  4. 関数の引数の型: (param i32)
    • 戻り値の型は(result ...)で宣言するが、この関数は戻り値を返さないので省略

まとめると、次のような意味になります。

  • jsImportsというモジュールにあるconsoleLogという名前の関数を、「整数を1つ受け取って結果は何も返さない関数」としてインポートする
  • このWATファイルで参照する際は$jsFuncという名前を使う

続・WATのコードの説明

続いて、(func ...)についてです。funcという名前に加え、(export "addTwo")や、(param i32 i32)といった単語が続くことから、勘のいい方はこれがaddTwo関数の定義に該当することにお気付きでしょう。ご明察💡です。

(export "addTwo")が、「この関数はaddTwoという名前でホスト(JavaScript側)に向けてエクスポートする」という宣言で、(param i32 i32)が、「この関数は引数として整数(i32)を2つ受け取るという意味になります。

(func ...)における残りの部分、(call ...)が、このaddTwo関数を呼び出すと実際に実行される命令です。二重に入れ子になっていて分かりづらいでしょうし、抜粋してコメントを追加します。

(call $jsFunc     ;; 関数 $jsFunc の呼び出し
  (i32.add        ;; 2つの整数を足し算する
    (local.get 0) ;; 1番目の引数の取得
    (local.get 1) ;; 2番目の引数の取得
  )
)

WATでは、このように関数の本体で実行する命令もS式で記述できます。(命令の名前 ...命令の引数)という構文になっています。実際に実行する順番と逆に書かれているので分かづらいですが、要約すると次の流れが読み取れるでしょうか?

  1. local.getで指定した番号(0始まり)の引数を2つ取得し
  2. それらをi32.addで足し合わせ
  3. callでインポートした関数$jsFuncに渡して実行する

Wasmの関数では引数もローカル変数の一部と見なされるので、引数の取得もlocal.getを使います。

WasmでできることをPyodideで体験する

最後に、既存のWasm製アプリケーションをビルドして動かしてみる過程で、Wasmを使ってできることや、大まかな手順を体験してみましょう。題材として使うアプリケーションはPyodideです。これはCPython6をEmscriptenでWasm向けにコンパイルしたものです。Emscriptenは現状で、C言語やC++で書かれたアプリケーションをWasmにコンパイルするデファクトスタンダードなツールとして知られています。

Pyodideを利用するにはドキュメント曰く、通常CDNから直接参照するか、GitHub Releasesにあるビルド済みのファイルをダウンロードすればいいみたいですが、ここでは敢えてソースコードからビルドして、その過程から既存のアプリケーションをWasmにビルドする方法に触れてみたいと思います。

とはいえ、あらかじめ必要なものがインストールされたDockerイメージを使うので、やることはそれほど多くありません。あらかじめDockerをインストールしておいてください。それぞれのコマンドで何をしているのか解説するので、参考になれば幸いです。

使用した環境・システム要件

執筆時に使用した環境は以下の通りですが、最近のバージョンのDockerが動くLinuxなら問題ないとは思います。

  • OS: Debian GNU/Linux 11.3
  • Docker Engine: 20.10.16

なお、Pyodideのリポジトリーやビルドしてできるファイル、Dockerイメージの容量が合わせて3.2GBかかります。それから、ドキュメントいわくビルド中にコンテナーに割り当てるメモリーは最低3GB程度あると望ましいとされています。こちらも念のためご注意を!

Pyodideをビルドする

まずは、Pyodideのリポジトリーgit cloneしましょう。

$ git clone -b 0.20.0 --depth=1 https://github.com/pyodide/pyodide.git

ビルドするバージョンを指定するために、-bオプションでタグを指定します7--depth=1と指定すれば取得するコミットを1つに絞れるので、今回のように大きなリポジトリーを少しのぞき見したい、というときにおすすめです。

git cloneが終わったら、cloneしたディレクトリーにcdし、そこにある./run_dockerというコマンドを実行してください。

$ cd pyodide
$ ./run_docker

./run_dockerを実行すると、ビルドに必要なツールが全て含まれたDockerイメージを取得し、コンテナ-を起動してからコンテナーの中でbashを起動します。しばらくお待ちください。bashが起動するまで進むとプロンプトが次のように変わるでしょう。

$ ./run_docker
# ... 省略 ...
<ユーザー名>@ab5cb704ac29:/src$

<ユーザー名>には、OSにおけるあなたのユーザー名が表示されます。

ここまで来たらmakeコマンドを実行してください。

<ユーザー名>@ab5cb704ac29:/src$ make

makeコマンドは、特にC言語などのビルドで長年利用されているツールです(実際にはC言語に限らず汎用的に使えるビルドツールです!)。この手のツールとしては元祖とも言うべき存在であり、Linuxを含め多くのUnixに標準でインストールされています。

makeコマンドは、makeを実行したディレクトリーの、Makefileというファイルに書かれた設定に従ってコマンドを実行します。PyodideのMakefileでは、間接的にEmscriptenに含まれているCコンパイラーのemccを呼ぶなどして、CPythonのソースコードをWasmにコンパイルします。例えば、makeを実行すると途中に出てくる下記のメッセージは、emccmain.cをコンパイルしていることを示します。

emcc -o src/core/main.o -c src/core/main.c -O2 -g0 -fPIC  -Wall -Wno-warn-absolute-paths -Werror=unused-variable -Werror=sometimes-uninitialized -Werror=int-conversion -Werror=incompatible-pointer-types -Werror=unused-result -I/src/cpython/installs/python-3.10.2/include/python3.10 -s EXCEPTION_CATCHING_ALLOWED=['we only want to allow exception handling in side modules'] -Isrc/core/

こちらのメッセージは、Emscriptenに含まれるem++を利用して、emccでコンパイルした.oファイルをリンクさせているようです8

em++ -o build/pyodide.asm.js src/core/docstring.o src/core/error_handling.o src/core/error_handling_cpp.o src/core/hiwire.o src/core/js2python.o src/core/jsproxy.o src/core/main.o src/core/pyproxy.o src/core/python2js_buffer.o src/core/python2js.o \
  -O2 -g0 # ... 残りは省略 ...

makeの実行もやはり時間がかかります。手元の環境では./run_dockerの実行開始から合わせて30分以上かかりました。

<ユーザー名>@ab5cb704ac29:/src$ make
# ... 省略 ...
SUCCESS!

上の通り、成功したら最後にSUCCESS!と表示されます。そこまで確認できたらexitコマンドを実行し、コンテナーごとbashを終了させましょう。

<ユーザー名>@ab5cb704ac29:/src$ exit

ビルドしてできたものを確認

ようやくできましたね! .wasmファイルを始めとする、以上の手順で生成されたもろもろのファイルは、build/ディレクトリーにあります。

> ls build/
Jinja2-3.1.1-py3-none-any.whl                        packaging-21.3-py3-none-any.whl   pytz-2022.1-py2.py3-none-any.whl
MarkupSafe-2.1.1-cp310-cp310-emscripten_wasm32.whl   pyodide.asm.data                  regex-2022.3.15-cp310-cp310-emscripten_wasm32.whl
console.html                                         pyodide.asm.js                    regex-tests.tar
cpp-exceptions-test-0.1.zip                          pyodide.asm.wasm                  sharedlib-test-1.0.zip
distutils.tar                                        pyodide.d.ts                      sharedlib_test_py-1.0-cp310-cp310-emscripten_wasm32.whl
fpcast_test-0.1.1-cp310-cp310-emscripten_wasm32.whl  pyodide.js                        ssl-1.0.0.zip
micropip-0.1-py3-none-any.whl                        pyodide.js.map                    test.html
module_test.html                                     pyodide.mjs                       test.tar
module_webworker_dev.js                              pyodide.mjs.map                   webworker.js
openssl-1.1.1n.zip                                   pyodide_py.tar                    webworker_dev.js
packages.json                                        pyparsing-3.0.7-py3-none-any.whl

pyodide.asm.wasmというファイルがPythonの処理系本体です。その他のファイルはPythonで書かれたライブラリーや、pyodide.asm.wasmを呼ぶためのJavaScriptで書かれたラッパーなどです。

残念ながら現状のWasmの仕様上、JavaScriptのラッパーなしではとても扱いづらいので、ラッパーもこのように自動的に生成されます。このあたりの事情は、私がZennに書いた「WebAssembly Reference Typesで、WasmでDOMを操作する壁がここまで下がった」という記事などを参照してください。

Pyodideを実行してみよう

おおむね上記のようなファイルができていることが確認できたら、いよいよビルドしたPyodideを実行してみましょう! Python標準のhttp.serverモジュールなど、静的ファイルを配信できる適当なウェブサーバーを使って、build/ディレクトリーを配信してください。

> python3 -m http.server --directory build/
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

※筆者の手元の環境ではpythonコマンドはPython 2.7を指していたのでpython3コマンドを使っています。

上記の実行例ではメッセージの通り8000番のポートを使っているので、おなじみhttp://localhost:8000にアクセスできます。そこでPythonのREPLが起動できるconsole.htmlを開いてみましょう。

http://localhost:8000/console.html

うまく行けば、ブラウザーに次のようなページが表示されているはずです。

Welcome to the Pyodide terminal emulator

やりました!🎉🐍

後は好きなPythonのコードを実行してお楽しみください。次は、PythonによるFizz Buzzの実行例です。

PythonによるFizz Buzz

ブラウザー上でPythonが使える! とはいえ

最後に、「よし! これならブラウザー上でPythonが使えるぞ! もうJavaScriptからはおさらばだ!」と思われた方へお知らせです9。当記事の前半で述べたとおり、Wasmは他言語で書かれた資産を再利用するための補助的な技術であって、JavaScriptを完全に過去のものにする技術ではありません。

その要因として、他のプログラミング言語をWasmで動かす際必要となる「ランタイムライブラリー」などのサイズが大きい、という点を挙げました。Pyodideの場合もやはりご多分に漏れず、DevToolsで確認するとWasmファイルの転送量はなんと9.05MBもありました!

pyodide.asm.wasm 9.05MB

もちろん本番環境で利用する場合はBrotliなどで圧縮させると思いますが、それでも大きすぎるでしょう。軽くDOMオブジェクトを操作する程度のアプリケーションでは割に合いません。すでにあるPython向けのライブラリーを手っ取り早くブラウザー上でも使いたい、というケースには適切かと思いますが。

ここまでで、Emscriptenを用いて既存のC言語製のアプリケーションをWasmにビルドするまでの流れを簡単に体験しました。今回はDockerイメージやMakefileが用意されたスクリプト(./run_docker)を通じて利用するだけの、いわば「お膳立て」された状態でのビルドでした。

皆さんが今後、まだWasmに移植できていないC・C++製ライブラリーやアプリケーションをビルドする場合も、使用するツール(Emscripten、WASI SDKなど)や実行方法は、大筋で変わらないはずです(だいたいClangを含む従来のビルドツールをフォークしたりラップしたりしたものでしょうから)

まとめ

Wasmはいろいろなプログラミング言語からコンパイラーが生成するという性格上、利用する方法はプログラミング言語ごとに異なります。そのためここで紹介したことが、皆さんがWasmを触るときにも当てはまるかどうかは分かりません。それでも本記事の情報が、Wasmについて知る上で何らかの一助になれば幸いです。

参考文献

本文中で言及していないドキュメントを挙げておきます。

山本 悠滋(やまもと・ゆうじ) twitter: @igrep / GitHub: igrep

igrep
Haskell-jp(日本Haskellユーザーグループ)発起人の一人にして、Haskell-jpで一番のおしゃべり。HaskellとWebAssemblyとプリキュアとポムポムプリンをこよなく愛する。株式会社インターネットイニシアティブ (IIJ)リサーチエンジニア。
the.igreque.info / 山本 悠滋 | IIJ Engineers Blog / 書きたいときに、書けばいい。

編集:はてな編集部


  1. そうした工夫の一環に興味のある方は、PPLサマースクール2021「JavaScript処理系とChromeブラウザの実装技術」の発表資料などがおすすめです。リンク先における「講演資料」をクリックすれば、各発表のスライドが読めます。

  2. それほど重要なGCだからこそ、Wasm自身にGCを加えようという提案もされていて、絶賛策定中です。

  3. ChromeやNode.jsで使われ、私たちにとって最も身近であろうJavaScript・Wasm処理系

  4. 詳細は申し訳なくも割愛します! 気になる方はWasmのバイナリーフォーマットの仕様のこの辺りを起点に照合させてみてください。

  5. あくまで構文としてS式を採用しているというだけで、普通のLispなら備えているであろうコンスセルやマクロなどの機能は一切ありません。おそらく構文にそれほどこだわりがないので、パースが比較的簡単なS式を選んだのでしょう。

  6. Guido van Rossumさんが最初に開発した、オリジナルのPython処理系。C言語で書かれているので他の処理系と区別するために「CPython」と呼ばれる。

  7. 後で気付いたのですが、執筆時点の最新版は0.20.0ではなく0.20.0aで、0.20.0aから出力先のディレクトリーがbuild/からdist/に変わったようです。今後のバージョンをビルドする際はご注意ください。

  8. 実際にリンクを行うのはem++が間接的に呼び出すリンカーというプログラムです。em++emccは、自身でコンパイル済みの.oファイルを与えると、コンパイルする代わりにリンクさせてくれます。この辺りの流れは、Wasmファイルを作る場合でもネイティブの実行ファイルを作る場合でも変わりません。詳細が気になった方はぜひEmscriptenやその他のCコンパイラーを調べてみてください。

  9. 諸般の事情で今回はあえて取り上げませんでしたが、PyodideをベースにDOMの操作を行うAPIまでPython向けに提供するPyScriptというプロジェクトもあります。「私は転送量なんか気にしない!!」という方はお試しください。

若手ハイキャリアのスカウト転職