mrubyをWebAssemblyで動かす(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Tom Black — Ruby on WebAssembly 原文公開日: 2018/04/28 著者: Tom Black 日本語タイトルは内容に即したものにしました。 mrubyをWebAssemblyで動かす(翻訳) 要約: WebAssemblyがやってきました!やってみたい方は本記事のwasm gemをお試しください。 2018年の世界へようこそ。最も興奮を呼び起こすイノベーションがコンパイラ界隈にも巻き起こりつつあります。ARやVRや機械学習など先端を行く話題はいくつもありますが、ソースコードを機械が実際に理解できる形に変換する役割を担うコンパイラの歴史に比べればまだまだ新顔です。コンパイラの復権は主にLLVMのおかげです(LLVMはモジュール形式のコンパイラインフラストラクチャプロジェクトです)。LLVMの重要性については、Wiredの2013年の記事に的確な指摘がまとまっています。 その希望とは、LLVMがソフトウェア開発の新時代の水先案内人となり、アプリケーションがあるマシンから別のマシンに、そしてあるプロセッサから別のプロセッサにすら自由に移動できるようになることだ。 以来5年が経過し、これはかなりの部分まで実現しました。LLVMの導入とそれがもたらすモジュラーパラダイムによって、ソフトウェア開発が根底から変革されました。LLVMによって、移植性/最適化/言語実装/コンパイルライフサイクル全体がラディカルに考え直されたのです。詳しく知りたい方はoverview of LLVMをご覧ください。著者のChris LattnerはLLVMの主要な作者であり、しかも Swiftの創立者でもあるのです。Chrisの昨年のインタビューをAccidental Tech Podcastで聞くことができます。 LLVMがWebといったい何の関係があるのでしょうか?Webのプログラミングといえば、その草創期(1995年)からJavaScriptと相場が決まっていました。JavaScriptはその後23年間批判と論争の嵐をくぐり抜け、今もWebプログラミング言語として居座っています。JavaScriptを置き換える強力なライバルが不在であったにもかかわらず、言語としてよい感じに成熟と進化を繰り返してWebに関わり続けています。Webブラウザも、静的コンテンツを表示するだけの単なるビューアから、数十億ものユーザーやシステムの基盤を追い求める企業の要求に後押しされたフル装備のアプリケーションプラットフォームへと着実に進化しています。しかしながら、ブラウザが「正しい」アプリケーションプラットフォームになるにはまだひとつ足りないものがあります。Wired誌の同じ記事から再度引用します。 JavaScriptの難点は、言語としては比較的シンプルであることだ。他の言語(Javaや、誉れ高きCやC++など)ならできることでもJavaScriptでできるとは限らない。私たちに必要なのは、アプリケーションをどんな言語で書いてもあらゆるマシンで動かせる方法だ。それも大きな妥協を強いられずに。そしてLLVMはそれを目指している。少なくともそう思っている人々がいる。 ブラウザがデスクトップOSやモバイルOSに匹敵するには、ブラウザに欠かせない例のあの言語からブラウザ自身が解放されなければなりません。つまりブラウザのプログラミング環境は、他のプラットフォームと同様もっと低レベルになるべきです。ブラウザは独自のアセンブリ言語を備え、特定のハードウェアに依存しないよう抽象化しつつ、マシンそのものにできるだけ近いところに配置されるべきです。2012年に始まったasm.jsプロジェクトも同じものを目指していました。asm.jsは、JavaScriptのサブセットを定義することで、コンパイラにとって理想的なターゲット言語になるべくJavaScriptのコンセプトを絞り込みます。ここにLLVMとasm.jsの歴史がひとつとなり、Alon Zakaiが生み出したLLVMからJavaScriptへのコンパイラであるEmscriptenというツールがすべてを可能にしました。今や私たちは、CやC++で書かれたプログラムを次のようにJavaScriptにコンパイルできます。 C/C++ → LLVM → Emscripten → JavaScript (asm.js) 詳しくは、Alonのスピーチ「Compiling to JavaScript」やCppCon 2014でのEmscriptenやasm.jsについてのスピーチ↓をご覧ください。 asm.jsの登場によってWebのアセンブリ言語が産声を上げたのです。これはあらゆるブラウザで標準化および統合化される可能性を秘め、そして実際に2015年のWebAssemblyのクロスブラウザ化作業の開始でまさにそれが起きたのです。この作業では、移植性と効率の高いバイナリインストラクション形式や安全な仮想マシン(VM)の定義をWeb向けにさらに進めます(Brendan Eichの“From ASM.JS to WebAssembly”も参照)。わずか2年の間にコミュニティは仕様を定めてプロトタイプをいくつも作り、Webプラットフォームに責任を持つ最大のあの企業と激論を戦わせ、主要な全ブラウザでWebAssemblyをリリースしたのです。これは誇張抜きで歴史に残る偉業です。おそらくブラウザは最初からこうあるべきだったのでしょう。ブラウザは低レベルのコンパイルターゲットを提供して、多くのプログラミン言語もWebでプログラミングできるようにし、既存のネイティブツールやライブラリやアプリも使えるようにすべきだったのです。しかし、ブラウザが今日のような地位を占めるようになると一体誰が予測できたでしょうか。とはいうものの、今や私たちがハッピーになれる道が開けてきましたし、そこに秘められた能力にはいやが上にも期待します。MozillaがWebAssemblyはゲームを変革すると考えています。やってみたい方はWebAssembly Studioをご覧ください。 RubyをWebに持ち込む 多くの人がWebで他のさまざまな言語を何年も使っていますが、そのためにはJavaScriptを使わないわけにいきません。その最もよく知られた例はCoffeeScriptでしょう。CoffeeScriptは2009年に始まり、JavaScriptにうんざりした人(またはJavaScriptに戻ってきた人)に別の選択肢をもたらしました。RubyやPythonに似た拡張文法で書くことができ、純粋なJavaScriptにトランスパイルされます。素晴らしいじゃないですか!それから何年もの間source-to-sourceコンパイラが全盛でした。JavaScriptにコンパイルされる言語の長大なリストを見ると何とも言えない気持ちになります。Rubyも例外ではありません。その中でも優れものはOpalです。 このsource-to-sourceアプローチには、効率やパフォーマンス、低レベル最適化の喪失などいくつも短所があります。JavaScriptを歪めてまで需要を満たそうとする必要がそもそもあるのかという根本的な批判もあります。WebAssembly登場以前であれば、その答えは単純そのもので、他に手段がないのだからそうするしかなかったのです。それが今や他の手段があるのです。この新しいコンパイルターゲットを利用して、実にさまざまな言語をWebプラットフォームに暖かく迎えようという動きが始まっています。他の言語たちは正確にはどんな形でWebにやってくるのでしょうか?CやC++などのネイティブ言語は既にLLVMへのコンパイルが可能ですし、LLVMのその他の実装(Rustなど)も、コンパイラツールチェインのターゲットをWebAssemblyに設定するという素直な手順でできます。Rubyのように、インタプリタやVMを自前で持つ動的言語だと少々難しくなります。 その理由を知るために、Rubyの主要な実装であるMRI(CRubyとも呼ばれます)を見ていくことにしましょう。どんなプログラミング言語であっても、構文の仕様よりはるかに多くのものが含まれています。そうしたものは、言語の実装や利用状況によっても定義されます。Rubyの場合は伝統的に、Unix的な環境に依存する各種ツールの総合的なエコシステムを意味し、ファイルシステム、コンパイラツールチェイン、標準ライブラリ、ネットワーク、その他OSが提供することが期待されるあらゆるものが含まれます。MRIすなわちCRubyはそれらを期待していますし、そのことは別に問題ありません。Rubyの使われ方、たとえばRailsアプリを実行するWebサーバーなどは、このモデルに完全に適応していて、その利点をすべて享受できます。 しかし、利用状況が変わるとそうはいかなくなります。たとえばRubyアプリをエンドユーザーに配布するなどです。期待するRubyインタプリタをローカルにインストールし、どのアプリにも正しいバージョンのgemを提供するとき、一般人と開発者と正規ユーザーを同じに扱うわけにはいきません。Rubyインタプリタを丸ごとアプリに仕込んでリリースするという回避方法もなくはありませんが、どう考えても現実的ではないでしょう。鼻で笑わないでください。一応実際に可能なのですから。Traveling Rubyは、Windows/Linux/macOS向けにRubyを同梱したアプリパッケージを作成するという印象的なプロジェクトです。Minqi PanによるRubyConf 2017の秀逸なトークでは、Rubyアプリを単一の実行ファイルに固め、メモリ上に仮想のファイルシステムを作成するなどの裏技を駆使して無理やり動かすというデモを披露しました(動画とコード)。どんな技を使ったのか私にはわかりませんが、私の好きなアプリの例(かつRubyデスクトップアプリとして最も知られているアプリ)はSonic Piというライブコーディング向けミュージックシンセサイザーです。 「インタプリタ同梱」的なアプローチは使えるは使えますが、サイズが相当巨大になるなど、いろいろ制約があります。参考までに、Ruby 2.5.1をmacOSに(rbenv install 2.5.1で)新規インストールしたときの典型的なサイズは20.7 MB、1,396ファイルにのぼります。Traveling Rubyでこれをtarball圧縮すると6.7 MB(未圧縮19 MB)、そしてMinqiのプロジェクトだと40.8 MB(34.1 MB圧縮)です。Sonic PIのようなデスクトップアプリなら大したことはありませんが、Webではそうはいきません。昨今だとどこかのWebサイトは10 MBをプッシュすることもあるようですが(某NYTimes.comとか)、これはあんまりです。それにまだ私たちはアプリのコード量を計算に入れていません。ファイルサイズだけではなく、MRI/CRubyの起動もかなり重く、メモリのフットプリントも相当な量になります。 MRI/CRubyインタプリタをブラウザに丸ごと同梱することもできなくはないかもしれませんが、現実的とは思えませんし、そもそも必要ですらないでしょう。とにかく、Rubyの実装だけの問題ではないことは明らかです。もっと効率的で軽量なRubyがどこかにあれば、インタプリタの必要な部分だけを取り出して、管理可能な小さいバイナリにコンパイルできるのではないでしょうか。おお、そういえばそんなものがあるじゃないですか! mrubyのあらまし mrubyのことをご存知ない方は、この機会にぜひお見知りおきを。個人的には、Rubyコミュニティで今アツいのはmrubyだと思います。mrubyプロジェクトを率いるのはRubyの作者としてお馴染みのMatzその人です。mrubyプロジェクトは2012年に組み込み(eMbedded)のユースケースに適した言語の実装としてスタートしました。組み込みの分野ではメモリやストレージといったリソースが限られています。ウォッシュレットのプログラミングも組み込みのひとつです。mrubyは完全にモジュール化されているので、MRI/CRubyのような巨大なインタプリタのモノリスをインストールするのではなく、好きなだけカスタマイズできます。ファイルシステムやネットワークアクセスが不要なら取っ払ってしまえばよいのです。ユーザーにできることを制限したいならそれもできます。mrubyはあなた好みのインタプリタをビルドできるのです。「完全なRubyとは違う」という制約はあるものの、利用状況に応じてmruby言語の主要な実装から本質的なメリットを得られます。mrubyについて詳しく知りたい方には、Zachary ScottのMRuby記事(訳注: サイトは現在ありません)とZacharyによるコミッターDaniel Bovensiepenへのインタビューがおすすめです。 mrubyは、インタプリタのアーキテクチャを考え直すことで新たな可能性の扉をも開きました。そのひとつが今私たちが興味津々の「Rubyスクリプトをネイティブコードにコンパイルする機能」です。プログラムを配布するのにありがたい機能ですね。アプリを単一の小さなバイナリで作成できるということは、Rubyの移植性が著しく高まるということです。マシンを問わずらゆる主要なプラットフォームで動く単独の実行ファイルを簡単に生成するmruby-cliのようなプロジェクトもあります。mrubyのバイトコードはCのプログラムに直接埋め込むこともできますので、clang(C言語ファミリーのLLVMフロントエンド)でコンパイルできます。コンパイルは次のような流れになります。 Rubyスクリプト → mrubyバイトコード → C → clang → LLVM → ネイティブ実行ファイル ここまでご理解いただけましたでしょうか? mrubyからWebAssemblyまで コンパイラのターゲットをLLVMにできるなら、WebAssemblyへの道が見えてきます。先ほどのEmscriptenがLLVM-to-JavaScriptコンパイラだったことを思い出してください。EmscriptenはWebAssemblyにコンパイルする機能を獲得しつつあります。すなわち、Rubyは次のような旅路を辿ることになります。 Rubyスクリプト → mrubyバイトコード → C → emcc(Emscriptenコンパイラのフロントエンド) → LLVM → Binaryen → WebAssembly ちょっと無理くりすぎな感もありますが、役者は揃いつつあります!フローの中にひとつ見慣れないものがありますが、BinaryenはWebAssembly向けのコンパイラ/ツールチェインインフラストラクチャです(名前はbinaryとEmscriptenのもじり)。AlonはBinaryenのコンパイルでWebAssemblyを生成するスピーチも行っています。 ぼちぼち実際に手を動かしてやってみましょう。まず必要なのはWebAssemblyツールチェインを取ってくることです(WebAssemblyの“Getting Started”を参照)。開発に必要なものを理解したら、Emscripten SDKをダウンロード/インストールして有効にします。 $ git clone https://github.com/juj/emsdk.git $ cd emsdk $ ./emsdk install latest $ ./emsdk activate latest SDKはgit cloneで取り、他の開発ツールと同じところに置くことをおすすめします。ツールの一部はまだGitHubの個人アカウントのリポジトリに置かれています。ほとんどの開発者はMozilaで作業しているようなので、今後WebAssembly GitHub orgのようなもっとオフィシャルな場所に統合されるのかなと思ったりもします。 次はPATHと環境変数を設定するスクリプトを実行しなければなりません(source ./emsdk_env.shのように実行)。Emscripten SDKでは独自のclangとnodeが提供されるので、通常の開発で使っているバージョンと衝突する可能性があります(Rubyをソースからコンパイル/インストールしてgemのネイティブ拡張をビルドするときにそのバージョンで失敗するなど)。そこで私は、常に新しいシェルで実行するのではなく、自分の.bash_profileへのエイリアスを追加して、WebAssemblyを使うシェルでのみwasm_initでSDKをアクティベートするようにしました。 alias wasm_init=”source ~/Developer/emsdk/emsdk_env.sh” 以上でEmscripten/WebAssemblyツールチェインが使えるようになります。 次はmrubyをインストールする必要があります。mirbやmrbcなどの便利なコマンドラインツールにアクセスできるよう、macOSならHomebrew(brew install mruby)でインストールすることをおすすめします。Linuxならsudo apt install mruby libmruby-devを使います。 いよいよWebAssembly向けのmrubyインタプリタをビルドします。まずはGitHubのmrubyリポジトリをcloneしましょう。 git clone https://github.com/mruby/mruby.git mruby/ディレクトリにcdします。ここからいよいよ面白くなってきます。mrubyインタプリタの実際のビルド設定は、シンプルなRubyスクリプトそのものであるbuild_config.rbファイルを定義してrakeを実行することで行います。リポジトリにあるデフォルトの設定には便利なboilerplateがいくつも詰まっていますし、ドキュメントもかなり詳しく書かれています。今回のニーズに応じて、設定ファイルの内容を以下で完全に置き換えることにします。 MRuby::Build.new do |conf| toolchain :gcc conf.gembox ‘default’ end MRuby::CrossBuild.new(’emscripten’) do |conf| toolchain :clang conf.gembox ‘default’ conf.cc.command = ’emcc’ conf.cc.flags = %W(-Os) conf.linker.command = ’emcc’ conf.archiver.command = ’emar’ end 新しい設定ファイルは、gccコンパイラツールチェイン(自分のシステムで同等に近いものなら何でも構いません)を用いてホストシステム向けのネイティブコンパイルを作成するようmrubyのビルドシステムに指示します。macOSの場合はgcc –versionで確認できます。 Apple LLVM version 9.1.0 … Continue reading mrubyをWebAssemblyで動かす(翻訳)