AssemblyBuilder を使った動的クラス生成

先日、Expression と DynamicMethod でそれぞれ動的にデリゲートを生成するサンプルを書いてみた。

tnakamura.hatenablog.com

メタプログラミングの目的がリフレクションの高速化なら、 動的なデリゲート生成で事足りる場合が多い。 今回は一歩進んで、動的にクラスを生成してみたいと思う。 お題は、「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 を使ってカンニングした。

実行結果は次の通り。

f:id:griefworker:20180427150106p:plain

動的に生成したクライアントのクラスを使って、WCF サービスの呼び出しに成功した。 この手のクライアントは .NET Remoting の RealProxy を使っていたけど、 .NET Remoting は .NET Core で使えないし未来もないので、 動的にクラスを生成する手法に乗り換えたいところだ。