$30 off During Our Annual Pro Sale. View Details »

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例

Yoshifumi Kawai
September 16, 2015
61

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例

Yoshifumi Kawai

September 16, 2015
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. Metaprogramming Universe in C#
    実例に見るILからRoslynまでの活用例
    2015/09/16 Metro.cs #1
    Yoshifumi Kawai - @neuecc

    View Slide

  2. Self Introduction
    @仕事
    株式会社グラニ 取締役CTO
    最先端C#によるサーバー/クライアント大統一ゲーム開発
    @個人活動
    Microsoft MVP for .NET(C#)
    Web http://neue.cc/
    Twitter @neuecc
    UniRx - Reactive Extensions for Unity https://github.com/neuecc/UniRx

    View Slide

  3. Realworld Metaprogramming

    View Slide

  4. PhotonWire
    リアルタイム通信用フレームワークを作成中
    近々GitHubに公開予定
    Photon Serverという通信ミドルウェアの上に乗った何か
    特にUnityとの強いインテグレーション
    Typed Asynchronous RPC Layer for Photon Server + Unity
    複数サーバー間やサーバー-クライアント間のリアルタイム通信
    これの実装を例に、C#でのメタプログラミングが実際のプログラ
    ム構築にどう活用されるのかを紹介します

    View Slide

  5. Client <-> Server(Inspired by SignalR)
    .NET/Unity向けのクライアントを自動生成して型付きで通信
    完全非同期、戻り値はIObservableで生成(UniRxでハンドリング可能)
    [Hub(0)]
    public class MyHub : Hub
    {
    [Operation(0)]
    public int Sum(int x, int y)
    {
    return x + y;
    }
    }
    var peer = new ObservablePhotonPeer(ConnectionProtocol.Tcp);
    peer.CreateTypedHub().Invoke.SumAsync(5, 10)
    .Subscribe(sum => { }); // 15

    View Slide

  6. Server <-> Server(Inspired by Orleans)
    [Hub(0)]
    public class MyServerHub : ServerHub
    {
    [Operation(0)]
    public virtual async Task SumAsync(int x, int y)
    {
    return x + y;
    }
    }
    var results = await PeerManager.GetServerHubContext()
    .Peers.Single.SumAsync(1, 10);
    メソッド呼び出しをネットワーク経由
    の呼び出しに動的に置換してサーバー
    間通信をメソッド呼び出しで表現

    View Slide

  7. True Isomorphic Architecture
    Everything is Asynchronous, Everything in the C#
    Rxとasync/awaitで末端のクライアントから接続先のサーバー、更
    に分散して繋がったサーバークラスタまでを透過的に一気通貫し
    て結びつける

    View Slide

  8. Expression Tree

    View Slide

  9. Expression Tree
    Code as Data
    用途は
    1. 式木を辿って何らかの情報を作る(EFのSQL文生成など)
    => LINQ to BigQuery
    => https://github.com/neuecc/LINQ-to-BigQuery/
    2. デリゲートを動的生成してメソッド実行の高速化
    => 今回はこっちの話
    Expression> expr = (x, y) => x + y;

    View Slide

  10. PhotonWire's Execution Process
    [Hub(0)]
    public class MyHub : Hub
    {
    [Operation(0)]
    public int Sum(int x, int y)
    {
    return x + y;
    }
    }
    var peer = new ObservablePhotonPeer(ConnectionProtocol.Tcp);
    peer.CreateTypedHub().Invoke.SumAsync(5, 10)
    .Subscribe(sum => { });
    Hub:0, Operation:0, args = x:5, y:10 という
    情報を(バイナリで)送信
    内部的にはnew MyHub().Sum(5, 10)が呼び出されて結果を
    取得、クライアントに送信している

    View Slide

  11. PhotonWire's Execution Process
    [Hub(0)]
    public class MyHub : Hub
    {
    [Operation(0)]
    public int Sum(int x, int y)
    {
    return x + y;
    }
    }
    var peer = new ObservablePhotonPeer(ConnectionProtocol.Tcp);
    peer.CreateTypedHub().Invoke.SumAsync(5, 10)
    .Subscribe(sum => { });
    Hub:0, Operation:0, args = x:5, y:10 という
    情報を(バイナリで送信)
    内部的にはnew MyHub().Sum(5, 10)が呼び出されて結果を
    取得、クライアントに送信している
    AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(x => x.GetTypes())
    .Where(x => typeof(Hub).IsAssignableFrom(x));
    事前にクラスを走査して対象クラス/メソッドの辞書
    を作っておく
    事前にクラスを走査して対象クラス/メソッドの辞書
    を作っておく
    var instance = Activator.CreateInstance(type);
    var result = methodInfo.Invoke(instance, new object[] { x, y });
    最も単純な動的実行
    ネットワークから来る型情報、メソッド情報を元にして
    動的にクラス生成とメソッド呼び出しを行うには?

    View Slide

  12. Reflection is slow, compile delegate!
    MethodInfoのInvokeは遅い
    最も簡単な動的実行の手法だが、結果は今ひとつ
    動的実行を高速化するにはDelegateを作ってキャッシュする
    // (object[] args) => (object)new X().M((T1)args[0], (T2)args[1])...
    var lambda = Expression.Lambda>(
    Expression.Convert(
    Expression.Call(
    Expression.MemberInit(Expression.New(classType), contextBind),
    methodInfo,
    parameters)
    , typeof(object)),
    contextArg, args);
    this.methodFuncBody = lambda.Compile();
    new MyHub().Sum(5, 10)になるイメージ
    ここで出来上がったDelegateをキャッシュする

    View Slide

  13. Expression Tree is still alive
    Roslyn or Not
    Expression Treeによるデリゲート生成は2015年現在でも第一級で、
    最初に考えるべき手段
    比較的柔軟で、比較的簡単に書けて、標準で搭載されている
    有意義なので積極的に使っていって良い
    ただし使えない局面もある(スライドの後で紹介)ので
    その場合は当然他の手段に譲る

    View Slide

  14. T4(Text Template Transformation Toolkit)

    View Slide

  15. クライアント-サーバー間の通信
    [Hub(0)]
    public class MyHub : Hub
    {
    [Operation(0)]
    public int Sum(int x, int y)
    {
    return x + y;
    }
    }
    var peer = new ObservablePhotonPeer(ConnectionProtocol.Tcp);
    peer.CreateTypedHub().Invoke.SumAsync(5, 10)
    .Subscribe(sum => { }); // 15
    呼び出すクラス名・メソッド名・引数の名
    前・引数の型・戻り値の型をサーバー/クラ
    イアントの双方で合わせなければならない

    View Slide

  16. Share Interface between Server and Client
    XML proto DSL
    Json
    Server Code
    Client Code
    IDL(Interface Definition Language)
    共通定義ファイルからサーバーコード/クライア
    ントコードの雛形を生成することで、サーバー/
    クライアントでのコード手動定義を避けれると
    いう一般的パターン

    View Slide

  17. Share Interface between Server and Client
    XML proto DSL
    Json
    Server Code
    Client Code
    IDL(Interface Definition Language)
    本来のプログラムコードと別に定義するのは
    面倒くさい&ワークフロー的にも煩雑

    View Slide

  18. Generate Client Code from Server Code
    Server Code
    Client Code
    Generate
    [Operation(2)]
    public async Task Echo(string str)
    public IObservable EchoAsync(System.String str)
    {
    byte opCode = 2;
    var parameter = new System.Collections.Generic.Dictionary();
    parameter.Add(ReservedParameterNo.RequestHubId, hubId);
    parameter.Add(0, PhotonSerializer.Serialize(str));
    var __response = peer.OpCustomAsync(opCode, parameter, true)
    .Select(__operationResponse =>
    {
    var __result = __operationResponse[ReservedParameterNo.ResponseId]
    return PhotonSerializer.Deserialize(__result);
    });
    return __response;
    }

    View Slide

  19. .NET DLL is IDL
    サーバー実装からジェネレート
    C#/Visual Studioの支援が効く(使える型などがC#の文法に則る)
    サーバー側を主として、テンプレートではなく完成品から生成
    クライアントは大抵通信を投げるだけなのでカスタマイズ不要
    自動生成に伴うワークフローで手間になる箇所がゼロになる
    Code vs DLL
    Roslynの登場によりC#コードの解析が比較的容易になった
    とはいえアセンブリとして組み上がったDLLのほうが解析は容易
    というわけでデータを読み取りたいだけならDLLから取得する

    View Slide

  20. T4 Text Template Transformation Toolkit
    Visual Studioと統合されたテンプレートエンジン(.tt)
    VSと密結合してVS上で変換プロセスかけたり、テンプレート上で
    EnvDTE(VSの内部構造)を触れたりするのが他にない強さ
    <#@ assembly name="$(SolutionDir)¥Sample¥PhotonWire.Sample.ServerApp¥bin¥Debug¥PhotonWire.Sample.ServerApp.dll" #>
    <#
    var hubs = System.AppDomain.CurrentDomain
    .GetAssemblies()
    .Where(x => x.GetName().Name == assemblyName)
    .SelectMany(x = x.GetTypes())
    .Where(x => x != null);
    .Where(x => SearchBaseHub(x) != null)
    .Where(x => !x.IsAbstract)
    .Where(x => x.GetCustomAttributes(true).All(y => y.GetType().FullName != "PhotonWire.Server.IgnoreOperationAttribute"));
    DLL をファイルロックせずに読みこめる、ふつー
    の.NETのリフレクションでデータ解析してテンプ
    レート出力に必要な構造を作り込める

    View Slide

  21. <# foreach(var method in contract.Server.Methods) { #>
    public <#= WithIObservable(method.ReturnTypeName) #> <#= method.MethodName #><#= useAsyncSuffix ?
    {
    byte opCode = <#= method.OperationCode #>;
    var parameter = new System.Collections.Generic.Dictionary();
    parameter.Add(ReservedParameterNo.RequestHubId, hubId);
    <# for(var i = 0; i < method.Parameter.Length; i++) { #>
    parameter.Add(<#= i #>, PhotonSerializer.Serialize(<#= method.Parameter[i].Name #>));
    <# } #>
    var __response = peer.OpCustomAsync(opCode, parameter, true)
    .Select(__operationResponse =>
    {
    var __result = __operationResponse[ReservedParameterNo.ResponseId];
    return PhotonSerializer.Deserialize<<#= method.ReturnTypeName #>>(__result);
    });
    return (observeOnMainThread) ? __response.ObserveOn(<#= mainthreadSchedulerInstance #>) : __r
    }
    <# } #> <# #>は一行に収めると比較的テンプレートが汚れない
    左端に置くと見たままにインデントが綺麗に出力される
    文法はふつーのテンプレート言語で、特段悪くはな
    い、Razorなどは汎用テンプレートの記述には向いて
    ないので、これで全然良い

    View Slide

  22. ILGenerator(Reflection.Emit)

    View Slide

  23. サーバー間通信の手触り
    [Hub(0)]
    public class MyServerHub : ServerHub
    {
    [Operation(0)]
    public virtual async Task SumAsync(int x, int y)
    {
    return x + y;
    }
    }
    var results = await PeerManager.GetServerHubContext()
    .Peers.Single.SumAsync(1, 10);
    対象の型のメソッドを直接呼
    べるような手触り

    View Slide

  24. 動的な実行コード変換
    [Hub(0)]
    public class MyServerHub : ServerHub
    {
    [Operation(0)]
    public virtual async Task SumAsync(int x, int y)
    {
    return x + y;
    }
    }
    var results = await PeerManager.GetServerHubContext()
    .Peers.Single.SumAsync(1, 10);
    .SendOperationRequestAsync(peer, methodOpCode: 0, arguments: new object[] { 1, 10 })
    実際は直接メソッド呼び出しではな
    く上のようなネットワーク通信呼び
    出しに変換されている

    View Slide

  25. RPC Next Generation
    コード生成 vs 動的プロキシ
    基本的に動的プロキシのほうが利用者に手間がなくて良い
    を指定するだけで他になにの準備もいらないのだから
    コード生成は依存関係が切り離せるというメリットがある
    サーバー側DLLの参照が不要、そもそもTaskがない環境(Unityとか)に向けて生成したり
    というわけでクライアントはコード生成、サーバー間は動的プロキシを採用
    .NET、ネットワーク間のメソッドを透過的に、う、頭が……
    昔話のトラウマ、通信など時間のかかるものを同期で隠蔽したのも悪かった
    現代には非同期を表明するTaskが存在しているので進歩している
    もちろん、そのサポートとしてのasync/awaitも

    View Slide

  26. ILGenerator generator = methodBuilder.GetILGenerator();
    generator.DeclareLocal(typeof(object[]));
    // Get Context and peer
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldfld, contextField); // context
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldfld, targetPeerField); // peer
    // OpCode
    var opCode = methodInfo.GetCustomAttribute().OperationCode;
    generator.Emit(OpCodes.Ldc_I4, (int)opCode);
    // new[]{ }
    generator.Emit(OpCodes.Ldc_I4, parameters.Length);
    generator.Emit(OpCodes.Newarr, typeof(object));
    generator.Emit(OpCodes.Stloc_0);
    // object[]
    for (var i = 0; i < paramTypes.Length; i++)
    {
    generator.Emit(OpCodes.Ldloc_0);
    generator.Emit(OpCodes.Ldc_I4, i);
    generator.Emit(OpCodes.Ldarg, i + 1);
    generator.Emit(OpCodes.Box, paramTypes[i]);
    generator.Emit(OpCodes.Stelem_Ref);
    }
    // Call method
    generator.Emit(OpCodes.Ldloc_0);
    generator.Emit(OpCodes.Callvirt, invokeMethod);
    generator.Emit(OpCodes.Ret);
    .SendOperationRequestAsync(peer, methodOpCode: 0, arguments: new object[] { 1, 10 })
    ハイパーIL手書きマン

    View Slide

  27. Reflection.Emit vs Expression Tree
    エクストリームIL手書きマン
    Expression Treeがどれだけ天国だか分かる
    しかしExpression Treeは静的メソッド/デリゲートしか生成できない
    今回はクラス(のインスタンスメソッド)を丸ごと置き換える必要がある
    それが出来るのは現状Reflection.Emitだけ
    置き換えのための制限
    インターフェースメソッドかクラスの場合virtualでなければならない
    と、いうわけでPhotonWireのサーバー間用メソッドはvirtual必須
    もしvirtualじゃなければ例外
    あとついでに非同期なので戻り値はTaskかTaskじゃないとダメ、そうじゃなきゃ例外
    public virtual async Task SumAsync(int x, int y)

    View Slide

  28. Roslyn CodeAnalyzer

    View Slide

  29. 起動時に起こるエラー
    [Hub(0)]
    public class MyServerHub : ServerHub
    {
    [Operation(0)]
    public virtual async Task Sum(int x, int y)
    {
    return x + y;
    }
    [Operation(0)]
    public virtual async Task Sum2(int x, int y)
    {
    return x + y;
    }
    } OperationIDが被ってるとダメなんだっ
    てー、ダメな場合なるべく早い段階で伝え
    る(フェイルファースト)ため起動時にエ
    ラーダイアログ出すんだってー

    View Slide

  30. Hub作成時のルール
    Hubには必ずHubAttributeを付ける必要がありその
    HubIdはプロジェクト中で一意である必要がありパブリッ
    クメソッドにはOperationAttributeを付ける必要がありそ
    のOperationIdはクラスのメソッド中で一意である必要が
    ある。ServerHubを継承したクラスにはHubAttribute
    を付ける必要がありメソッドOperationAttributeを付ける
    必要があり全てのpublicインスタンスメソッドの戻り値は
    TaskもしくはTaskでvirtualでなければならない

    View Slide

  31. FFFFFFFFFFFFFFFFFFFFFFFFFF
    FFFFFFFFFFFFFFFFFFFFFFFFFF
    FFFFFFFFFUUUUUUUUUUUUUUUUU
    UUUUUUUUUUUUUUUUUUUUUUUUUU
    UUUUUUUUUUUUUUUUUUUUUUUUU-

    View Slide

  32. ルールがある
    Hubには必ずHubAttributeを付ける必要がありその
    HubIdはプロジェクト中で一意である必要がありパブリッ
    クメソッドにはOperationAttributeを付ける必要がありそ
    のOperationIdはクラスのメソッド中で一意である必要が
    ある。ServerHubを継承したクラスにはHubAttribute
    を付ける必要がありメソッドOperationAttributeを付ける
    必要があり全てのpublicインスタンスメソッドの戻り値は
    TaskもしくはTaskでvirtualでなければならない
    例えばC#で普通に書いてて同じ名前のクラ
    スはダメ、同じ名前のメソッドがあるとダ
    メ、とかそういったのと同じ話。そんなに
    特殊なことではない。でもAttributeで制御
    したりしているので、実行時にならないと
    そのチェックができない。のが問題。

    View Slide

  33. Fucking convention over configuration
    独自制約 is 辛い
    習熟しなければ問答無用の実行時エラー
    Analyzerでコンパイルエラーに変換
    リアルタイムに分かる
    Attributeついてないとエラーとか
    virtualついてないとエラーとか
    IDが被ってるとエラーとか

    View Slide

  34. Code Aware Libraries
    利用法をVisual Studioが教えてくれる
    マニュアルを読み込んで習熟しなくても大丈夫
    間違えてもリアルタイムにエラーを出してくれる
    明らかに実行時エラーになるものは記述時に弾かれる
    Analyzer = Compiler Extension
    ライブラリやフレームワークに合わせて拡張されたコンパイラ
    「設定より規約」や「Code First」的なものにも効果ありそう
    + 事前コード生成(CodeFix)が現在のRoslynで可能
    コンパイル時生成も可能になれば真のコンパイラ拡張になるが……

    View Slide

  35. Mono.Cecil

    View Slide

  36. PhotonWire.HubInvoker
    専用WPFアプリ
    サーバーのHubをリストアップ
    メソッドを実際に叩いて結果確認
    デバッグに有用
    複数枚立ち上げて複数接続確認
    Unityなどの重いクライアントを立ち
    あげなくても、サーバーのメソッド
    を直接実行できるのでブレークポイ
    ントで止めてデバッグなど

    View Slide

  37. Assembly.LoadFrom
    解析のため対象のClass/Methodを読み込む
    ド直球の手段はAssembly.LoadFrom("hoge.dll").GetTypes()
    お手軽ベンリ動く、しかしアプリ終了までDLLをロックする
    HubInvokerを起動中はアプリのリビルドが出来ない = 使いものにならない
    ので不採用
    ファイルロック回避のために
    別のAppDomainを作りShadowCopyを有効にし、そこにDLLを読むという手法
    別AppDomainで読むと扱いの面倒さが飛躍的に増大する
    ので不採用
    もしくは.Load(File.ReadAllBytes("hoge.dll"))で読み込む
    まぁまぁうまくいくが、依存する型を解決しないとTypeLoadExceptionで死ぬので地味に面倒
    ので不採用

    View Slide

  38. Mono.Cecil
    Analyze, Generate, Modify
    https://github.com/jbevain/cecil
    JB Evain先生作
    作者は色々あって現在はMicrosoftの中の人(Visual Studio Tools for Unity)
    DLLを読み込んで解析して変更して保存、つまり中身を書き換えれる
    PostSharpやUnityなど色々なところの裏方で幅広く使われている
    CCI(Microsoft Common Compiler Infrastructure)ってのもあるけど、一般的には
    Cecilが使われる(CCIは些か複雑なので……Cecilは使うのは割と簡単)
    今回はDLLをファイルロックなしで
    解析(対象クラス/メソッド/引数を
    取り出す)したいという用途で使用、
    なので読み込みのみ

    View Slide

  39. var resolver = new DefaultAssemblyResolver();
    resolver.AddSearchDirectory(Path.GetDirectoryName(dllPath));
    var readerParam = new ReaderParameters
    {
    ReadingMode = ReadingMode.Immediate,
    ReadSymbols = false,
    AssemblyResolver = resolver
    };
    var asm = AssemblyDefinition.ReadAssembly(dllPath, readerParam);
    var hubTypes = asm.MainModule.GetTypes()
    .Where(x => SearchBaseHub(x) != null)
    .Where(x => !x.IsAbstract)
    .Where(x => x.CustomAttributes.Any(y => y.AttributeType.FullName == "PhotonWire.Server.HubAttribute"));
    対象DLLが別のDLLのクラスを参照
    しているなどがある場合に設定して
    おくと読み込めるようになる
    概ね.NETのリフレクションっぽいようなふんい
    きで書ける(Type = TypeDefinitionであったり、
    似て非なるものを扱うことにはなる)ので
    IntelliSenseと付き合えばすぐに扱えるはず

    View Slide

  40. Conclusion

    View Slide

  41. 今回触れていないトピック
    CodeDOM
    RealProxy
    Castle.DynamicProxy
    DLR
    まぁ基本的にほとんどオワコンなのでいいでしょう(そうか?)

    View Slide

  42. まとめ
    C# Everything
    クライアントの末端からサーバークラスタまで透過的に繋がる
    C#フレンドリーな手触り(人道性)を重視、もちろん、性能も
    PhotonWire早く公開したいお
    やりすぎない
    目的を第一に考えることと、結果その中に採用される手段は少なけれ
    ば少ないほどいい(という点でPhotonWireが多めなのはいくない)
    とはいえ必要になる場合はあるわけで手札は多いに越したことはない
    かつ、一個一個は別にそんな難しいわけじゃない、大事なのは組み合わせと発想
    今回の例が、それぞれのテクニックの使いみちへの参考になれば!

    View Slide