FlowingDev

C# Playgroundの解説:.NETランタイム丸ごとブラウザにデプロイする

WebAssemblyがいかにして本物のRoslynコンパイラと.NETランタイムをブラウザにもたらし、WebをネイティブなC#のプレイグラウンドに変えるのかを探る。

ツールを試す: C# Playground

一言でいうと

C#というプログラミング言語のフルパワー、Roslynコンパイラ、そして.NETランタイムのすべてをWebAssemblyを使ってWebブラウザの中に丸ごとテレポートさせたもの。数ギガバイトもあるIDEをインストールすることなく、コードを書き、実行し、本物のDLLにコンパイルできる。

解決する課題

歴史的に見て、C#での開発は「ヘビーメタル(重厚長大)」の代名詞だった。2000年代初頭にJavaに対するMicrosoftのエレガントな回答として生まれたC#は、Windowsエコシステムに深く縛られていた。ちょっとしたC#のスクリプトを書いたり、特定のLINQクエリの挙動をテストしたりしたいときでも、単にターミナルを開けばいいというわけにはいかなかった。Visual Studioを立ち上げ(スプラッシュスクリーンを眺めながらコーヒーブレイクの始まりだ)、新規プロジェクトを作り、ConsoleApp42みたいな後悔するような名前を付け、NuGetパッケージの復元を待ち、5行のコードを書いて実行し、最終的にはそのフォルダを削除するハメになっていたのだ。

Microsoftは.NETをオープンソース化し、クロスプラットフォーム化し、.NET CLIを導入することでこれに革命を起こした。しかしそれでもなお、シンプルな「Hello, World」をコンパイルするだけのために、巨大なSDKをダウンロードしてインストールする必要があった。

開発者という生き物は、即座のフィードバックループを切望している。アルゴリズムのプロトタイプを作り、構文を検証し、ゼロフリクションでバグを再現したい。私たちにはサンドボックスが必要なのだ。しかし、C#のような静的型付けのコンパイル言語の場合、Web上で真のサンドボックスを作ろうとすると、従来はコードをリモートのバックエンドサーバーに送信し、そこでコンパイルしてDockerコンテナ内で実行し、テキスト出力をブラウザに送り返す必要があった。そのアプローチは遅く、ホスティングコストもかさみ、常時インターネット接続を必要とする。

C# Playgroundは、サーバーを完全に排除することでこれを解決する。コンパイラとランタイムを直接クライアントに持ってくることで、あなたのブラウザを、使い捨ての超高速開発環境へと変貌させるのだ。

仕組みの裏側

C#をWebブラウザ内でネイティブに実行するなんて黒魔術のように聞こえるかもしれない。しかし実際には、2つの巨大なエンジニアリングの成果、WebAssemblyとRoslynコンパイラのエレガントな組み合わせによるものだ。

WebAssemblyランタイム

WebAssembly (Wasm)は、Web向けのポータブルなコンパイルターゲットとして設計されたバイナリ命令フォーマットだ。これによって、C、C++、Rustのような言語を、ブラウザ上でネイティブに近い速度で実行できる。

C#をブラウザで動かすために、MicrosoftはC#をJavaScriptで書き直したわけではない。代わりに彼らは.NETランタイム(歴史的にはMonoランタイムで、現在は.NET Wasmとして統合されている)のC/C++コアをWebAssemblyにコンパイルした。あなたがC# Playgroundをロードすると、ブラウザはdotnet.wasmをダウンロードする。これは、ブラウザのV8エンジン(あるいはSpiderMonkeyやJavaScriptCore)の内部で直接実行される、完全な機能を持った.NET仮想マシンなのだ。

Roslyn:Compiler as a Service

.NETランタイムがコードを実行する前に、人間が読めるC#からMicrosoft Intermediate Language(MSIL、または単にIL)へとコンパイルする必要がある。

そこでRoslynの登場だ。(ソースコードを入れると実行ファイルが出てくるという)ブラックボックスとして機能していた古いコンパイラとは異なり、Roslynは豊富なAPIを備え、C#でゼロから構築された。いわゆる「Compiler as a Service」である。Roslyn自体がC#で書かれており、Wasm経由でブラウザ内で実行される.NETランタイムがあるということは、実際にRoslynコンパイラをブラウザの中で動かすことができるということだ。

実行パイプライン

Playgroundにコードを打ち込んで「Run」を叩いたとき、あなたのマシンのメモリ内部で完全に完結して起こるのが以下のプロセスだ。

  1. パース (Parsing): ブラウザ内のRoslynコンパイラが、生のテキストを構文木(Syntax Tree)にパースする。
  2. コンパイル (Compilation): Roslynが構文木をインメモリのアセンブリ(ILバイトコード)にコンパイルする。
  3. 実行 (Execution): ブラウザ内の.NET Wasmランタイムがこのインモリアセンブリをロードし、IL命令を実行する。標準出力(Console.WriteLineなど)はインターセプトされ、あなたの画面にパイプ(出力)される。

DLLの書き出し(エクスポート)

Roslynは本物の標準的なコンパイルを行っているため、生成されるインモリアセンブリは、デスクトップ上にある.dllファイルと機能的にまったく同じだ。Playgroundはメモリからそのバイト配列を取得し、ブラウザのダウンロードをトリガーするだけでいい。得られるのは「モック(偽物)」のDLLではなく、公式エンジンによってコンパイルされた標準的な.NETライブラリであり、そのままサーバーやUnityのゲーム、あるいはデスクトップアプリにぶち込んで使える代物なのだ。

