TypeBuilderを使って、既存のクラスにメソッドを生やす

継承可能なDynamicObjectを作ろうとしたが挫折中 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/8672

なところで、Xamarin.Forms からイベントを探索しているのはリフレクションを使っているなあ、ということが分かったので、じゃあ元のクラスに仮のメソッドをつけて無視すればいいのでは?と考えました。

XAML で、Clicked イベントがついていた時に

<Button Text="Click me" Clicked="Button_Clicked" />

↓なように、動的に Button_Click を生やしたいわけです。

public class MainPage : Xamarin.Forms.ContentPage
{
    private void Button_Click(object sender, EventArgs e)	
    {
    }
}

本来ならば、コンパイル時にメソッド名が決まっていればよいので、普通に ContentPage を継承してあらかじめコードで Button_Click を付けておけばよいのですが、XamlPreview のように XAML だけを送る場合は動的に Button_Click に作りたいのですよね。

実行時に Button_Click メソッドを作る

TypeBuilder.CreateType メソッド (System.Reflection.Emit)
https://msdn.microsoft.com/ja-jp/library/system.reflection.emit.typebuilder.createtype(v=vs.110).aspx

というのがあって実行時にクラスが作れます。
最初に断っておきますが、これは Xamarin.iOS では動きません。動かないので、XamlPreview の目的に達しないのですが、まあ、忘備録的に記録しておくというとで。Xamarin.Android 上では動くし、どうやら .NET Core 上でも動くので他にも応用が利きそうかなと。

public class PageGenerator : IPageGenerator
{
    public Type Create()
    {
        var assemblyName = new AssemblyName("dynamicassembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
        var tb = moduleBuilder.DefineType("DynamicContentPage", TypeAttributes.Class, typeof(Xamarin.Forms.ContentPage));
            
        MethodBuilder meth = tb.DefineMethod(
            "Button_Clicked",
            MethodAttributes.Public,
            typeof(void),
            new Type[] { typeof(object), typeof(EventArgs) });
        ILGenerator methIL = meth.GetILGenerator();
        methIL.Emit(OpCodes.Ret);

        Type t = tb.CreateType();
        return t;
    }
}

IPageGenerator を定義しているのは、PCL プロジェクト内では動かないので DependencyService.Get するためです。
動的にアセンブリを作って、クラスを作成しています。AssemblyBuilderAccess.Run を指定するとメモリ上で動きますね。DefineType メソッドで継承先に Xamarin.Forms.ContentPage を指定します。
メソッド名は、DefineMethod で指定して、中身は GetILGenerator で作るという感じ。

こうすると、ContentPage を継承した DynamicContentPage というクラスが動的にできます。
これをメインのほうで、

var tg = DependencyService.Get<IPageGenerator>();
Type t = tg.Create();
ContentPage page = Activator.CreateInstance(t) as ContentPage;

とすれば、無事 ContentPage オブジェクトとして使えます。DynamicContentPage クラス自体は動的に作ったものなので、プログラムを書いているときには存在しません。なので、当然インテリセンスとかは効きません。

これ、ビルド時にアセンブリに落として参照設定すれば、F# の TypeProvider と同じ動きになるんじゃないかなと思うんですが、どうなんでしょう?

メソッドの中身を Expression.CompileToMethod で書ける?

動的に作成した Button_Clicked ですが、IL なので、ちょっと面倒くさい。

ILGenerator methIL = meth.GetILGenerator();
methIL.Emit(OpCodes.Ret);

じゃあ、Expression を使って、Expression.Lambda で既存のメソッドを呼び出せば楽じゃないか?と思って作ったのがこれ。CompileToMethod を使うと IL を吐き出してくれます。

var mi = typeof(PageGenerator).GetMethod("Button_Clicked", new Type[] { typeof(object), typeof(EventArgs) });
var arg1 = Expression.Parameter(typeof(object));
var arg2 = Expression.Parameter(typeof(EventArgs));
var lambda = Expression.Lambda(Expression.Call(mi, arg1, arg2), arg1, arg2);
lambda.CompileToMethod(meth);

でも、なぜか Xamarin.Android 上では実行時に CompileToMethod でダンマリになるという感じでうまくいかない。
何かしたいというと、動的に作った Button_Clicked から、既存のコードを呼び出して、それをさらに Desktop のクライアントに送れたら便利かなと思った次第なのですが、CompileToMethod がうまくいかない。

Xamarin.iOS では動かない

iOS では動的コードが動かないので、Xamarin.iOS では動きません。ビルドは通るけど、実行時に AssemblyBuilder.DefineDynamicAssembly で落ちます。

iOS で Emit 絡みがダメなのが分かったので、XamlPreview では使えないのだけど、デスクトップ側でアセンブリができるのだから、F# の TypeProvider っぽいのができないかなぁと思案中…なので続く。

カテゴリー: 開発, Xamarin パーマリンク