2013年12月19日木曜日

.NET の動的コード生成技術の紹介

この記事は C# Adventcalendar 2013 の記事です。この記事では、ちょっとだけ濃い目に、MsgPack for CLI というシリアライザライブラリで使っている、動的コード生成技術について簡単に解説します。この記事が動的コード生成を組み入れるときの助けになればいいな、と思います。

昨日の @rabitarochan さんの記事は CodeDOM と MEF を使用した、文字通り動的にコードを生成する話でしたが、今日は、もうちょっとライブラリの話にします。といっても、それほど深い話ではありません。

動的コード生成とは

「動的コード生成」と言う用語は、あまりちゃんとした用語ではないと思うので、この記事のために定義しておきます。

ここでは、「プログラムの挙動を実行時に変更するために、実行時にプログラムを生成し、その結果を実行すること」、とします(後々の分類のために、こういう定義にします)。一見すると、なんだか色々できそうですが、具体的に何ができるか正直想像しづらいかと思います。

どんな時に使うのか

基本的に、動的コード生成しないとできないことってあまりないです。リフレクションとか使っておけば大体できると思います(foddy みたいなメソッド呼び出し割り込み(いわゆる AOP)機構を実装したいならともかく)。

では、何故動的コード生成を使うかというと、主な理由はパフォーマンスです(あとは TargetInvocationException が憎くて仕方がないときとか)。リフレクションは遅いです。いや、もちろん I/O なんかよりはずっと速いんですが、少なくとも普通の仮想メソッド呼び出しやインターフェイスメソッド呼び出しに比べて遅いです。なので、何回も実行されるコードの場合、動的コード生成をやる価値があります。何回も呼び出されたときに、積み重なったパフォーマンスの改善が効いてくるからです。そうでない場合、そもそも動的コード生成自体がそこそこ重いとか、正直書くのがしんどいとかあるので、動的コード生成なんか使っちゃダメです。

たとえば?

身近な所では、以下のようなところで使われています。いずれも、リフレクションなどでは十分な速度が得られない場合に使用されています(と思います)。

  • Regex に RegexOptions.Compiled を渡した場合。内部的に動的コード生成されます。
  • P/Invoke の IL スタブ。これも、マネージドとアンマネージドの世界の間の相互変換(マーシャリングとか)を実行時に動的に生成しているはずです。
  • XmlSerializer。(昔は CodeDOM の代表格でしたが、最近は IL ベースになったようですね)

コード生成ライブラリについて

さて、FCL(だか BCL だか)には、動的コード生成のライブラリがいくつかあります(Cecil 等のライブラリについては、話が大きくなるので触れません)。今さら感もありますし、MSDN 読めばわかることですが、まぁゆるふわということで。

  • AssemblyBuilder:伝統的な動的コード生成 API で、CTS のデータ構造(アセンブリとか型とか)と、その実装である IL をごりごり書くタイプです。ステートや依存先オブジェクト用のバッキングストア(つまりはフィールド)が必要な場合に有用な選択肢です。また、生成した Assembly はディスクに書き出すことができます(書き込み先のディレクトリは AssemblyBuilder をインスタンス化するときに指定し、ファイル名は保存時に指定するという謎設計なので注意です)
  • DynamicMethod:.NET 2.0 から追加された API で、型やアセンブリを作らずに直接 IL を書き出すことができます。LGC(Lightweight Code Generation)とか言ったりしますが、開発者的にはあまりライトじゃないです。ExpressionTree ができたので、はっきり言っていらないと思います。Windows Phone 7 系で動的コード生成をする場合の唯一の選択肢です。
  • ExpressionTree:.NET 3.5 から追加され、4.0 で強化された API です。基本的には式ベースの、関数型スタイルのコード生成をしますが、4.0 でブロックや goto が実装され、かなり強力になりました。任意の MethodBuilder にコンパイル後の IL を出力できたりもします。ただし、フィールドにステートや依存先を持たせることはできません。
  • CodeDOM:古き良きコード生成技術です。これは他の技術と異なり、ソースコードを生成します。式ツリーが式ベースなのに対し、こいつは文ベースです。DOM オブジェクトからコンパイルしてアセンブリとしてロードしたりもできますが、実装としては一時ファイルにコードを吐いてコンパイラを実行しているので重いです。SSD に換装したところで、Windows のプロセス起動の遅さがネックになる感じです。あと、foreach や using、三項演算子はもちろん、単項演算子がサポートされてなくてなけます。後、空の文や式の指定方法が仕様として曖昧だったりします。あと、Windows XP 日本語版という残念な OS を使っていると、CodeDOM の内部で使っている csc の起動時に、Console IME がデッドロックしたりするので要注意です(もう直ったかもしれないけど)。

ポータビリティについて

