ページ

2012-10-25

C#の"ref"引数のMSIL的取り扱い(.NETと参照渡し)

C#でrefを使ったメソッドのMSIL的作り方/呼び出し方(参照渡しの方法)についてメモしておく。

サマリ

  • メソッドに渡す際は、アドレスを渡す。Cのポインタで言う&valのイメージ。
    • 引数のpushには ldloca命令を使う。
  • メソッド内では、アドレスの解決を行う。Cのポインタで言う*aのイメージ。
    • 取得にはldind命令シリーズ(ldobj)、設定にはstind命令シリーズ(stobj)を使う。
  • 型は、実際の型に&がついた「ref型」を利用する(例: System.Int32&)
    • Type.MakeByRefTypeでref型化、Type.GetElementTypeで非ref型化できる。
    • ref型かどうかは、Type.IsByRefで判断できる。

具体例として、参照を使うコードにありがちなSwapメソッドを作ってみる。

C#コード

void Swap(ref int x, ref int y) {
    int tmp;
    tmp = x;
    x = y;
    y = tmp;
}

void SampleFunc()
{
    int x = 10; int y = 20;
    Swap(ref x, ref y);
    System.Console.WriteLine("x, y: {0},{1}", x, y);
}

これをコンパイルしたものを逆アセンブルしたものが以下。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
{
  .maxstack  2
  .locals init ([0] int32 tmp)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
  IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4
  IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
  IL_000b:  ret
}

.method private hidebysig instance void  SampleFunc() cil managed
{
  .maxstack  3
  .locals init ([0] int32 x,
           [1] int32 y)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
  IL_0011:  nop
  IL_0012:  ldstr      "x, y: {0},{1}"
  IL_0017:  ldloc.0
  IL_0018:  box        [mscorlib]System.Int32
  IL_001d:  ldloc.1
  IL_001e:  box        [mscorlib]System.Int32
  IL_0023:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_0028:  nop
  IL_0029:  ret
} // end of method Program::SampleFunc

呼ばれるSwapメソッド

適当に端折りながら、上から見ていく。
(評価スタックのイメージを忘れてしまった場合は、この絵を思い出してほしい。)

まずメソッドの定義。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
int32ではなく、int32&型が使われている。これは、
Type t = typeof(Int32).MakeByRefType();
てな感じで得られる。

そしてtmp = x;の部分。
IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
これは、
  1. 1番目の引数(ref x)を持ってくる
  2. 持ってきたアドレスからint32の値を持ってくる
  3. 0番目のローカル変数に格納する
というイメージである。

x = y;はいったん飛ばして、y = tmp;部分を見るとこう。
IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
これは、
  1. 2番目の引数(ref y)を持ってくる
  2. 0番目のローカル変数からint32の値を持ってくる
  3. 持ってきたアドレスに持ってきた値を入れる
というイメージである。

x = yは上記の組み合わせ。
IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4

ここでは、int32なので、ldind.i4/stind.i4だが、型によって使うべき命令は違う。ただし、
すべての ldind 命令は、対応している組み込み値クラスを指定する Ldobj 命令のショートカットです。
(OpCodes.Ldind_I4 フィールド)なので、ldobj [type]でもよい。

ldobj/stobjの例としては、ref int?を使うと、以下の感じで生成される。
IL_0008: stobj valuetype [mscorlib]System.Nullable`1

Swap()を呼ぶメソッド

こちらは実際に呼び出しているところだけ。
IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
10、20をそれぞれx、yに入れたあと、this、xのアドレス、yのアドレスの順に持ってきて、関数をコールしている。

リフレクションでの扱い

すでにあるメソッドのMethodInfoをGetMethod()で持ってきて、ParameterTypeを見ると、
&のついたref型で入っている。この元の型を得たいときは、こんな感じになる。
if (parameterType.IsByRef) {
  Type valueType = parameterType.GetElementType();
}
(Type.MakeByRefType メソッド)

0 件のコメント:

コメントを投稿