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

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

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

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

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

2015/09/16 Metro.cs #1Yoshifumi Kawai - @neuecc

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

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

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

Realworld Metaprogramming

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

PhotonWire

リアルタイム通信用フレームワークを作成中

近々GitHubに公開予定

Photon Serverという通信ミドルウェアの上に乗った何か

特にUnityとの強いインテグレーション

Typed Asynchronous RPC Layer for Photon Server + Unity

複数サーバー間やサーバー-クライアント間のリアルタイム通信

これの実装を例に、C#でのメタプログラミングが実際のプログラ

ム構築にどう活用されるのかを紹介します

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

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

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

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

メソッド呼び出しをネットワーク経由の呼び出しに動的に置換してサーバー間通信をメソッド呼び出しで表現

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

True Isomorphic Architecture

Everything is Asynchronous, Everything in the C#

Rxとasync/awaitで末端のクライアントから接続先のサーバー、更

に分散して繋がったサーバークラスタまでを透過的に一気通貫し

て結びつける

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

Expression Tree

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

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;

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

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

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

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

最も単純な動的実行

ネットワークから来る型情報、メソッド情報を元にして動的にクラス生成とメソッド呼び出しを行うには?

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

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

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

Expression Tree is still alive

Roslyn or Not

Expression Treeによるデリゲート生成は2015年現在でも第一級で、

最初に考えるべき手段

比較的柔軟で、比較的簡単に書けて、標準で搭載されている

有意義なので積極的に使っていって良い

ただし使えない局面もある(スライドの後で紹介)ので

その場合は当然他の手段に譲る

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

T4(Text Template Transformation Toolkit)

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

クライアント-サーバー間の通信

[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

呼び出すクラス名・メソッド名・引数の名前・引数の型・戻り値の型をサーバー/クライアントの双方で合わせなければならない

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

Share Interface between Server and Client

XML proto DSLJson

Server Code

Client Code

IDL(Interface Definition Language)

共通定義ファイルからサーバーコード/クライアントコードの雛形を生成することで、サーバー/クライアントでのコード手動定義を避けれると

いう一般的パターン

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

Share Interface between Server and Client

XML proto DSLJson

Server Code

Client Code

IDL(Interface Definition Language)

本来のプログラムコードと別に定義するのは面倒くさい&ワークフロー的にも煩雑

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

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;

}

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

.NET DLL is IDL

サーバー実装からジェネレート

C#/Visual Studioの支援が効く(使える型などがC#の文法に則る)

サーバー側を主として、テンプレートではなく完成品から生成

クライアントは大抵通信を投げるだけなのでカスタマイズ不要

自動生成に伴うワークフローで手間になる箇所がゼロになる

Code vs DLL

Roslynの登場によりC#コードの解析が比較的容易になった

とはいえアセンブリとして組み上がったDLLのほうが解析は容易

というわけでデータを読み取りたいだけならDLLから取得する

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

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のリフレクションでデータ解析してテンプ

レート出力に必要な構造を作り込める

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

<# 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 #>) : __response;}

<# } #> <# #>は一行に収めると比較的テンプレートが汚れない左端に置くと見たままにインデントが綺麗に出力される

文法はふつーのテンプレート言語で、特段悪くはない、Razorなどは汎用テンプレートの記述には向いて

ないので、これで全然良い

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

ILGenerator(Reflection.Emit)

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

サーバー間通信の手触り

[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);

対象の型のメソッドを直接呼べるような手触り

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

動的な実行コード変換

[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 })

実際は直接メソッド呼び出しではなく上のようなネットワーク通信呼び

出しに変換されている

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

RPC Next Generation

コード生成 vs 動的プロキシ

基本的に動的プロキシのほうが利用者に手間がなくて良い

<T>を指定するだけで他になにの準備もいらないのだから

コード生成は依存関係が切り離せるというメリットがある

サーバー側DLLの参照が不要、そもそもTaskがない環境(Unityとか)に向けて生成したり

というわけでクライアントはコード生成、サーバー間は動的プロキシを採用

.NET、ネットワーク間のメソッドを透過的に、う、頭が……

昔話のトラウマ、通信など時間のかかるものを同期で隠蔽したのも悪かった

現代には非同期を表明するTask<T>が存在しているので進歩している

もちろん、そのサポートとしてのasync/awaitも

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

ILGenerator generator = methodBuilder.GetILGenerator();generator.DeclareLocal(typeof(object[]));

// Get Context and peergenerator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldfld, contextField); // contextgenerator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldfld, targetPeerField); // peer