現場のリアルなストーリー

「これコンパイル通る?」論争

2人の開発者がプルリクエストのレビューで白熱していた。1人は、C# 12のパターンマッチングとswitch式の特定の組み合わせを使うと、到達不能コード(unreachable code)によりコンパイルエラーを吐くと主張した。もう1人は、絶対に有効な構文だと断言していた。どちらもコンテキストスイッチをしたくなかったし、ローカルのgitの変更をスタッシュして新しいテストブランチを立ち上げるのも面倒だった。代わりに、彼らはブラウザのC# Playgroundにスニペットを貼り付けた。3秒後、コードはコンパイルされ、期待通りの結果を出力し、この賭けは一瞬で決着がついた。 教訓: 言語仕様のセマンティクスに関する議論を終わらせる最速の方法は、即座でフリクションのないフィードバックループである。

即席のDLLビルダー

あるコンサルタントは、ガチガチにロックダウンされたクライアントのノートPCで作業をしており、.NET SDKをインストールする管理者権限を持っていなかった。メインのアプリケーション(ドラッグ&ドロップでプラグインDLLを追加できる仕様だった)で利用するために、独自のログフォーマットをパースする小さなユーティリティ関数を急いで書く必要があった。ITヘルプデスクにVisual Studioのインストールを申請して3日待つ代わりに、そのコンサルタントはC# Playgroundを開き、パースのロジックを書き、コンパイル済みのDLLをエクスポートして、アプリケーションのプラグインフォルダに直接放り込んだ。 教訓: ブラウザが本物のコンパイラを搭載しているとき、それはローカルマシンの制限をすり抜ける、完璧に実用的なビルドサーバーと化す。

サンドボックスでの面接

あるテックリードがリモートで技術面接を行っていた。候補者に使いにくいIDEのセットアップを画面共有させたり、リモートデスクトップのレイテンシに悩まされたりする代わりに、2人は共有のC# Playgroundセッションにログインした。候補者はリアルタイムのフィードバックを得ながら複雑なLINQクエリを組み立て、オブジェクト指向のデータ構造をテストし、環境構築のストレスなしに自分のスキルを証明することができた。 教訓: 純粋で環境構築不要なサンドボックスは、開発者の純粋なコーディング能力をシステム管理スキルから切り離してくれる。

よくある間違いと罠

  • 本物のファイルシステムがあると思い込むこと: System.IOクラスを使うことはできるが、あなたがやり取りしているのはWebAssembly環境が提供する仮想的で使い捨てのファイルシステムだ。C:\temp\log.txtにファイルを書き込んでも、それはブラウザのメモリ上に存在するだけで、ページをリフレッシュした瞬間に消滅する。
  • OSネイティブのライブラリを呼び出すこと: P/Invoke (Platform Invocation Services) を使って [DllImport("user32.dll")] のようなネイティブWindows APIを呼び出そうとしても、滑稽なほど見事に失敗する。今やあなたのOSはWebブラウザなのだ。ここにネイティブのデスクトップライブラリは存在しない。
  • ハイパフォーマンスなクラスターのように扱うこと: WebAssemblyは高速だが、ブラウザによって強力にサンドボックス化されており、メモリやスレッドの機能に制限があることが多い。Playgroundはロジックのテストには最高だが、2GBのデータセットに対して並列で Parallel.ForEach を回すのには絶望的に向いていない。
  • async void の罠: サクッとスクリプトを書いていると、操作をawaitする場合に Main メソッドが async Task を返す必要があることを忘れがちになる。サンドボックスで async void を使うと、非同期タスクが実際に完了する前にランタイムが実行コンテキストをシャットダウンしてしまい、結果的にサイレントに失敗する原因になる。
  • アセンブリ参照を忘れること: DLLをエクスポートしようとするなら、コードはサンドボックスで提供されているベースクラスライブラリに対してコンパイルされることを忘れてはならない。PlaygroundのRoslynコンテキストにロードされていない外部のサードパーティ製NuGetパッケージに依存している場合、コードはコンパイルできない。

なぜ注目すべきなのか

フル装備のIDEを立ち上げる精神的・物理的なオーバーヘッドが目の前のタスクの重さを上回ったときは、いつでもC# Playgroundに手を伸ばすべきだ。ローカルのツールチェーンをアップグレードすることなく、新しい言語機能(最近のC#リリースで導入されたプライマリコンストラクタやコレクション式など)を味見してみるには完璧なツールである。

また、再現可能なコード例を共有するための計り知れない価値を持つアセットでもある。オープンソースライブラリのバグを見つけたり、StackOverflowで質問したりするときに、相手が正しく環境を構築してくれることを祈ってテキストの壁を貼り付けるよりも、その挙動を即座に再現できるPlaygroundへのリンクを提供する方が無限に役立つ。そして最後に、本物の .dll を簡単にエクスポートできるこの機能は、軽量なプラグインやヘルパーライブラリをその場で生成するための、強力でインストール不要なコンパイルツールとして大いに活躍してくれる。

さらに深掘りする

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: C# Playground