2016年10月4日火曜日

マルチプラットフォーム対応のNuGetパッケージの作り方

まず、2016/10/4 現在、NuGet Docs サイトはリニューアルされ、かなり見やすくなってますので、ぜひそちらも参照ください。
昔のサイトもそれほど読みにくくはありませんでしたが、色々と情報が増えてどこを見ればいいのかわからない感じだったのも事実です。なので、個人的にはかなり良くなったと思います。 全文検索もできますし。
たとえば、docs.nuget.org のトップページにある「Guides」には目的別のガイドラインがありますし、クイックスタート的な記事もあります。このあたりを見れば結構何とかなるはずですし、リファレンスもメニューからたどることができます。
さて、その公式ドキュメントを見ればマルチプラットフォーム対応のやり方もわかると思います。色々書いてありますが(<file src> とか初めて知った)、ようは以下のようにすれば OK です。
  • .nupkg ファイル生成のソースとなるディレクトリツリー内の lib の下に、ターゲットプラットフォームを表す名前(TPM:Target Platform Moniker)のディレクトリを切り、そこに .dll と、IntelliSense 用の XML ドキュメントコメントファイルを入れます(XML 入れなくてもかまいません)
  • 依存先のパッケージがある場合、.nuspec/package/dependencies 要素の下に、TPM ごとに <group targetFramework="{TPM}"> 要素を対象の TPM ごとに記述します。この子要素として、依存先のパッケージを <dependency id="{パッケージの名前}" version="{バージョン}" /> といった具合に書いていきます。
  • .NET Standard 用の TPM を含む場合、サポートしていない古いバージョンの NuGet でよくわからないエラーが出ることがないように、/package/metadata 要素に minClientVersion="2.12" と記述します(これ、docs.nuget.org に書いてませんね……)
参考までに、MsgPack for CLI の .nuspec ファイルはこちら
ちょっと文字ばかりでわかりづらいでしょうか。実際に展開した .nupkgtree /F した結果はこんな感じです。
C:.
│  MsgPack.Cli.nuspec
│  [Content_Types].xml
│
├─lib
│  ├─MonoAndroid10
│  │      MsgPack.dll
│  │      MsgPack.xml
│  │
│  ├─net35-client
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─net45
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─net46
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─sl5
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─windowsphone8
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  └─Xamarin.iOS10
│          MsgPack.dll
│          MsgPack.XML
│
├─package
│  └─services
│      └─metadata
│          └─core-properties
│                  d502c190230a45328393d9c274cf792e.psmdcp
│
└─_rels
        .rels
ターゲットモニカについては、公式ドキュメント を参照してください。
Xamarin についてはちょっとわかりづらいですが、上記の例の記述で動くはずです。なお、monotouch が旧形式の Xamarin iOS、Xamarin.iOS が Unified app 版になります(Xamarin のドキュメント)。なお、旧形式については、Xamarin iOS の 10.0 からサポートされていません
さて、実際に作る際には、以下のようにするといいでしょう。
  • 実際に対応している OSS パッケージのディレクトリ構造や .nuspec ファイルを参考にする。私は JSON.NET や CoreFX のパッケージを参考にしました。
  • ローカルのディレクトリに配置して、動作を確認する。なお、v3 の場合、配置先のディレクトリを nuget init しておき、nuget add でパッケージを追加する必要があります。また、前に試した時、ローカルディレクトリは Windows でしかうまく動作しませんでしたが、最近は変わったかもしれません。
それでは、happy hacking!

.NET Fringe Japan 2016でしゃべりました

少し間が空いてしまいましたが、先週末、.NET Fringe Japanというイベントで(珍しく)しゃべってきました。当日のスライドはこちら

内容としては、まずは準備運動よろしく F# の話から始まり、C# の言語機能への提案方法まさかの .NET じゃない話Xamarin ソースの見方の Deep DiveOSS 開発のためになる話 と続いていき、日が沈むとともに IL を追加する方法の話 に行ったときにはどうなるかと思いましたが、最後は地に足の着いた deep dive でうまく終わりました。いや、松井さん最高です。正直あまり運営のお手伝いはできなかったのですが、色々な人の助けもあり、成功に終わったのかなと思います。いやー、みなさんコンテンツもしゃべりも上手で、そちらの方でも勉強になりました。

セッションスケジュール的には、ちょっと詰め込みすぎでしたかね。ただ、初回ということで、こんなマニアックな会で本当に人が集まるのか、という懸念もあり、1 トラックでまずはやってみようという話になったのでした。雨で、かつ色々と楽しそうな裏番組があった中で 100 人近い来場者があったので、次回は 2 トラックくらいにできるのかもしれません。

