Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Realworld Metaprogramming

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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); メソッド呼び出しをネットワーク経由 の呼び出しに動的に置換してサーバー 間通信をメソッド呼び出しで表現

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Expression Tree

Slide 9

Slide 9 text

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;

Slide 10

Slide 10 text

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)が呼び出されて結果を 取得、クライアントに送信している

Slide 11

Slide 11 text

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 }); 最も単純な動的実行 ネットワークから来る型情報、メソッド情報を元にして 動的にクラス生成とメソッド呼び出しを行うには?

Slide 12

Slide 12 text

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をキャッシュする

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

T4(Text Template Transformation Toolkit)

Slide 15

Slide 15 text

クライアント-サーバー間の通信 [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 呼び出すクラス名・メソッド名・引数の名 前・引数の型・戻り値の型をサーバー/クラ イアントの双方で合わせなければならない

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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; }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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のリフレクションでデータ解析してテンプ レート出力に必要な構造を作り込める

Slide 21

Slide 21 text

<# 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などは汎用テンプレートの記述には向いて ないので、これで全然良い

Slide 22

Slide 22 text

ILGenerator(Reflection.Emit)

Slide 23

Slide 23 text

サーバー間通信の手触り [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); 対象の型のメソッドを直接呼 べるような手触り

Slide 24

Slide 24 text

動的な実行コード変換 [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 }) 実際は直接メソッド呼び出しではな く上のようなネットワーク通信呼び 出しに変換されている

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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手書きマン

Slide 27

Slide 27 text

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)

Slide 28

Slide 28 text

Roslyn CodeAnalyzer

Slide 29

Slide 29 text

起動時に起こるエラー [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が被ってるとダメなんだっ てー、ダメな場合なるべく早い段階で伝え る(フェイルファースト)ため起動時にエ ラーダイアログ出すんだってー

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

FFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFFFFFFFFFFFF FFFFFFFFFUUUUUUUUUUUUUUUUU UUUUUUUUUUUUUUUUUUUUUUUUUU UUUUUUUUUUUUUUUUUUUUUUUUU-

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Mono.Cecil

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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をファイルロックなしで 解析(対象クラス/メソッド/引数を 取り出す)したいという用途で使用、 なので読み込みのみ

Slide 39

Slide 39 text

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と付き合えばすぐに扱えるはず

Slide 40

Slide 40 text

Conclusion

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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