最近は .NET 以外にも、Windows Phone とか、Silverlight とか、Windows Runtime とか、Unity とか Xamarin とかあります。なので、そういった各種 CLI(Common Language Infrastructure)実装系の間でのポータビリティも気になるところです。ポータビリティと言って思い浮かぶのは Portable Class Library、略して PCL です。PCL はプラットフォームの差異を吸収してくれるデファクトの機能です(デファクトであるということは、Mono でサポートされるようになったりと、エコシステムが出来上がるので楽なわけです)。ですが、動的コード生成は PCL にありません。そもそも、PCL はプラットフォーム間で共通の機能を提供するわけなのですが、以下の表を見るとわかるように、そもそもどのプラットフォームでも使える動的コード生成ライブラリがありません。

API.NET 3.5.NET 4.5.1Silverlight 5Windows Phone 7.1Windows Phone 8WinRT
AssemblyBuilder××××
DynamicMethod×××
ExpressionTree(※1)△(※2)××
CodeDOM××××

※この表に Unity と Xamarin を入れたかったのですが、まだ検証できてません。

※1 ExpressionTree の内容を IL にダンプするには、AssemblyBuilder がサポートされていなければなりません。

※2 BlockExpression や GotoExpression がないので、他の方法に比べるとかなり苦労するはずです。

うーむ。CLR 2 系がなくなれば ExpressionTree が PCL に入るかもしれませんね。フィールド使えませんが。

一応、上記の表を簡単にまとめると、以下のようになります。

  • AssemblyBuilder.Save や CodeDOM、LambdaExpression.CompileToMethod など、生成結果を保存する機能は、デスクトップ CLR にしかありません。
  • 新しいプラットフォームであれば、ExpressionTree が使えます。BlockExpression などを使えば(IL よりも変数のスコープの癖が大きい気がしますが)ほとんどの処理は書けるはずです。
    • .NET 3.5 ではちょっと心元ありませんが、AssemblyBuilder が使えるので頑張れるはずです。
    • Windows Phone7 の場合は、DynamicMethod 一択です。

余談:ステップ実行のサポートについて

動的に生成されたコードをデバッグしたい、ということはよくあります。そして、AssemblyBuilder や ExpressionTree にはデバッグシンボルを生成するための API があります。ありますが、実際にデバッガーをアタッチして動的コード生成の場合、そもそも対応させるソースコードをどうやって作って吐き出すのか(難しくはないけど面倒くさい)という問題があります。なので、この記事ではここについてはもう触れません。ステップ実行をサポートしたいのであれば、CodeDOM を使って吐き出すのがいいでしょう。DebuggerNonUserCode 属性を付けておくと、Visual Studioのデバッガオプションで「マイコードのみ」を有効にしているかどうかで、ステップ実行するかどうかが切り替えられていいかもしれません。

実装事例について

さて、あなたが何の因果かポータブルな動的コード生成が必要になったとしましょう。パフォーマンスを売りにしているライブラリをうっかり移植しようと思うとか、AOP フレームワークを実装する仕事が来たとか、ちょっとしたユーティリティでどうしても TargetInvocationException をスローしたくないとか、いろいろな偶然が重なることもないとは言えません。もっと可能性の高いこととして、突然 github でプルリクエストを送りたくかもしれません。人間万事塞翁が馬です。そのときに困らないように、どのように動的コード生成をポータブルな感じにしたのか、実例を紹介しましょう。

ここでは、MessagePack for CLI というシリアライザライブラリの事例を紹介します。このライブラリは、.NET 系(Silverlight や Mono)で、MessagePack というバイナリ形式へのシリアル化ができるようにするライブラリです(ちなみに、CLI は Command Line Interface ではなく、Common Language Infrastructure です)。パフォーマンスを確保しつつ(MessagePack は実装系のパフォーマンスを売りにしているので)、気軽に使えるようにするために(あらかじめコードを作っておくのはちょっと敷居が高いです)、基本的には実行時にコード生成を行います(余談ですが、ビルドシステムを作り込んでいるので事前コード生成の方が良いという意見もあったので、0.5 では CodeDOM もいれてます)。ちなみに、ちょっとでも遅くなると、ネットですぐにベンチマークがさらされるので油断なりません。

さて、ライブラリの紹介はこれくらいにして、事例としては、どのように動的コード生成を使いつつ複数のプラットフォームをサポートするのかと、単体テストをどのように行っているのかを紹介しましょう。

補足:シリアライザがやることについて

と、その前に、簡単にライブラリの内部動作に触れておきましょう。MessagePack は伝統的に(?)オブジェクトグラフをサポートせず、循環参照のないツリー構造のみをサポートします(他の言語のバインディングで循環参照がサポートされているものはないはずです。そして、相互運用の必要がないなら、高機能な BinaryFormatter なり NetDataContractSerializer を使えばよいので、MessagePack を使う理由がありませんね)。なので、おおざっぱに言うとシリアル化は以下のようなシンプルな処理になります。

  1. 対象のオブジェクトのフィールドやプロパティを列挙します。
  2. そのフィールドやプロパティの値が null でもプリミティブ(整数、実数、文字列)でもなければ、再帰的に展開します。
  3. プリミティブ型を MessagePack の仕様に従ってエンコードします。