さて、私も好き勝手しゃべったわけですが、荒井さんのセッションの質問とか、懇親会での話とかを考えると、そもそもマルチプラットフォーム対応の NuGet パッケージの作り方を普通に説明すればよかったのでは、と気づいたので、一筆書こうと思います。そんなの日本語で話しても需要がないだろうと思っていたのですが、あそこはそういうことをする人たちの集まりなのでした :)

あと、あのメンツはどう見ても亜流とかいう生易しいものじゃなくて過激派グループだと思います

2016年6月24日金曜日

DispatchProxyのPRを送ってみた

前回の続きです。

というわけで、.NET Core の、System.Reflection.DispatchProxy が返すオブジェクトからなぜかプロパティとイベントが取れない問題があって、さすがにこれはどうかなと思ったので、PRを送ってみたので、その顛末でも書きます。ほとんどポエムです。

問題の解説

そもそも、

IFoo foo = DispatchProxy.Create<IFoo, SomeProxy>();
foo.Bar = "ABC";

みたいなコードがコンパイルされ、動作するのに、

foo.GetType().GetRuntimeProperty("Bar");

nullを返すのはなぜでしょうか?
それは、fooオブジェクトの実態である動的に生成された型が、プロパティ(やイベント)のアクセッサーメソッドのみを実装してプロパティやイベントを宣言していないからです。
前回の記事をよく見ると、

動的に生成したクラスに、インターフェイスが宣言しているすべてのメソッドを宣言します。

と書いてありますね。 そう、DispatchProxy.Create<T, TProxy>()は動的に生成した型にアクセッサーメソッドを実装するだけで、それらが属するプロパティやイベントを型で宣言させないのです(もう少し内部的な話をすると、動的に生成されたプロキシ型のメタデータテーブルにプロパティとイベントのエントリを書き出さないということです)。
しかもなんと、インターフェイスを実装した型では、そのインターフェイスのプロパティやイベントのアクセッサーメソッドさえ宣言されていれば、プロパティやイベントは宣言しなくても動作するのです(これが動作するのが正しいかどうかという話はありそうですが)。
C# が出力するのはアクセッサーメソッド呼び出しのILですからちゃんとコンパイルでき、かつ動作するのですが、プロパティやイベントが宣言されていないので、リフレクションでプロパティやイベントそのもののメタデータは取れないというわけです。

ポエム

これだとちょっと困るので、DispatchProxyGenerator の中で、プロキシ型にインターフェイスのプロパティとイベントを宣言させるようにしたPRを送ったよという話です。
ちなみに、diffを見ながらじゃないとついていくのがつらいと思います。

第0の関門:どう実装するか

TypeBuilder.DefineProperty()DefineEvent()ですね。あと、アクセッサーメソッドをPropertyBuilderEventBuilderに設定する必要があります。
なぜかこの辺のAPIは頭に入っているので、実装はあっさり終わりました。

第1関門:設計意図

意気揚々と直しましたが、そういえば、そもそもこれって意図的にプロパティやイベントを実装していないのかもしれません。
なので、とりあえずissueをあげて様子を見ました。
きっとRTM前で忙しいでしょうし、ということで、10日くらい待ちました。
で、音沙汰もないうちにリリース1週間前になり、よく見ると依存先のバージョンがrc3からrc4になってるし、これはもう1.1向けの開発モードになってそうだなということで、「何か問題あるかな? なさそうなら実装持ってるからPRできるよ」って聞いてみたところ、「急いで入れる必要ないよね? 実際に直し始めたら色々聞きたくなると思うけど、他にやることあるからちょっと待ってー」って言われて、「ていうか直したんだったらもうPR投げていいよー」と言われたので、PRすることにしました。

第2の関門:ビルドと単体テスト

PRを出すには、当然単体テストを実行しなければなりません(追加のテストは既に書いたものとします)。
ところが、corefxのプロジェクトは少々特殊なproject.jsonになっているので、素直にビルドできません。
corefxのドキュメントを読めばいいのですが、1.0 RC2 時点で、Windows環境でビルドするには以下のようにします。Visual Studio(ライセンス的に問題がない限り、無料のCommunity Editionでも大丈夫のはずです)が必要です。

  1. Visual Studio 2013の MSBuild へのパスが通っているコマンドプロンプトを開きます(「開発者コマンドプロンプト」とかそういうやつです。ちなみに、私はVS 2015用でやりましたが、問題はありませんでした)。
  2. corefxのルートにある、init-tools.cmdを実行します。
  3. 修正したコードのテストプロジェクトのディレクトリに移動し、msbuild /t:BuildAndTestを実行します。

