ILの call と callvirt の違い

当面ILとC#と.NETな記録。インサイドかもしれないし、ダークサイドかもしれない実験記です。

実験1

ILの call と callvirt の違いがよくわからないのにプロパティを読もうってのがそもそもの間違いだなと思って、比較するコードを書いてみました。
よくある継承のサンプルコードでC#のコール、ILのcall、callvirtを比較。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ")" );
  }
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived();
    Console.WriteLine( " b.Say():" );
    b.Say();

    Console.WriteLine( "\n d.Say():" );
    d.Say();

    Console.WriteLine( "\n CallSay( b ):" );
    CallSay( b );

    Console.WriteLine( "\n CallSay( d ):" );
    CallSay( d );

    Console.WriteLine( "\n CallvirtSay( b ):" );
    CallvirtSay( b );

    Console.WriteLine( "\n CallvirtSay( d ):" );
    CallvirtSay( d );

    Console.ReadKey();
  }

  delegate void Call( Base b );

  static void CallSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果は…

 b.Say():
Base (Base)

 d.Say():
Derived (Derived)

 CallSay( b ):
Base (Base)

 CallSay( d ):
Derived (Derived)

 CallvirtSay( b ):
Base (Base)

 CallvirtSay( d ):
Derived (Derived)

あれ、そのまんま。

つづく

実験2

違いがないはずがないので、コードを改造してみます。

callとcallvirtに与えるMethodInfoを引数の型、Base型、Derived型で3つ呼んでみます。それって動かないんじゃ?と思って試してみたら、動いてしまいました。

わかりやすくするため、Derivedにプライベートなフィールドを追加します。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  private int n; // 追加

  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ") " + n );
  }

  public Derived( int x ) { this.n = x; } // 追加
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived( 5 );  // 変更
    Console.WriteLine( " b.Say():" );
    b.Say();

    Console.WriteLine( "\n d.Say():" );
    d.Say();

    Console.WriteLine( "\n CallSay( b ):" );
    CallSay( b );

    Console.WriteLine( "\n CallSay( d ):" );
    CallSay( d );

    Console.WriteLine( "\n CallvirtSay( b ):" );
    CallvirtSay( b );

    Console.WriteLine( "\n CallvirtSay( d ):" );
    CallvirtSay( d );

    Console.ReadKey();
  }

  delegate void Call( Base b );

  static void CallSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( Base b )
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( Base ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果は

 b.Say():
Base (Base)

 d.Say():
Derived (Derived) 5

 CallSay( b ):
Base (Base)
Base (Base)
Derived (Base) 0     ← おぉっ (^^;

 CallSay( d ):
Derived (Derived) 5
Base (Derived)       ← Baseを呼べた!
Derived (Derived) 5

 CallvirtSay( b ):
Base (Base)
Base (Base)
Base (Base)          ← これはいいの?

 CallvirtSay( d ):
Derived (Derived) 5
Derived (Derived) 5
Derived (Derived) 5

callにDerivedのthisを渡して、BaseのSayを呼べるってところがキモでしょうか。
一部、なんで動くの?ってところで混乱してます…。見なかったことにしたい…。

実験 その3

まださっぱりわからないのでもうちょっと実験。
Base/Derivedと継承関係のない、でもSay()をたまたま持っているクラスはどうなる?って実験。

Say()を持ったDuckクラスを追加してみます。意味深な名前ですが、これをDuckTypingと言えるのかどうか…。CallSayとCallvirtSayはobjectを取るようにします。Base/Derivedは変更なし。

using System;
using System.Reflection.Emit;

class Base
{
  public virtual void Say()
  {
    Console.WriteLine( "Base (" + this.GetType() + ")" );
  }
}

class Derived : Base
{
  private int n;

  public override void Say()
  {
    Console.WriteLine( "Derived (" + this.GetType() + ") " + n );
  }

  public Derived( int x ) { this.n = x; }
}

class Duck  // 追加
{
  public void Say()
  {
    Console.WriteLine( "ガー" );
  }
}

class Class3
{
  static void Main()
  {
    Base b = new Base(), d = new Derived( 5 );
    Duck duck = new Duck();  // 追加

    Console.WriteLine( " b.Say():" );  b.Say();
    Console.WriteLine( "\n d.Say():" );  d.Say();
    Console.WriteLine( "\n duck.Say():" );  duck.Say();

    Console.WriteLine( "\n CallSay( b ):" );  CallSay( b );
    Console.WriteLine( "\n CallSay( d ):" );  CallSay( d );
    Console.WriteLine( "\n CallSay( duck ):" );  CallSay( duck );

    Console.WriteLine( "\n CallvirtSay( b ):" );  CallvirtSay( b );
    Console.WriteLine( "\n CallvirtSay( d ):" );  CallvirtSay( d );
    Console.WriteLine( "\n CallvirtSay( duck ):" );  CallvirtSay( duck );

    Console.ReadKey();
  }

  delegate void Call( object b );  // objectに変更

  static void CallSay( object b )  // objectに変更
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( object ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Call, typeof( Duck ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callSay( b );
  }

  static void CallvirtSay( object b )  // objectに変更
  {
    DynamicMethod dm = new DynamicMethod( "", null,
      new Type[] { typeof( object ) }, typeof( Class3 ) );
    ILGenerator ilgen = dm.GetILGenerator();

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, b.GetType().GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Base ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Derived ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ldarg_0 );
    ilgen.Emit( OpCodes.Callvirt, typeof( Duck ).GetMethod( "Say" ) );

    ilgen.Emit( OpCodes.Ret );
    Call callvirtSay = ( Call ) dm.CreateDelegate( typeof( Call ) );
    callvirtSay( b );
  }
}

結果

 b.Say():
Base (Base)

 d.Say():
Derived (Derived) 5

 duck.Say():
ガー

 CallSay( b ):
Base (Base)
Base (Base)
Derived (Base) 0
ガー                 ← もう驚かない

 CallSay( d ):
Derived (Derived) 5
Base (Derived)
Derived (Derived) 5
ガー

 CallSay( duck ):
ガー
Base (Duck)          ← (^^;
Derived (Duck) 0     ← (^^;
ガー

 CallvirtSay( b ):
Base (Base)
Base (Base)
Base (Base)
ガー                 ← あれ (^^;

 CallvirtSay( d ):
Derived (Derived) 5
Derived (Derived) 5
Derived (Derived) 5
ガー                 ← あれ (^^;

 CallvirtSay( duck ):
ガー
ガー
ガー
ガー

なんとなくわかったことは、どの型のどのメソッドを呼ぶかを決めるのは言語の仕事で、ILはシグネチャが一致してれば何でも呼んじゃうよってことですか。

じゃあ、C#はどれをどう使ってるのか見てみる。Mainのはじめの部分のILから抜粋。

callvirt   instance void Base::Say() // b.Say()
callvirt   instance void Base::Say() // d.Say()
callvirt   instance void Duck::Say() // duck.Say()

なるほど。変数の型::メソッドをcallvirtで呼ぶ、と。

これまでのまとめ。

call : 指定メソッドそのものを呼ぶ。スーパークラスのメソッドを呼べる。
callvirt : 最派生を呼ぶ。
どちらも : 名前とシグネチャがあっていれば何でも呼べちゃう(?)。thisの型は無視(?)、所詮ポインタ(?)

これを手がかりに、JIS検索で JISX3016 をもっと読んでみます。