Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Yoshifumi Kawai
September 16, 2015
82

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

Yoshifumi Kawai

September 16, 2015
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. 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
  2. PhotonWire リアルタイム通信用フレームワークを作成中 近々GitHubに公開予定 Photon Serverという通信ミドルウェアの上に乗った何か 特にUnityとの強いインテグレーション Typed Asynchronous RPC Layer

    for Photon Server + Unity 複数サーバー間やサーバー-クライアント間のリアルタイム通信 これの実装を例に、C#でのメタプログラミングが実際のプログラ ム構築にどう活用されるのかを紹介します
  3. 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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); // 15
  4. Server <-> Server(Inspired by Orleans) [Hub(0)] public class MyServerHub :

    ServerHub { [Operation(0)] public virtual async Task<int> SumAsync(int x, int y) { return x + y; } } var results = await PeerManager.GetServerHubContext<MyServerHub>() .Peers.Single.SumAsync(1, 10); メソッド呼び出しをネットワーク経由 の呼び出しに動的に置換してサーバー 間通信をメソッド呼び出しで表現
  5. True Isomorphic Architecture Everything is Asynchronous, Everything in the C#

    Rxとasync/awaitで末端のクライアントから接続先のサーバー、更 に分散して繋がったサーバークラスタまでを透過的に一気通貫し て結びつける
  6. Expression Tree Code as Data 用途は 1. 式木を辿って何らかの情報を作る(EFのSQL文生成など) => LINQ

    to BigQuery => https://github.com/neuecc/LINQ-to-BigQuery/ 2. デリゲートを動的生成してメソッド実行の高速化 => 今回はこっちの話 Expression<Func<int, int, int>> expr = (x, y) => x + y;
  7. 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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); Hub:0, Operation:0, args = x:5, y:10 という 情報を(バイナリで)送信 内部的にはnew MyHub().Sum(5, 10)が呼び出されて結果を 取得、クライアントに送信している
  8. 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<MyHubProxy>().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 }); 最も単純な動的実行 ネットワークから来る型情報、メソッド情報を元にして 動的にクラス生成とメソッド呼び出しを行うには?
  9. Reflection is slow, compile delegate! MethodInfoのInvokeは遅い 最も簡単な動的実行の手法だが、結果は今ひとつ 動的実行を高速化するにはDelegateを作ってキャッシュする // (object[]

    args) => (object)new X().M((T1)args[0], (T2)args[1])... var lambda = Expression.Lambda<Func<OperationContext, object[], object>>( 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をキャッシュする
  10. Expression Tree is still alive Roslyn or Not Expression Treeによるデリゲート生成は2015年現在でも第一級で、

    最初に考えるべき手段 比較的柔軟で、比較的簡単に書けて、標準で搭載されている 有意義なので積極的に使っていって良い ただし使えない局面もある(スライドの後で紹介)ので その場合は当然他の手段に譲る
  11. クライアント-サーバー間の通信 [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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); // 15 呼び出すクラス名・メソッド名・引数の名 前・引数の型・戻り値の型をサーバー/クラ イアントの双方で合わせなければならない
  12. Share Interface between Server and Client XML proto DSL Json

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

    Server Code Client Code IDL(Interface Definition Language) 本来のプログラムコードと別に定義するのは 面倒くさい&ワークフロー的にも煩雑
  14. Generate Client Code from Server Code Server Code Client Code

    Generate [Operation(2)] public async Task<string> Echo(string str) public IObservable<System.String> EchoAsync(System.String str) { byte opCode = 2; var parameter = new System.Collections.Generic.Dictionary<byte, object>(); 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<System.String>(__result); }); return __response; }
  15. 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のリフレクションでデータ解析してテンプ レート出力に必要な構造を作り込める
  16. <# 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<byte, object>(); 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などは汎用テンプレートの記述には向いて ないので、これで全然良い
  17. サーバー間通信の手触り [Hub(0)] public class MyServerHub : ServerHub { [Operation(0)] public

    virtual async Task<int> SumAsync(int x, int y) { return x + y; } } var results = await PeerManager.GetServerHubContext<MyServerHub>() .Peers.Single.SumAsync(1, 10); 対象の型のメソッドを直接呼 べるような手触り
  18. 動的な実行コード変換 [Hub(0)] public class MyServerHub : ServerHub { [Operation(0)] public

    virtual async Task<int> SumAsync(int x, int y) { return x + y; } } var results = await PeerManager.GetServerHubContext<MyServerHub>() .Peers.Single.SumAsync(1, 10); .SendOperationRequestAsync(peer, methodOpCode: 0, arguments: new object[] { 1, 10 }) 実際は直接メソッド呼び出しではな く上のようなネットワーク通信呼び 出しに変換されている
  19. RPC Next Generation コード生成 vs 動的プロキシ 基本的に動的プロキシのほうが利用者に手間がなくて良い <T>を指定するだけで他になにの準備もいらないのだから コード生成は依存関係が切り離せるというメリットがある サーバー側DLLの参照が不要、そもそもTaskがない環境(Unityとか)に向けて生成したり

    というわけでクライアントはコード生成、サーバー間は動的プロキシを採用 .NET、ネットワーク間のメソッドを透過的に、う、頭が…… 昔話のトラウマ、通信など時間のかかるものを同期で隠蔽したのも悪かった 現代には非同期を表明するTask<T>が存在しているので進歩している もちろん、そのサポートとしてのasync/awaitも
  20. 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<OperationAttribute>().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手書きマン
  21. Reflection.Emit vs Expression Tree エクストリームIL手書きマン Expression Treeがどれだけ天国だか分かる しかしExpression Treeは静的メソッド/デリゲートしか生成できない 今回はクラス(のインスタンスメソッド)を丸ごと置き換える必要がある

    それが出来るのは現状Reflection.Emitだけ 置き換えのための制限 インターフェースメソッドかクラスの場合virtualでなければならない と、いうわけでPhotonWireのサーバー間用メソッドはvirtual必須 もしvirtualじゃなければ例外 あとついでに非同期なので戻り値はTaskかTask<T>じゃないとダメ、そうじゃなきゃ例外 public virtual async Task<int> SumAsync(int x, int y)
  22. 起動時に起こるエラー [Hub(0)] public class MyServerHub : ServerHub { [Operation(0)] public

    virtual async Task<int> Sum(int x, int y) { return x + y; } [Operation(0)] public virtual async Task<int> Sum2(int x, int y) { return x + y; } } OperationIDが被ってるとダメなんだっ てー、ダメな場合なるべく早い段階で伝え る(フェイルファースト)ため起動時にエ ラーダイアログ出すんだってー
  23. Code Aware Libraries 利用法をVisual Studioが教えてくれる マニュアルを読み込んで習熟しなくても大丈夫 間違えてもリアルタイムにエラーを出してくれる 明らかに実行時エラーになるものは記述時に弾かれる Analyzer =

    Compiler Extension ライブラリやフレームワークに合わせて拡張されたコンパイラ 「設定より規約」や「Code First」的なものにも効果ありそう + 事前コード生成(CodeFix)が現在のRoslynで可能 コンパイル時生成も可能になれば真のコンパイラ拡張になるが……
  24. 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をファイルロックなしで 解析(対象クラス/メソッド/引数を 取り出す)したいという用途で使用、 なので読み込みのみ
  25. 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と付き合えばすぐに扱えるはず