ちゃんとドキュメントを読んでいればなんてことない話でした。

あと、実は初めてのPRだったのでちょっと戸惑ったり、CLAへの署名とかもありましたが、特に躓きはありませんでした。

第3の関門:レビュー

主にvarの使い方でダメ出しをくらいました。「型が明確な場合を除きvarを使わない」とありますが、これは「コンストラクター呼び出しの結果を割り当てる場合を除き」くらいのニュアンスだったようです。
他には、

  • 変数の宣言位置(実はリファクタリングしたときの修正漏れ)
  • ifの本体の改行位置(普段は中括弧必須派なので、うっかり)

などのポカミスが多かったです。正直手間を取らせて申し訳ない。
そして、次の関門。

第4の関門:MethodInfo.Equalsの罠

MethodInfo.Equalsは.NET Core だと参照比較として実装されているから、MethodInfoDictionaryのキーにしちゃダメよー」というありがたい突っ込みをいただきました。
(.NET メタプログラミング界隈の人たちが青ざめる姿が見えた気がしました)
MetadataToken比較するようなEqualityComparer作ればいいと思うYo」とのアドバイスをもらったので、それはそもそも.NET Coreに入っているべきではと思いつつ、それを英語で記述するよりは C# でコードを書く方が早いので書きました。確かに、メタデータテーブルのインデックスであるMetadataTokenを比較するのは確実で、かつ高速な手段ですしね。

第5の関門:netstandard1.3

はい。MemberInfo.MetadataTokenはnetstandard1.5のAPIなのです(.NET Framework側には昔からありますが)。System.Reflection.DispatchProxyは1.3用のライブラリなので、MetadataTokenは使えません。
なので、愚直に名前とパラメーターの型を比較する実装にしました。

第6の関門:ジェネリクス

意気揚々とプッシュしたEqualityComparer<MethodInfo>実装について、「この比較だとジェネリック型引数見てないからダメじゃね?」という真っ当な指摘を受けました。その通りですね。
ついでに「ECMAがどうかわからないけど、静的メソッドかインスタンスメソッドかも比較しないとまずいんじゃないかなー」と言われたので、ECMA-335を見てみました。
そのものずばりのところはないのですが、「I.8.6.1.6 Signature Matching」を見る限り、

  • 静的メソッドかインスタンスメソッドは比較すべき
  • 戻り値の型も比較すべき
  • 呼び出し規約(CallingConvention)も比較すべき

と判断し、これの真偽(実はいらないんじゃないか)を確かめるよりこの通り実装した方が早いので、実装しました(DispatchProxy.Create()をループの中で呼び出して遅いとかいう人はいないでしょうし、そもそも動的コード生成やJITコンパイルに比べたら大したことないでしょうし)。

第7の関門:raise

ここで、「そういえば、イベントのaddメソッドとremoveメソッドは割り当ててるけど、raiseメソッドも割り当てないとダメじゃないかな」という発言が出ました。こいつ、気付きやがった……。同時に「じゃあotherはどうする?」って聞き返そうかと思いましたが、.NET Coreにotherメソッドを割り当てるためのAPIないし、0x20歳を過ぎた大人なので藪蛇になることは言わないことにしました。

とはいえ、C#コンパイラにraiseメソッドを出力させる方法はありません。つまり、テストコードで動的型生成をしないと行けないということです。それは正直ちょっとやりすぎかなーと思った(この後保守するのが面倒になるんじゃないかなと思った)ので、「ほんとにやる?」って返信して、寝ました。もう丑三つ時回ってましたし。

翌日、仕事から帰ってきたところ、ノーレスだったので、四の五の言わずにやれや、ということだと判断し、あきらめて実装しました。実装はECMA-335を見ながらやったのですが、raiseメソッドのシグネチャはvoid raise_xxx(Event e)でなければならないとか書いてありつつ、Eventが何なのかは一切説明がありませんでした。ただ、普通に考えればイベントハンドラーに渡すデータでよかろうということで、適当にEventArgsを渡すようなメソッドを生成させました(これを書いていて思ったのですが、イベントの型は任意のデリゲートになり得るで、EventArgsではないような気はしますね。じゃあ何なのかというと謎ですが、テストでコード生成例外は起きてないし、所詮テストコードの動作の話なので良しとしましょう)。

