WebAssemblyとBlazor: 何十年の問題を解決する

WebAssemblyとBlazor: 何十年の問題を解決する

原文(投稿日:2019/05/30へのリンク

2019年4月中旬にMicrosoftは、「なんでもできる」実験フェーズから「これを実現することを約束している」プレビューに若いフレームワークを前に進めた。フレームワークはBlazorと名付けられ、ブラウザーで実行され、Razorと呼ばれるテンプレートシステムまたは「Viewエンジン」を利用するため、.NET開発者がほとんど諦めていたシナリオを可能にする。これはただ、開発者が(JavaScriptを必要とせず)C#でクライアントサイドの構築を可能にするだけでなく、開発者がプラグインを使うことなく既存の.NET Standard DLLをブラウザで実行できる。

Silverlightの希望

どこでも実行できる.NETの夢は2006年に「Windows Presentation Foundation/Everywhere (WPF/E)」というコードネームのフレームワークで、Silverlightとして公開された。最初のバージョンはWPFを介して世界に導入されたExtensible Application Markup LanguageまたはXAMLと呼ばれる宣言型ユーザーインターフェイスをサポートしていた。このプラットフォームはUI要素に関するきめ細かい制御を提供し、JavaScriptからアクセス可能なDocument Object Model (DOM)を表面化させた。

Silverlight 2は2008年にCommon Language Runtime (CLR)をブラウザープラグインとして実装して、完全な.NETをサポートしており、導入が加速した。開発者はWebアプリの構築に任意の.NET言語を利用でき、Model-View-ViewModel (MVVM)のような成熟したデータバインディングパターンと、RESTやWindows Communication Foundation (WCF)クライアントを使ったWeb APIで通信できる。これにより.NET開発者はJavaScriptのホコリを払い、クロスブラウザーのテストを心配することなく、共通のコードベースを持つひとつのプラットフォームに集中してアプリを配信できるようになった。

Silverlightは気づいていないが、2007年はこのプラットフォームにとって厳しい年であった。一見関係なさそうな出来事が2つ起こり、最終的には終焉に向かうことになった。1つ目は、Web Hypertext Application Technology Working Group (WHATWG)とWorld Wide Web Consortium (W3C)の共同作業が始まり、HTML5仕様の最初のドラフトが2008年に公開された。

2つ目が2007年6月29日に、AppleがiPhoneをリリースした。

レースは始まった。携帯電話はほぼ一晩で連絡先リスト付きの折りたたみ式電話から、ゲームや組み込みWebブラウザーを持つポータブルコンピューターに進化した。短期的にはSilverlightの未来は有望に見えた。iPhoneに対するMicrosoftの応答は、開発プラットフォームとしてSilverlightのフレーバーをサポートしたWindows Phone 7であった。Chromeがサポートした。 MicrosoftがSilverlightをiPhonesやAndroid電話に搭載することを考えれば、「一度書けばどこでも実行できる」という聖杯がついに発見されていただろう。

ただ、そうはならなかった。

「ブラウザー上の仮想マシン」は実行中のセキュリティの問題、実用上の目的、そして潜在的なバッテリーの消耗など多くの理由により、特にモバイルデバイス上のブラウザープラグインへの扉は閉じらることとなった。業界はモバイルのエクスペリエンスを構築するのにHTML5に注目し始めた。Microsoftは焦点を変えており、Silverlight 5がリリースされた2011年には、多くの開発者は、もう新しいバージョンがないという前兆を見ていた。

HTML5とJavaScriptはWeb開発者の心と精神を勝ち取り続けた。jQueryのようなツールはDOMを正規化し、マルチブラウザーアプリケーションを構築しやすくしたのと同時に、ブラウザーエンジンは一度ビルドすればどこでも実行できるようにする一般的なDOM標準を採用し始めた。Angular, React, Vue.jsのようなフロントエンドフレームワークの増大により、Single Page Applications (SPA)は主流になり、ブラウザーのオペレーティングシステムに最適な言語としてJavaScriptが採用された。

プラットフォームとしてのJavaScript

2013年3月、asm.jsが世界に紹介された。ドキュメンテーションでは、コンパイラのターゲット言語として使用できる低レベルに利用できるJavaScriptの厳密なサブセットとしてそれを説明している。この仕様では基本的に一連のJavaScript規則を定義しており、事前コンパイルでコードの最適化を可能にし、(JavaScript自身を動的言語にした)厳格な型付けとヒープベースのメモリモデルを提供する。

asm.jsの導入により、C/C++コードをJavaScriptにコンパイルできるようになり、新たな可能性が広がった。規約の制限により「asm.js-aware」エンジンは、JavaScriptを高パフォーマンスのネイティブコードに効率的にコンパイルできるようになった。これがどのように実現されているのかを理解するために以下のCのコードスニペットを見てほしい:

int find(char *buf, char test) { char *cur = buf; while (*cur != 0 && *cur != test) {cur++; }if (*cur == 0) {return -1; } return (cur - buf);}

このコードは文字列からテスト文字か、その終わりを示すゼロバイトを効率的にスキャンして、オフセットを計算する。すでにC++は、Clangと呼ばれるツールを使ってLLVMツールチェインでバイトコードに準拠したコンパイルができる。LLVMは高速にクロスプラットフォームのコードにコンパイルできる一連の技術である。Emscriptenと呼ばれるプロジェクトは、ツールチェインを利用してasm.jsを生成する。

C++コードをEmscriptenでコンパイルすると非常に最適化された数十行のJavaScriptが生成される。以下のコードは生成されたコードを示すために簡略化している:

function find(buf, test) { buf = buf|0; var cur = buf|0; var result = -1|0; while (1) {var check = HEAP8[cur>>0]|0;var foundZero = (check) === (0);if (foundZero) {break;}var foundTest = (check) === (test|0);if (foundTest) {result = (cur - buf)|0;break;} } return result|0;}

生成されたJavaScriptはすべてのブラウザーで正しく実行される互換性がある。ゼロ付きの排他的論理和(|0)は、単に任意の数値を符号付き整数に変換するだけである。古いブラウザーでは、小数部分を持たない数値であることを保証する。モダンなブラウザでは、この規約により事前コンパイラは32ビット整数を使用され、デフォルトの64ビット浮動小数点数と比べて結果としてより高速な算術演算が可能になる。ゼロの右シフト(>>0)は、オーバーフローを防ぎ、「インデックス」の型を整数型として宣言するとともに、asm.jsで利用できるHEAP8という繰り返し利用できる型付きのバイトのバッファを宣言する。

asm.jsではforによる繰り返しは定義されていない。全てwhile(1)ループに変換される。これによりコンパイラ最適化が簡単に適用できる。最適化は非常に効果的なため、チームはUnreal 4エンジンに移植され、ブラウザーの中で、ネイティブに近いパフォーマンスで3D一人称ゲームを実行できた。

WebAssembly: 新しい希望

2017年の早くスタックベースの仮想マシンにおけるバイナリ命令フォーマットのWebAssemblyがリリースされた。WebAssemblyはいくつかの点でasm.jsよりも優れた移植可能なコンパイルターゲット(略してWasmと呼ばれる)を提供する:

asm.js にコンパイルするすべてのコードはWebAssemblyをターゲットにできる。前の例では、単純にコンパイラフラグを変更すると、拡張子.wasmが生成される。ファイルはたった116バイトしかない。ファイルにはバイトコードが含まれているが、標準化されたテキスト表現がWebAssemblyテキストフォーマットという名前で存在している。これはWebAssemblyのfindモジュールのテキスト表現である:

(module(type $t0 (func (param i32 i32) (result i32)))(import "env" "memory" (memory $env.memory 256 256))(func $a (type $t0) (param $p0 i32) (param $p1 i32) (result i32) (local $l0 i32) (local $l1 i32) (local $l2 i32) (local $l3 i32) get_local $p0 set_local $l0 loop $L0get_local $l0i32.load8_stee_local $l2i32.eqzset_local $l1get_local $l0i32.const 1i32.addset_local $l3get_local $l1i32.const 1i32.xorget_local $p1i32.const 24i32.shli32.const 24i32.shr_sget_local $l2i32.nei32.andif $I1get_local $l3set_local $l0br $L0end end i32.const -1 get_local $l0 get_local $p0 i32.sub get_local $l1 select)

コードサイズは最適化されているため、関数名はaに変更されている。

WebAssemblyとBlazor: 何十年の問題を解決する

WebAssemblyは現在1.xが安定版としてリリースされており、モバイルを含むすべてのモダンブラウザーでサポートされている。いくつかの言語は有効なコンパイルターゲットとしてWasmを採用している。C, C++, Go, Rust, TypeScriptなど多数の言語を使用してWebAssemblyをビルドできる。コンピュータービジョン、音声合成、ビデオコーデックサポート、デジタル信号処理、医療画像処理、物理シミュレーション、暗号化、圧縮などのソリューションで実装されている。

しかしC#ではどう?

WebAssemblyが紹介された直後に、(Common Language Runtimeを含む).NET FrameworkをWebAssemblyに移植する作業が始まった。

努力は成功した。

ブラウザーとRazorビューエンジン

MicrosoftのソフトウェアエンジニアSteve Sanderson氏は、2017年の年末に彼のBlogでBlazorを発表した。それは「単なる実験」であり、正式な製品ではなかった。それは「.NETをWebAssemblyで実行するにはどうしたらよいですか?」という質問から始まった。最初の答えは、彼がたった数時間でWasmバイナリとしてコンパイルしたコンパクトバージョンの.NETランタイムであった。.NET自身はブラウザーではあまり便利ではなく、ユーザーと対話するUIを何かしらの方法で作る必要がある。 Webテンプレートを作成するマークアップとC#の組み合わせたRazorファイルの安定した作業を構築し、Blazorはデータバインディングと依存性注入から、再利用可能なコンポーネント、レイアウト、JavaScripttoとfromの呼び出し機能など、多数のサービスが追加された。これらのすべてのサービスを組み合わせることで.NETとC#を使ってSingle Page Applications (SPA)を構築することが可能になった。

Figure 1: デフォルトのBlazorアプリケーション

なぜ誰を気にする必要がある?Blazorに対する初期の開発者の反応は圧倒的にポジティブであった。その理由はいくつかある:

ここまででBlazorの歴史と動機が理解できたと思う。ここからは技術的な詳細を探っていこう。

Blazorインストールに必要なものはBlazor入門記事にある。Blazorがインストールできたら、クライアントのみか、クライアントとASP.NET Coreバックエンドプロジェクトかを選択できる。このプロジェクトは、既存のサーバーサイドMVCベースのプロジェクトに非常に馴染みがある。生成されたDLLが、ブラウザー上に直接読み込まれて、WebAssemblyバージョンの.NETで実行される。

Figure 2: Blazorアプリのネットワークアクティビティ

mono.js JavaScriptでmono.wasmが動的に読み込まれ、ブラウザー内で.NETが実行される。アプリケーションを構成する残りのDLLが読み込まれる。

(依存性注入を含む)ブラウザー内のC#

デフォルトテンプレートにはダミーの天気情報をフェッチするページが含まれている。これはクライアントのWasmによって完全にレンダリングされるRazorビューである。

This component demonstrates fetching data from the server.

@if (forecasts == null){

Loading...

}else{@foreach (var forecast in forecasts){}
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
}

テンプレートの上部でページのルートを決定する一連のディレクティブがあり、usingディレクティブを宣言して、ブラウザー内部で利用できるようになっている.NET FrameworkのHttpClientのコピーを依存性注入で取得している。

@page "/fetchdata"@using GetStarted.Shared@inject HttpClient Http

最後にページのコードの一部が@functionsブロックに埋め込まれている。ここで注意することは、コードがすべてC#であることだ。ネットワーク操作は、おなじみのHttpClientasync/awaitがサポートされている。

WeatherForecast[] forecasts;protected override async Task OnInitAsync(){ forecasts = await Http.GetJsonAsync("api/SampleData/WeatherForecasts");}

ビューテンプレートはページをレンダリングするが、コントロールはどうだろうか?

再利用可能なコンポーネント

Blazorは階層コンポーネントの構成可能なUIをベースにしている。weather forecast コンポーネントと他との唯一の違いは、ルートを提供するpageディレクティブである。これは、現在の値を表示するspanと、組み込みHTMLinputのレンジを補強するLabelSlider.razorと呼ばれるコンポーネント用のテンプレートである。

@CurrentValue

バインド構文はbind-{property}-{event}というフォーマットである。eventはオプションで、イベントが発生したらいつでもバインドが更新される。これがない場合、ユーザーがスライダーバーの移動を止めたときにspanのみを更新する。oninput値をフックすることで、スライダーが移動するごとに更新される。

関連付けられたコードは、親コンポーネントに最大値と最小値の幅の値をセットでき、現在の値にdata-bindできるようにパラメータを公開する。Actionプロパティは名前でCurrentValueに関連付けられており、規約によりCurrentValueChangedが双方向のデータバインディング(親コンポーネントは変更イベントとバインドされた値を更新するために「listen」できる)を容易にする。

[Parameter]int Min { get; set; }[Parameter]int Max { get; set; }private int _currentValue;[Parameter]int CurrentValue{ get => _currentValue; set {if (value != _currentValue){_currentValue = value;CurrentValueChanged?.Invoke(value);} }}[Parameter]Action CurrentValueChanged { get; set; }

タグ付けされていないParameter属性はコンポーネントでのみ表示される。コンポーネントの再利用は、コンポーネント名のタグをドロップして、必須パラメーターを指定するだけである。以下がその使われ方である:

この例では、親コンポーネントのcurrentCountプロパティが双方向バインディングとして確立している。

既存のライブラリを使用する

Blazorの非常に強力な利点は、既存のライブラリを「そのまま」統合でいることである。例えば、markdownを使って、ブラウザーでHTMLをプレビューできるBlogエンジンを考えてみよう。 Blazorでこれを構築する場合、NuGetパッケージをインストールするだけである。このケースではオープンソースのMarkDigプロセッサーが利用できる。このライブラリーは直接呼び出すことができる:

var html = Markdig.Markdown.ToHtml(SourceText);

NuGet DLLはブラウザーにインポートされ、他のプロジェクトの参照と同様に、クライアントサイドアプリケーションから呼び出すことができる。

Figure 3: ブラウザーで変換されたMarkdown

JavaScriptにおける対話

Blazorが提供する重要なサービスは.NETからJavaScriptを呼び出すことと、その逆である。 BlazorからJavaScriptメソッドを呼び出すためにグローバルのwindowオブジェクトにアクセスする必要がある。これを呼び出すには:

window.jsAlert = msg => alert(msg);

相互運用機能は以下のように使用する:

await JsRuntime.InvokeAsync("jsAlert", "Wow!");

InvokeAsyncメソッドは、値の受け渡しと受け取りをサポートしており、値はJavaScript/.NET間でBlazorランタイムによってマーシャリングされ、自動的に双方向で変換される。JsInvokable属性を使って、C#メソッドを公開するとJavaScriptから呼び出せる。markdownを変換する呼び出しの例:

public static class Markdown{ [JSInvokable] public static string Convert(string src) {return Markdig.Markdown.ToHtml(src); }}

JavaScriptからの呼び出しはDotNet.invokeMethodでアセンブリ名、公開されたメソッド名、すべてのパラメーターを受け渡して呼び出す。

Figure 4: JavaScriptから.NETの呼び出し

これにより、従来のアプリケーションの拡張を可能にし、既存のJavaScriptを利用可能にする。Blazorアプリから他のWebAssemblyモジュールを呼び出すこともできる。

Blazorは前進している

MicrosoftはBlazorを実験フェーズから正式プレビューに移行させた。サーバーサイドレンダリングにコンポーネントモデルを使ったBlazorのバージョンは、.NET Core 3の最終リリースとともにリリースされ(.NET Coreロードマップを参照)、クライアントリリースは間もなく続く予定である。最終化までは、まだ作業がある。デバッグエクスペリエンスは非常に制限されており、改善の必要がある。事前コンパイルでネイティブのWasmを生成することによってコードパフォーマンスを最適化する機会がある。全体のサイズは、ブラウザーに送る前に(tree-shakingとして知られているプロセスで)未使用のライブラリのコードを削ることによって削減する必要がある。WebAsssemblyの関心と採用は日々増しており、Blazorは夢にまで見たどこででも実行できるC#と.NETコードを実現した。

著者について

Jeremy Liknessは、MicrosoftのAzureクラウドアドボカシーである。Jeremyは1982年に初めてプログラムを書き、25年間エンタープライズアプリケーションを構築してきた。彼は4冊のテクノロジー本を書いており、以前は8年間Microsoft MVPであり、国際的な基調講演スピーカーである。 Jeremyは食物ベースの食事をとり、自由時間のほとんどを、北西海岸にある彼の家の近くでランニング、ハイキング、キャンピングをして過ごしている。Jeremyのブログをフォローしよう。