先日、Expression と DynamicMethod でそれぞれ動的にデリゲートを生成するサンプルを書いてみた。
メタプログラミングの目的がリフレクションの高速化なら、 動的なデリゲート生成で事足りる場合が多い。 今回は一歩進んで、動的にクラスを生成してみたいと思う。 お題は、「WCF のサービスコントラクトから動的にクライアントのクラスを生成する」。
using System; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.ServiceModel; using System.ServiceModel.Channels; namespace BlackMagicSample { // WCF サービスコントラクト [ServiceContract] public interface IGreetingService { [OperationContract] string GoodMorning(string name); [OperationContract] string Hello(string name); [OperationContract] string GoodBye(); [OperationContract] void GoodAfternoon(string name); } // WCF サービス class GreetingService : IGreetingService { public void GoodAfternoon(string name) { Console.WriteLine($"Good afternoon, {name}"); } public string GoodBye() { return "Good bye."; } public string GoodMorning(string name) { return $"Good morning, {name}."; } public string Hello(string name) { return $"Hello, {name}."; } } // このクラスのようなものを実行時に生成する //class GreetingServiceClient : ClientBase<IGreetingService>, IGreetingService //{ // public GreetingServiceClient(Binding binding, EndpointAddress endpointAddress) // : base(binding, endpointAddress) // { // } // // public string GoodMorning(string name) // { // return Channel.GoodMorning(name); // } // // public string Hello(string name) // { // return Channel.Hello(name); // } // // public string GoodBye() // { // return Channel.GoodBye(); // } // // public void GoodAfternoon(string name) // { // Channel.GoodAfternoon(name); // } //} class Program { static void Main(string[] args) { Console.Title = "BlackMagicSample"; // WCF サービスを開始 var address = "net.pipe://localhost/GreetingService"; var binding = new NetNamedPipeBinding(); var host = new ServiceHost(typeof(GreetingService)); host.AddServiceEndpoint( typeof(IGreetingService), binding, address); host.Open(); // WCF サービスを呼び出すクライアントの型を生成 var clientType = GenerateClientType<IGreetingService>(); // WCF サービスを呼び出せるかテスト var client = (IGreetingService)Activator.CreateInstance( clientType, binding, new EndpointAddress(address)); Console.WriteLine(client.GoodMorning("Honda")); Console.WriteLine(client.Hello("Kagawa")); Console.WriteLine(client.GoodBye()); client.GoodAfternoon("Nagatomo"); Console.ReadLine(); // 後始末 ((ClientBase<IGreetingService>)client).Close(); host.Close(); } // WCF サービスを呼び出すクライアントの型を生成する static Type GenerateClientType<T>() where T : class { var interfaceType = typeof(T); var moduleName = $"Client_{interfaceType.Name}"; var clientBaseType = typeof(ClientBase<T>); var clientBaseConstructor = clientBaseType.GetConstructor( bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance, binder: null, types: new[] { typeof(Binding), typeof(EndpointAddress) }, modifiers: null); var channelProperty = clientBaseType.GetProperty( "Channel", BindingFlags.NonPublic | BindingFlags.Instance); var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( name: new AssemblyName(moduleName), access: AssemblyBuilderAccess.RunAndSave); var moduleBuilder = assemblyBuilder.DefineDynamicModule( moduleName, moduleName + ".cs"); // ClientBase<T> 派生クラスを定義 var typeBuilder = moduleBuilder.DefineType( name: moduleName, attr: TypeAttributes.Public, parent: typeof(ClientBase<T>), interfaces: new[] { typeof(T) }); // Biding と EndpointAddress を受け取るコンストラクタを定義 var constructorBuilder = typeBuilder.DefineConstructor( attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, callingConvention: CallingConventions.Standard, parameterTypes: new[] { typeof(Binding), typeof(EndpointAddress) }); var ctorIL = constructorBuilder.GetILGenerator(); ctorIL.Emit(OpCodes.Ldarg_0); ctorIL.Emit(OpCodes.Ldarg_1); ctorIL.Emit(OpCodes.Ldarg_2); ctorIL.Emit(OpCodes.Call, clientBaseConstructor); ctorIL.Emit(OpCodes.Ret); var methods = interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance); foreach (var method in methods) { var parameterTypes = method.GetParameters() .Select(p => p.ParameterType) .ToArray(); var methodBuilder = typeBuilder.DefineMethod( name: method.Name, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final, returnType: method.ReturnType, parameterTypes: parameterTypes); var methodIL = methodBuilder.GetILGenerator(); // Channel プロパティからチャネルを取得 methodIL.Emit(OpCodes.Ldarg_0); methodIL.Emit(OpCodes.Call, channelProperty.GetMethod); // チャネルのメソッドを呼び出す for (var i = 0; i < parameterTypes.Length; i++) { methodIL.Emit(OpCodes.Ldarg, i + 1); } methodIL.Emit(OpCodes.Callvirt, method); methodIL.Emit(OpCodes.Ret); } return typeBuilder.CreateType(); } } }
動的にクラスを生成する場合は、AssemblyBuilder を使う。 AssemblyBuilder を使って、 動的にアセンブリを作り、 動的にモジュールを作り、 動的に型を作り、 動的にメソッドを作る。 メソッドの中身は IL 手書き。 超大変なので、今回も ILSpy を使ってカンニングした。
実行結果は次の通り。
動的に生成したクライアントのクラスを使って、WCF サービスの呼び出しに成功した。 この手のクライアントは .NET Remoting の RealProxy を使っていたけど、 .NET Remoting は .NET Core で使えないし未来もないので、 動的にクラスを生成する手法に乗り換えたいところだ。