ある程度慣れた人であれば、上記の処理はリフレクションを使用して実装可能なことが分かるでしょう。そして、エンコード処理はそれほど重くないので、処理時間のほとんどはリフレクションによるメンバーの再帰的な分解になります。MessagePack for CLI では、これを高速化するために動的コード生成を行います(ちなみに、逆シリアル化も似たようなものですが、ストリームのパース処理と検証が入るため、リフレクション以外の部分のパフォーマンスも重要になります。どちらにせよ、複雑なのでここでは触れません)。

複数のプラットフォームのサポート

さて、MessagePack for CLI では、デスクトップ以外にも、Silverlight、Windows Phone(0.4 で凍結)、WinRT(WinRT コンポーネントではなく、CoreCLR 用のライブラリとして実装)などをサポートしています。また、そのため、単一のコード生成技術ではなく、プラットフォーム毎に対応した動的コード生成ライブラリを使用しています。

動的コード生成ライブラリによるコード生成と、シリアライザの機能の実装を分離するため、0.5 から抽象構造(AbstractSerializers 名前空間)を実装しています。この抽象クラスの実装が、コード要素を表す抽象クラスの派生型を返して、コードを組み立てていきます。まぁカッコつけて言うと Strategy とか AbatractFactory とかそういうものです(Visitor にしなかったのは、単に面倒だったからです)。シリアライザを実装するうえで必要なプログラム構造はある程度固定されているので、あまり汎用性や粒度のきれいさを考えずに、実装のしやすさで作ってます。具体的には、このあたりが抽象構造で、ここここに実装がありますので、興味のある方はご覧ください。

さて、構造を抽象化したところまではいいのですが、ビルドを上手く工夫しないと、そもそもビルドできないコードになってしまいます。 MessagePack for CLI では、ファイル内で切り替えが必要な部分、例えばコード生成実装クラスのインスタンス化処理で、コンパイラプリプロセッサディレクティブ(#if)を使用してます。

さらに、全部のファイルに #if 書くのはさすがにかったるいので、ターゲットプラットフォームごとにプロジェクトを分け、さらに名前空間ごと除外できるようにしています。名前空間と使用している動的コード生成技術に対応を持たせているので、名前空間(を射影したディレクトリ)ごと除外しています(興味のある方は sln ファイルを開いてみてください)。プロジェクト間の同期には色々ツールもあるのですが、そもそもソリューションを分けていたりもするのと(たとえば CLR 2 用の単体テストと CLR4 系の単体テストを混在させると、Visual Studio の単体テストエンジンがプロセス内 SxS してくれないので、それでロードの失敗とか基本的なバグを見過ごすとか起きがちなので)、ファイルごとにこれは同期しない、これは同期する、など細かく制御したかったので、カスタムのちょっとしたツールを使ってやってます。

単体テスト

動的コード生成のテストですが、パブリックなコンパイラを作っているわけではないので、取りうる入力値をゆるっと同値分割して、それらをテストするコードを書いて、期待通り動作するか見ています(同値分割をサボるために、T4 に型を食わせて、力押しでテストコードを量産したりしてます)。ようは、コード生成そのものを網羅的にテストするのではなく、出来上がった結果が正しく動作するかどうかをテストしています。

なお、各コード生成技術は、プラットフォームに合わせてビルドされますが、デスクトップ CLR ですべてのコード生成技術を実行し、MsgPack for CLI 自身のバグを検出できるようにしています。ちなみに、IL 系だと、一応アセンブリをダンプして PEVerify もかけます。偶然間違った IL が動いてしまったという事態を避けたいので。

あとは、デバッグ用のコードが仕込まれていて、どのライブラリを通っても、最終的な結果を DLL またはソースコードとしてダンプできるようにしています(AssemblyBuilder.Save に、LambdaExpression.CompileToMethod を組み合わせたりしてます)。それを ILDasm やら ILSpy やら PEVerify やらに通してデバッグします。(余談ですが、CodeDOM バインディング実装したので、ここはだいぶ楽になりました)

まとめ

とりとめのないゆるふわ記事ですが、いちおう以下のようにポイントをまとめておきます。

  • 動的コード生成は、実行時にコードを生成して、動的なコード(いわゆる Interpreter 的な処理)を高速化する技術である。
  • ポータブルな動的コード生成ライブラリはない。
  • テストは、まずは出来上がった結果のコードが正しく動くのかをテストするとよい。
  • デバッグでは、生成したコードを検査できるようにするとよい。

免責事項として、MessagePack for CLI 0.5 ブランチは今も変更中のため、後からこの記事を見た人がいたら、リンク切れになっていたりするかもしれません。そのときは、お手数ですが、いったんローカルに clone するなどして、2013/12/19 時点のブランチまで git 系のツールで巻き戻していただければと思います。

明日は @terry_u16 さんです。