あと、このためにテストプロジェクトからSystem.Reflection.Emitパッケージへの参照を増やす必要がありました。

第8の関門:dotnet-bot

10日間も間を開けたのは失敗でした。そう、1.1開発モードにシフトしているので、依存先パッケージのバージョンがガンガンアップデートされていきます(いつの間にかrc4betaになってました)。
そう、raise_メソッドのテストのためにテストプロジェクトのproject.jsonをいじっているので、botによって依存先パッケージのバージョンが書き換わると、修正がコンフリクトするのです。具体的には、変更をプッシュして「コンフリクトがないからマージできるよ」ってGitHubに言われても、数時間のうちにコンフリクト状態になってしまう状態でした。
ところが、レビューしてくれた人たち以外は忙しいのか何なのか、LGTMが増えず、マージされません。
コンフリクトのまま放置していもレビューしてくれなさそうなので、仕事から帰ったらマージして再プッシュとかしてみたものの、毎日再プッシュされてもレビューする側は嫌だと思うので、「毎日project.jsonマージしなおして上げなおしたらうざいと思うんで、レビューできる状態になったら教えて。そのときまとめて再プッシュするから」と言ったら、「いや、もう何も言わないってことはいいってことでしょ」と言われ、マージされたわけです。

結論

  • corefxの「varは控えめ」は本当に控えめ。
  • さすがにレビューはしっかりやってる
  • project.jsonいじると頻繁にマージコンフリクトするから気を付けよう

感想としては結構楽しかったです(レビューコメントの英語も勉強になりましたし)。

余談

レビューしてくれた人、日本人っぽい名前だったのですが、日本時間の23:00(シアトルは7:00)台にレスくれるので、早起きなのか、日本にいる人が夜中に対応してくれてたのかがよくわからず。
いずれにせよ、深夜早朝に丁寧な対応ありがとうございました。

DispatchProxy

.NET Core には、System.Reflection.DispatchProxy という型が追加されています。
こいつが何かというと、インターフェイスベースの AOP を実現するプロキシを生成するための仕組みです(Java の Proxy に近いといえばわかる人もいるでしょうか)。
DispatchProxy を使用して特定のインターフェイスのプロキシを作成すると、返されたオブジェクトに対するメソッド呼び出しに対する割り込みがかけられるようになります。

この説明でピンと来る人はいないでしょうから、もう少し丁寧に説明していきます。

使い方

まず、割り込み処理を実行するプロキシクラスを作ります。これは、System.Reflection.DispatchProxy 抽象クラスを継承したクラスです。
基底型である DispatchProxy クラスには、以下のような抽象メソッドが定義されています。

protected abstract object Invoke(MethodInfo targetMethod, object[] args);

どう見ても、メソッド呼び出しに割り込んで、処理を実装しろと言っていますね。この中で、

  • 割り込み処理を行う
  • 本来のディスパッチ先のメソッドを呼び出す
  • メソッド呼び出しの戻り値を返す(targetMethod の戻り値型が void なら null で OK)

などをすればOKです。
後は、やりたいことに応じて、このプロキシにプロパティやメソッドを生やしてください。
ただし、このプロキシ型はpublicでなければならず、さらにpublicなデフォルトコンストラクターが必要です(後述しますが、つらい)。
そして、DispatchProxy.Create<T, TProxy>() 静的メソッドで、プロキシ型のインスタンスを作成します。このとき、任意のインターフェイス型と、先ほど作成したプロキシ型を指定します。

IMyModel obj = DispatchProxy.Create<IMyModel, MyAopProxy>();

この戻り値のobjのメンバーを呼び出すと、MyAopProxyInvoke(MethodInfo,object[])が呼び出されます。やった。

さっそく例を見てみましょう。
昔から、AOPと言えばトランザクション管理とログ出力と相場が決まっています。
たとえば、IMyModelというインターフェイスと、実装であるMyModelがあり、呼び出しのトレースを出力する TracingAopProxy を作るとしましょう。
まず、プロキシクラスを以下のように記述します。追加のプロパティはAPからは呼んでほしくないので、internalにしてます。

public sealed class TracingAopProxy : DispatchProxy
{
    // トレース出力先
    private TraceSource _trace;
    // 実際の処理を行うオブジェクト。
    private object _model;
    // インターフェイスメソッドと
    private Dictionary<MethodInfo, MethodInfo> _dispatchTable;

    public TracingAopProxy() { }

