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

More Related Content

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