// OpCodevar 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 methodgenerator.Emit(OpCodes.Ldloc_0);generator.Emit(OpCodes.Callvirt, invokeMethod);

generator.Emit(OpCodes.Ret);

.SendOperationRequestAsync(peer, methodOpCode: 0, arguments: new object[] { 1, 10 })

ハイパーIL手書きマン

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

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)

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

Roslyn CodeAnalyzer

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

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

ラーダイアログ出すんだってー

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

Hub作成時のルール

Hub<T>には必ずHubAttributeを付ける必要がありその

HubIdはプロジェクト中で一意である必要がありパブリッ

クメソッドにはOperationAttributeを付ける必要がありそ

のOperationIdはクラスのメソッド中で一意である必要が

ある。ServerHub<T>を継承したクラスにはHubAttribute

を付ける必要がありメソッドOperationAttributeを付ける

必要があり全てのpublicインスタンスメソッドの戻り値は

TaskもしくはTask<T>でvirtualでなければならない

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

FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU-

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

ルールがある

Hub<T>には必ずHubAttributeを付ける必要がありその

HubIdはプロジェクト中で一意である必要がありパブリッ

クメソッドにはOperationAttributeを付ける必要がありそ

のOperationIdはクラスのメソッド中で一意である必要が

ある。ServerHub<T>を継承したクラスにはHubAttribute

を付ける必要がありメソッドOperationAttributeを付ける

必要があり全てのpublicインスタンスメソッドの戻り値は

TaskもしくはTask<T>でvirtualでなければならない

例えばC#で普通に書いてて同じ名前のクラスはダメ、同じ名前のメソッドがあるとダメ、とかそういったのと同じ話。そんなに特殊なことではない。でもAttributeで制御したりしているので、実行時にならないとそのチェックができない。のが問題。

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

Fucking convention over configuration

独自制約 is 辛い

習熟しなければ問答無用の実行時エラー

Analyzerでコンパイルエラーに変換

リアルタイムに分かる

Attributeついてないとエラーとかvirtualついてないとエラーとか

IDが被ってるとエラーとか

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

Code Aware Libraries

利用法をVisual Studioが教えてくれる

マニュアルを読み込んで習熟しなくても大丈夫

間違えてもリアルタイムにエラーを出してくれる

明らかに実行時エラーになるものは記述時に弾かれる

Analyzer = Compiler Extension

ライブラリやフレームワークに合わせて拡張されたコンパイラ

「設定より規約」や「Code First」的なものにも効果ありそう

+事前コード生成(CodeFix)が現在のRoslynで可能

コンパイル時生成も可能になれば真のコンパイラ拡張になるが……

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

Mono.Cecil

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

PhotonWire.HubInvoker

専用WPFアプリ

サーバーのHubをリストアップ

メソッドを実際に叩いて結果確認

デバッグに有用

複数枚立ち上げて複数接続確認

Unityなどの重いクライアントを立ち

あげなくても、サーバーのメソッド

を直接実行できるのでブレークポイ

ントで止めてデバッグなど

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

Assembly.LoadFrom

解析のため対象のClass/Methodを読み込む

ド直球の手段はAssembly.LoadFrom("hoge.dll").GetTypes()

お手軽ベンリ動く、しかしアプリ終了までDLLをロックする

HubInvokerを起動中はアプリのリビルドが出来ない= 使いものにならない

ので不採用

ファイルロック回避のために

別のAppDomainを作りShadowCopyを有効にし、そこにDLLを読むという手法

別AppDomainで読むと扱いの面倒さが飛躍的に増大する

ので不採用

もしくは.Load(File.ReadAllBytes("hoge.dll"))で読み込む

まぁまぁうまくいくが、依存する型を解決しないとTypeLoadExceptionで死ぬので地味に面倒ので不採用

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

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

なので読み込みのみ

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

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

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

Conclusion

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

今回触れていないトピック

CodeDOM

RealProxy

Castle.DynamicProxy

DLR

まぁ基本的にほとんどオワコンなのでいいでしょう(そうか?)

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

まとめ

C# Everything

クライアントの末端からサーバークラスタまで透過的に繋がる

C#フレンドリーな手触り(人道性)を重視、もちろん、性能も

PhotonWire早く公開したいお

やりすぎない目的を第一に考えることと、結果その中に採用される手段は少なければ少ないほどいい(という点でPhotonWireが多めなのはいくない)

とはいえ必要になる場合はあるわけで手札は多いに越したことはない

かつ、一個一個は別にそんな難しいわけじゃない、大事なのは組み合わせと発想

今回の例が、それぞれのテクニックの使いみちへの参考になれば!