    // コンストラクターが引数を宣言できないので、初期化メソッドを用意。
    internal void Initialize(Type interfaceType, object model, string sourceName)
    {
        _model = model;
        _trace = new TraceSource(sourceName);
        _dispatchTable = new Dictionary<MethodInfo, MethodInfo>(new MethodInfoEqualityComparer());
        foreach (var method in interfaceType.GetRuntimeMethods())
        {
            // .NET Core には GetInterfaceMap() が実装されていないので、頑張って比較。
            // 厳密にやるなら、ReturnType と CallingConvention も。
            // インターフェイスの明示的な実装とかあるので、public メソッドかどうかは問わない。
            var target = model.GetType().GetRuntimeMethods().Single( m =>
                             m.Name == method.Name
                             && m.GetGenericArguments().SequenceEqual(method.GetGenericArguments())
                             && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(method.GetParameters().Select(p => p.ParameterType))
                         );
            _dispatchTable.Add(method, target);
        }
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        _trace.TraceEvent(TraceEventType.Verbose, "Enter: {0}", targetMethod);
        try
        {
            var result = _dispatchTable[targetMethod].Invoke(_model, args);
            _trace.TraceEvent(TraceEventType.Verbose, "Success: {0}", targetMethod);
            return result;
        }
        catch(TargetInvocationException ex)
        {
            _trace.TraceEvent(TraceEventType.Error, "Error: {0}", ex.InnerException);
            throw;
        }
    }

    // .NET Core では MethodInfo.Equals が参照比較で、うまく比較できないことがあるので必要
    private class MethodInfoEqualityComparer : EqualityComparer<MethodInfo>
    {
         public MethodInfoEqualityComparer() { }

         public override bool Equals(MethodInfo left, MethodInfo right)
         {
              return left?.MetadataToken == right?.MetadataToken;
         }

         public override int GetHashCode(MethodInfo obj)
         {
              return (obj?.MetadataToken.GetHashCode()).GetValueOrDefault();
         }
    }
}

で、以下のようなラッパーメソッドを作ってあげればOKです。

public static T WithTrace<T, TImpl>(TImpl model)
    where TImpl : T
{
    var proxy = DispatchProxy.Create<T, TracingProxy>();
    ((TracingProxy)(object)proxy).Initialize(typeof(T), model, "the.source");
    return proxy;
}

ここでのポイントは、DispatchProxy.Create<T, TProxy>() の戻り値の型は T 型ですが、同時に TProxy の派生クラスでもあるということです。
そのため、TProxy 型にキャストすることで、追加の初期化処理を呼び出すことができます(上記の例では、C# コンパイラーをごまかすために、いったんobjectにキャストしています。理由は本筋ではないので割愛します)。

内部動作の説明

DispatchProxy.Create<T, TProxy>() は、ざっくりいうと以下のような処理をやってくれます(他にも細かいことをいろいろやっていますので、興味のある方はソースを見るといいでしょう)。
1. TProxy クラスを継承したプロキシクラスを動的に生成します。
2. 動的に生成したクラスに、指定したインターフェイス(T型)を実装させます。
3. 動的に生成したクラスに、インターフェイスが宣言しているすべてのメソッドを宣言します。
4. 現在のメソッド情報をMethodInfoとして取得するILを出力します。
5. 実引数を格納するobject[]をインスタンス化するILを出力します。
6. 実引数をobject[]につめるILを出力します。
7. Invoke(MethodInfo, object[])を呼び出すILを出力します。
8. メソッドが戻り値を持つ場合、Invoke(MethodInfo, object[])の戻り値をキャストして戻します。

注意点

  • プロキシ型はpublicである必要があります。ライブラリやフレームワークを作るお仕事の人は「まじかよ……」と思ったかと思いますが、そう思った方は我慢せずにIssueを投げてみるといいと思います(自分は、後述するように他にもっと気になったことがあったので投げてないです)
  • 巷のよくある AOP の仕組みと同様に、インスタンスメソッドだけに割り込めます。
  • プロキシ型のInvoke(MethodInfo, object[])の実装によりますが、上記の例のように素直に実装してしまうと、呼び出し先で発生した例外がTargetInvocationExceptionになったりして透過的な割り込みではなくなります。気を付けましょう。式木か何かでMethodInfo.Invokeせずに呼び出すとか。
  • DispatchProxy.Create() で返されたオブジェクトに対してリフレクションを実行した場合、なぜかインターフェイスが実装しているはずのプロパティとイベントの情報が取れません。
    たとえば、
public interface IFoo
{
    string Bar { get; set; }
}

というインターフェイスに対して、

IFoo foo = DispatchProxy.Create<IFoo, TracingAopProxy());

