読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

dynamicとinterfaceによる実行時例外

Twitterを見ていたら、SignalRの開発者@davidfowlさんがこんなツイートをしていました。

彼が発見したというバグを再現するコードは次のようなものです。パッと見、実行できない箇所なんてなさそうに思えます。あまりに信じがたいので実際に試してみました。

using System;

namespace CompilerBug
{
    //--- 基底のインターフェース
    public interface IBar
    {
        void Bar(string name);
    }

    //--- 継承したインターフェース
    public interface IFoo : IBar
    {}
 
    //--- インターフェースの実装
    public class Foo : IFoo
    {
        public void Bar(string name)
        {
            Console.WriteLine("Hello World");
        }
    }

    //--- エントリークラス
    public class Program
    {
        //--- OK
        static void WorkingDynamic()
        {
            Foo foo = new Foo();
            dynamic d = "test";
            foo.Bar(d);
        }

        //--- NG
        static void BrokenDynamic()
        {
            IFoo foo = new Foo();
            dynamic d = "test";
            foo.Bar(d);
        }

        //--- エントリーポイント
        static void Main()
        {
            WorkingDynamic();
            BrokenDynamic();
        }
    }
}

実行してみると、40行目でRuntimeBinderExceptionが発生します。下図はデバッグ実行したときのキャプチャ画像です。IFooにBarの定義がないなんて、そんなバカな...orz

RuntimeBinderException

このようなバグと思しきものを目の当たりにしたのは初めてです。とりあえず、いつかコレに遭遇してしまうかもしれないので書き残しておかないとですね。

ちょっと検証

なんでこんな事になるのか全然意味が分からないので、ちょこっと挙動の確認をしてみました。

static void Main()
{
    string @string   = "test";
    dynamic @dynamic = "test";
    {
        Foo foo = new Foo();
        foo.Bar(@string);  //--- OK
        foo.Bar(@dynamic); //--- OK
    }
    {
        IFoo foo = new Foo();
        foo.Bar(@string);  //--- OK
        foo.Bar(@dynamic); //--- NG!!
    }
    {
        IBar foo = new Foo();
        foo.Bar(@string);  //--- OK
        foo.Bar(@dynamic); //--- OK
    }
    {
        dynamic foo = new Foo();
        foo.Bar(@string);  //--- OK
        foo.Bar(@dynamic); //--- OK
    }
}

なんでそこだけピンポイントでダメなの...orz @neueccさんや@takeshikが仰っていたのですが、なんかDLRの実行時解決に問題があるように思える状況ではあります。問題となっている箇所をILSpyを使って逆コンパイルしてみると、次のようになります。(読みやすくするためにだいぶ整形しています)

static void Main()
{
    IFoo foo     = new Foo();
    dynamic text = "test";
    foo.Bar(text);
}
static CallSite<Action<CallSite, IFoo, object>> site;
static void Main()
{
    object text = "test";
    IFoo foo    = new Foo();
    if (site == null)
    {
        var info = new []
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        };
        var member = Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Bar", null, typeof(Program), info);
        site       = CallSite<Action<CallSite, IFoo, object>>.Create(member);
    }
    site.Target(site, foo, text); //--- RuntimeBinderException発生!
}

この逆コンパイルしたコードをを直接実行してみると16行目でRuntimeBinderExceptionが発生します。DLRはちっとも詳しくないですが、実行時にIFooのBarメソッドをリフレクションで探して実行しているハズです。そしてここで次のコードを確認してみます。

static void Main()
{
    IFoo foo  = new Foo();
    var info1 = typeof(IFoo).GetMethod("Bar");  //--- info1 == null
    var info2 = foo.GetType().GetMethod("Bar"); //--- info2 != null
}

前者だとBarメソッドがないと判断されますが、後者だとBarメソッドがあると言われます。今回のエラーは "IFooにBarメソッドがない" ことだったので、その挙動からするとCallSite<T>.Createメソッドの内部実装が前者のような形になっているように思えます。逆に、後者で実装されていれば問題は発生しないのではないかと推測されます。とはいえ、前者にしている理由は何らかあるのではないかと思うと単純に後者にしてしまって良いかは分かりません。

また、実際に内部を追いかけてみましたが、最後に大量の式木が出てきて敢え無く挫折しました...。ということで、検証はここまで...。