Advertisement

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

CTO at Cysharp
Sep. 16, 2015
Advertisement

More Related Content

Slideshows for you(20)

Viewers also liked(17)

Advertisement

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

Advertisement

Recently uploaded(20)

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

  1. Metaprogramming Universe in C# 実例に見るILからRoslynまでの活用例 2015/09/16 Metro.cs #1 Yoshifumi Kawai - @neuecc
  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
  3. Realworld Metaprogramming
  4. PhotonWire リアルタイム通信用フレームワークを作成中 近々GitHubに公開予定 Photon Serverという通信ミドルウェアの上に乗った何か 特にUnityとの強いインテグレーション Typed Asynchronous RPC Layer for Photon Server + Unity 複数サーバー間やサーバー-クライアント間のリアルタイム通信 これの実装を例に、C#でのメタプログラミングが実際のプログラ ム構築にどう活用されるのかを紹介します
  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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); // 15
  6. 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); メソッド呼び出しをネットワーク経由 の呼び出しに動的に置換してサーバー 間通信をメソッド呼び出しで表現
  7. True Isomorphic Architecture Everything is Asynchronous, Everything in the C# Rxとasync/awaitで末端のクライアントから接続先のサーバー、更 に分散して繋がったサーバークラスタまでを透過的に一気通貫し て結びつける
  8. Expression Tree
  9. 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;
  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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); Hub:0, Operation:0, args = x:5, y:10 という 情報を(バイナリで)送信 内部的にはnew MyHub().Sum(5, 10)が呼び出されて結果を 取得、クライアントに送信している
  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<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 }); 最も単純な動的実行 ネットワークから来る型情報、メソッド情報を元にして 動的にクラス生成とメソッド呼び出しを行うには?
  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<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をキャッシュする
  13. Expression Tree is still alive Roslyn or Not Expression Treeによるデリゲート生成は2015年現在でも第一級で、 最初に考えるべき手段 比較的柔軟で、比較的簡単に書けて、標準で搭載されている 有意義なので積極的に使っていって良い ただし使えない局面もある(スライドの後で紹介)ので その場合は当然他の手段に譲る
  14. T4(Text Template Transformation Toolkit)
  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<MyHub>().Invoke.SumAsync(5, 10) .Subscribe(sum => { }); // 15 呼び出すクラス名・メソッド名・引数の名 前・引数の型・戻り値の型をサーバー/クラ イアントの双方で合わせなければならない
  16. Share Interface between Server and Client XML proto DSLJson Server Code Client Code IDL(Interface Definition Language) 共通定義ファイルからサーバーコード/クライア ントコードの雛形を生成することで、サーバー/ クライアントでのコード手動定義を避けれると いう一般的パターン
  17. Share Interface between Server and Client XML proto DSLJson Server Code Client Code IDL(Interface Definition Language) 本来のプログラムコードと別に定義するのは 面倒くさい&ワークフロー的にも煩雑
  18. 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; }
  19. .NET DLL is IDL サーバー実装からジェネレート C#/Visual Studioの支援が効く(使える型などがC#の文法に則る) サーバー側を主として、テンプレートではなく完成品から生成 クライアントは大抵通信を投げるだけなのでカスタマイズ不要 自動生成に伴うワークフローで手間になる箇所がゼロになる Code vs DLL Roslynの登場によりC#コードの解析が比較的容易になった とはいえアセンブリとして組み上がったDLLのほうが解析は容易 というわけでデータを読み取りたいだけならDLLから取得する
  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のリフレクションでデータ解析してテンプ レート出力に必要な構造を作り込める
  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<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などは汎用テンプレートの記述には向いて ないので、これで全然良い
  22. ILGenerator(Reflection.Emit)
  23. サーバー間通信の手触り [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); 対象の型のメソッドを直接呼 べるような手触り
  24. 動的な実行コード変換 [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 }) 実際は直接メソッド呼び出しではな く上のようなネットワーク通信呼び 出しに変換されている
  25. RPC Next Generation コード生成 vs 動的プロキシ 基本的に動的プロキシのほうが利用者に手間がなくて良い <T>を指定するだけで他になにの準備もいらないのだから コード生成は依存関係が切り離せるというメリットがある サーバー側DLLの参照が不要、そもそもTaskがない環境(Unityとか)に向けて生成したり というわけでクライアントはコード生成、サーバー間は動的プロキシを採用 .NET、ネットワーク間のメソッドを透過的に、う、頭が…… 昔話のトラウマ、通信など時間のかかるものを同期で隠蔽したのも悪かった 現代には非同期を表明するTask<T>が存在しているので進歩している もちろん、そのサポートとしてのasync/awaitも
  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<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手書きマン
  27. 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)
  28. Roslyn CodeAnalyzer
  29. 起動時に起こるエラー [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が被ってるとダメなんだっ てー、ダメな場合なるべく早い段階で伝え る(フェイルファースト)ため起動時にエ ラーダイアログ出すんだってー
  30. Hub作成時のルール Hub<T>には必ずHubAttributeを付ける必要がありその HubIdはプロジェクト中で一意である必要がありパブリッ クメソッドにはOperationAttributeを付ける必要がありそ のOperationIdはクラスのメソッド中で一意である必要が ある。ServerHub<T>を継承したクラスにはHubAttribute を付ける必要がありメソッドOperationAttributeを付ける 必要があり全てのpublicインスタンスメソッドの戻り値は TaskもしくはTask<T>でvirtualでなければならない
  31. FFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFUUUUUUUUUUUUUUUUU UUUUUUUUUUUUUUUUUUUUUUUUUU UUUUUUUUUUUUUUUUUUUUUUUUU-
  32. ルールがある Hub<T>には必ずHubAttributeを付ける必要がありその HubIdはプロジェクト中で一意である必要がありパブリッ クメソッドにはOperationAttributeを付ける必要がありそ のOperationIdはクラスのメソッド中で一意である必要が ある。ServerHub<T>を継承したクラスにはHubAttribute を付ける必要がありメソッドOperationAttributeを付ける 必要があり全てのpublicインスタンスメソッドの戻り値は TaskもしくはTask<T>でvirtualでなければならない 例えばC#で普通に書いてて同じ名前のクラ スはダメ、同じ名前のメソッドがあるとダ メ、とかそういったのと同じ話。そんなに 特殊なことではない。でもAttributeで制御 したりしているので、実行時にならないと そのチェックができない。のが問題。
  33. Fucking convention over configuration 独自制約 is 辛い 習熟しなければ問答無用の実行時エラー Analyzerでコンパイルエラーに変換 リアルタイムに分かる Attributeついてないとエラーとか virtualついてないとエラーとか IDが被ってるとエラーとか
  34. Code Aware Libraries 利用法をVisual Studioが教えてくれる マニュアルを読み込んで習熟しなくても大丈夫 間違えてもリアルタイムにエラーを出してくれる 明らかに実行時エラーになるものは記述時に弾かれる Analyzer = Compiler Extension ライブラリやフレームワークに合わせて拡張されたコンパイラ 「設定より規約」や「Code First」的なものにも効果ありそう + 事前コード生成(CodeFix)が現在のRoslynで可能 コンパイル時生成も可能になれば真のコンパイラ拡張になるが……
  35. Mono.Cecil
  36. PhotonWire.HubInvoker 専用WPFアプリ サーバーのHubをリストアップ メソッドを実際に叩いて結果確認 デバッグに有用 複数枚立ち上げて複数接続確認 Unityなどの重いクライアントを立ち あげなくても、サーバーのメソッド を直接実行できるのでブレークポイ ントで止めてデバッグなど
  37. Assembly.LoadFrom 解析のため対象のClass/Methodを読み込む ド直球の手段はAssembly.LoadFrom("hoge.dll").GetTypes() お手軽ベンリ動く、しかしアプリ終了までDLLをロックする HubInvokerを起動中はアプリのリビルドが出来ない = 使いものにならない ので不採用 ファイルロック回避のために 別のAppDomainを作りShadowCopyを有効にし、そこにDLLを読むという手法 別AppDomainで読むと扱いの面倒さが飛躍的に増大する ので不採用 もしくは.Load(File.ReadAllBytes("hoge.dll"))で読み込む まぁまぁうまくいくが、依存する型を解決しないとTypeLoadExceptionで死ぬので地味に面倒 ので不採用
  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をファイルロックなしで 解析(対象クラス/メソッド/引数を 取り出す)したいという用途で使用、 なので読み込みのみ
  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と付き合えばすぐに扱えるはず
  40. Conclusion
  41. 今回触れていないトピック CodeDOM RealProxy Castle.DynamicProxy DLR まぁ基本的にほとんどオワコンなのでいいでしょう(そうか?)
  42. まとめ C# Everything クライアントの末端からサーバークラスタまで透過的に繋がる C#フレンドリーな手触り(人道性)を重視、もちろん、性能も PhotonWire早く公開したいお やりすぎない 目的を第一に考えることと、結果その中に採用される手段は少なけれ ば少ないほどいい(という点でPhotonWireが多めなのはいくない) とはいえ必要になる場合はあるわけで手札は多いに越したことはない かつ、一個一個は別にそんな難しいわけじゃない、大事なのは組み合わせと発想 今回の例が、それぞれのテクニックの使いみちへの参考になれば!
Advertisement