とすると、

foo.Bar = "A";

というコードは(もちろん)コンパイルするし動作するにもかかわらず、

foo.GetType().GetRuntimeProperty("Bar")

nullを返します。なので、このオブジェクトをリフレクションベースのツールやライブラリに渡すと、うまく動作しないかもしれません。
対処方は今のところありません(次回に続く)

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 さんです。

2012年6月21日木曜日

MessagePack for CLI

ずいぶんと長い間書いていなかった気がします……
さて、github で週末ちまちま書いていた MessagePack for CLI が、公式のリポジトリに取り込まれました。びっくりです。

MessagePack とは

相互運用可能で、コンパクトバイナリ形式のオブジェクトシリアル化フォーマットまたはそのライブラリです。詳しくは 公式サイト なり、 @frsyuki さんのブログなどを参照してください。
今回はそれの .NET 用の実装を作った(ている)ということになります。

.NET 用の実装について

.NET 用の実装は実は 3 代目だったりします。初代は @neuecc さんの blog のエントリで使用されているもの。わりとざっくりとした実装で、まだまだパフォーマンスチューニングの余地がある状態でした。
2 代目は NuGet で MessagePack for C# で出てくるもの。こちらは去年の春に出てきたもので、IL 何かを使って高速化されてます。ちなみに、オブジェクトは Map(Dictionary)としてシリアル化されます。パフォーマンスはかなり高く、場合によっては protobuf-net 以上になるようです。
3 代目が for CLI で、こちらが私が作ったものになります。パフォーマンスなどについては後述します。説明についてはつたない英語ですが wiki があります

MessagePack for CLI という名前の理由

よく誤解されてるんですが、.NET 系の言語は、CLS(共通言語仕様)に準拠しているライブラリを作れば、CLI (共通言語基盤)上で動作する他の言語でも実行できます(.NET の FCL(Framework Class Library)も Mono の FCL も大部分は C# で記述されていますが、VB や F# から問題なく呼び出せているはずです)。なので、「for C#」もないかなぁと思って作り始めました。そのため、MessagePack for CLI は VB、F#、C++/CLI、PowerShell、IronRuby、IronPython などからも呼び出すことができます(FAQ に補足あります)。
そして、MessagePack がうれしいのって、どちらかというと Web の世界の方なのかと思い、Silverlight にも対応した方が良いよねとか(始めたときは 2010 年の夏ですしね)、Mono を除外する理由はないよねといったところで、for CLI にしました。たとえば、MessagePack-RPC のサーバーとして Mono を載せた Linux がいてもいいはずです。今は Unity もありますしね。

MessagePack-RPC

MessagePack-RPC というのもありまして、こちらは MessagePack を使用してシリアル化したメッセージを呼び出すフレームワークです。MessagePack-RPC for CLI もあります。Silverlight でも簡単な動作確認を行っています。これも詳細については 公式サイトwikiをご覧ください。

入手方法

プレリリース版として NuGet に上がっていますので、ご利用ください。MessagePack for CLI というアイコンに気合の感じられない方です。

パフォーマンス

前述の@neuecc さんの blog のエントリについて、自宅のマシン(Windows 7、.NET 4、x86 リリースビルド、Core i7 3770 Quad Core 3.4GHz)で測ったところ、以下のような感じです。なお、XmlSerializer についてはそもそもシリアル化できていないので割愛しました。また、逆シリアル化後のチェックロジックで DateTime 型の比較については UTC に変換し、マイクロ秒以下を無視して比較しています(端から UTC にすると JSON.NET がうまくいかなかったりとか、マイクロ秒以下が MessagePack では合わないとかあるので)。
Serialize BinaryFormatter
00:00:07.3377291
53MB
Serialize DataContractSerializer
00:00:02.2492265
149MB
Serialize DataContractSerializer Binary
00:00:01.5054592
78MB
Serialize DataContractJsonSerializer
00:00:02.7829966
47MB
Serialize DataContractJsonSerializer Binary
00:00:02.7813589
75MB
Serialize NetDataContractSerializer
00:00:12.2300278
183MB
Serialize NetDataContractSerializer Binary
00:00:10.5490878
99MB
Serialize Formatter - Protocol Buffers
00:00:00.5668601
13MB
Serialize AutoMessagePackSerializer`1 - MessagePack
00:00:00.6150166
11MB
Serialize JsonSerializer - JSON.NET
00:00:08.5988969
38MB
Serialize JsonSerializer - JSON.NET BSON
00:00:12.5111917
44MB

Deserialize BinaryFormatter
00:01:07.9430564
Deserialize XmlSerializer
00:00:02.8090175
Deserialize DataContractSerializer
00:00:06.9147897
Deserialize DataContractSerializer Binary
00:00:03.7894125
Deserialize DataContractJsonSerializer
00:00:12.3970537
Deserialize DataContractJsonSerializer Binary
00:00:06.2072323
Deserialize NetDataContractSerializer
00:00:09.4501272
Deserialize NetDataContractSerializer Binary
00:00:07.3537588
Deserialize Formatter - Protocol Buffers
00:00:01.0971126
Deserialize AutoMessagePackSerializer`1 - MessagePack
00:00:05.0464446
Deserialize JsonSerializer - JSON.NET
00:00:12.3115143
Deserialize JsonSerializer - JSON.NET BSON
00:00:12.2420603
protobuf-net にシリアル化のパフォーマンスは肉薄できているかなと思います。サイズは MessagePack の方が有利です。
ところが、逆シリアル化は protobuf-net よりもずいぶん時間がかかっています。ここは改善の余地があります(ちなみに、2 代目のパフォーマンスをはかると、そもそもシリアル化と逆シリアル化でパフォーマンスの差がここまで開いていないので、フォーマットの差というよりは MessagePack for CLI の逆シリアル化の実装に問題があると考えています。というか、Map モードで for CLI 使った場合、逆シリアル化では 2 代目の方が数倍速いです)。

はまりどころ

使ってみるとハマるかもしれない仕様についていくつか言っておきます(使ってみてがっかりされてしまうのは嫌ですし)。相互運用性のため(他にも C++、Java、D、Erlang、Ruby、Python、Haskell、Go などなど色々あります)、次の仕様上の制限があることをご承知おきくださいませ。
  • 文字列は既定では utf-8(BOM なし)になります。Encoding を指定すれば上書き可能です(相互運用性はなくなります)。
  • DateTime/DateTimeOffset は UTC の Unix Epoc になります。マイクロ秒以下の値とタイムゾーン情報は失われます。 これを回避するには、独自の MessagePackSerializer を定義するとか、ToString("o") するとかする必要があります。
  • オブジェクトは配列としてシリアル化されます。Map ではありません。そのため、長期間保存する場合、うっかりソースコードで型のフィールドの順番(MessagePackMemberAttribute カスタム属性で順序を明示的に指定できます)を変えたりすると逆シリアル化できなくなります。これは SerializationContext.SerializationMethod オプションで変更できます。

FAQ

NuGet は?
公開済みです。ただし、現在のところプレリリース版です。
今後の予定は?
とりあえず品質の安定、逆シリアル化のパフォーマンスの向上、Silverlight/Phone での自動テストの追加、WinRT サポート、IDL サポートくらいでしょうか。
IDL は言語(C# や VB)ごとに Haskell 版のパーサーに Pull Request するのがいいのか、自前で作るのが良いのかちょっと悩んでいます。GHC なんかは簡単にインストールできますが、.NET 以外入れたくない、という方もいるでしょうし。
それ以外はノーアイディアなので、気付いた点などあればご連絡いただけると嬉しいです。(Unpacker の効率改善案とか)
WinRT 対応予定は?
ちまちまやってますが、テストをどうしたものか、といったところです。
PowerShell/IronRuby/IronPython から使いづらいんですが
『.NET のクラスライブラリ設計』を訳しておきながら恐縮ですが、動的言語についてはあまり考えずに API を設計していますので、使いづらい面もあるかと思います(型引数とかそもそもの API のスタイルとか)。IronRuby、IronPython については公式リポジトリの Ruby 版や Python 版を試してみてください。もちろん、何か改善案があれば、ぜひ教えていただけると嬉しいです。
F# の API が気に食わない。お前これ名古屋でも F# 対応って言えるの?
なごやこわい。MessagePack for F# を作るなら今です。早くしないと出張中(ただし世間は GW)のホテルで帰ってきたら先を越されてへこむとかいう事態になりかねません。もちろん、改善で対応できるかもしれませんし、改善案があればいただけると嬉しいです。
戻り値が IEnumerable<T> じゃないのはおかしい
MessagePack for LINQ や MessagePack-Rx とか作ればいいじゃない。
Mono for Android や Mono Touch では動きますか?
レポートお待ちしております m(_ _)m
2012/6/21 Code Contract 周りでそもそもビルドできない問題があるようですが、次の MfA ではビルドできそうです。もちろん、ご意見等歓迎です。

2011年7月27日水曜日

例外の分類

元ネタは、C#チームのEric Lippert氏のブログの数年前の投稿です。

例外処理というと、「とりあえずキャッチしとけ」とか、逆に「とりあえずキャッチするな(集約例外ハンドラーで対処しろ)」とか言われたりします。かと思えば、「プロなんだから自分の頭で考えろ」とか、「業務フロー次第でしょ」などと千尋の谷に突き落とされたりもします。
極論を言えばそうなのですが、もう少しガイドラインとなるものは無いのか、ということでEricのブログの内容をかいつまんで紹介します。

Ericは、例外は4種類に分類できると言っています[1]

例外の4分類(Eric Lippert氏による)
種類説明特徴対処方法
致命的な例外(fatal exception)
プロセスに深刻な問題が発生し、今にも死にそうな状態にある場合に発生する例外。 プログラマーの過失ではない[2]
復旧は無理(finallyを実行することで悪化する場合も)
何もしない、場合によっては Environment.​FailFast Out​Of​Memory​ExceptionStack​Overflow​ExceptionExecution​Engine​Exceptionなど
うっかり例外(boneheaded exception) プログラマーのうっかりミスにより発生する例外。 プログラマーの過失、
本来、テスト・デバッグの段階ですべて洗い出すべきもの
何もしない(正確に言えばバグとして対処する) Argument​Exception系(後述の頭の痛い例外の場合を除く)、Null​Reference​ExceptionIndex​Out​Of​Range​ExceptionInvalid​Cast​ExceptionDevide​Zero​Exception など。
頭の痛い例外(vexing exception) 失敗する可能性を前提とすべきAPIが、失敗時にスローしてくる例外。
APIの設計ミス
  1. 例外をスローしないAPIを探す(TryParseとかTryGetValue[3]とか)
  2. Try系のメソッドがない場合は例外をキャッチする(もちろん、APIのドキュメントで明示された例外を選択的にキャッチする)
(厄介なメソッドの例)ParseXml​Convert.​VerifyXXXType​Converter.​ConvertXXX [4] など。
外因性の例外(exogenous exception) プログラムの外部の環境に原因のある例外。 避けようがないが、対処しようがある場合もある。 適宜対処する[5] IO​ExceptionSocket​ExceptionWeb​ExceptionExternal​Exception(コードによって例外の種別が異なるので注意、ADO.NET の例外を含む)、Unauthorized​Access​ExceptionSecurity​Exception など。

つまり、処理しなければいけない例外というのは、本来は外因性の例外なわけです。

Windowsなど最近の環境はマルチタスク環境なので、アプリケーションとは別のプロセスがなんらかの処理をしています。ユーザーがうっかりファイルを操作すること だってあり得ます。なので、直前のif文で存在をチェックしたファイルが、その直後にFileStreamを開くまでの間に削除された、なんてことは十分あり得ます。また、ファイルがロックされているかどうかは実際に開いてみるまでわかりませんし(そもそも チェックするAPIがないのですが、あったとしても、if文でチェックした後に他のプロセスがロックする可能性は残ります)、アクセス権も別のプロセス(Explorerとか)が変更するかもしれません。別のマシンにあるプログラムと通信している場合は、障害を起こす可能性のあるものが途中に多数介在しますので、基本的にエラーが起こるものとして考えないといけません。

何かお役にたてば。

[1] InterruptionExceptionCancellationExceptionなど、この4つに入らないものもあります。

[2] CLRやJITの実装の隙間を探るようなコードを書くような方については、もちろんその人の過失。

[3] ただし、IDictionary<TKey,TValue>のインデクサが例外をスローするのは、値の型が値型の場合(たとえばInt32)に、値として0が格納されているのか、要素がなかったからdefault(Int32)(= 0)を返したのかを区別できないので、設計ミスではないと思います。
参照型とNullable<T>をいっしょくたにしたジェネリック型制約がかけられないジェネリックの設計ミスとも言えなくもありませんが、IDictionary<string, int?>としなければならないといったルールが追加され、APIの敷居が上がってしまうように思います。

[4] CanConvertXXXメソッドもあるのですが、おそらくは同じであろうロジックを2回呼び出すというのは設計として微妙だと思っています。WPFのIValueConverterは値を変換できない場合にDependencyProperty.UnsetValueを返すというコントラクトでいい感じですね。

[5] 結局千尋の谷に突き落としていますが、ここだけを考えれば良いということでどうかご容赦を。