LoginSignup
20
22

More than 1 year has passed since last update.

【C#】本物の関数ポインタの使い方と関数アドレスの取り方

Posted at

歴史

C#9でひっそり関数ポインタがC#に追加されました。
別に関数ポインタ自体は新しい機能ではなく、大昔からあります。ただし、C#から扱う構文が追加されたのがC#9です。

C#の関数には、staticな関数と、非staticな関数(いわゆるメソッド)があります。
これらは、似て非なるものです。

class Program()
{
   public static void Hoge()
   {

   }

   public void Fuga()
   {

   }
}

上記の例では、HogeFugaはどちらも同じ引数の関数のように見えます。
しかし、実際には(低レベルには)Fuga関数はProgramのインスタンスを引数として(暗黙的に)取っているのです。
よって、Fuga関数を呼び出すには、関数ポインタの他に、Programのインスタンスが必要です。
実際にILレベルでは、非静的関数を呼び出すにはスタック上にインスタンス=マネージドポインタを積まなければなりません。

疑似ILコード
ldc.i4.0
stloc.0
ldloca.s 0
call System.Int32.ToString
pop
0.ToString();

C#では、これらの違いを意識することなく使えるような関数ポインタに相当する機能として、delegateが導入されました。
delegateは、内部的に関数ポインタと、その関数を呼び出す上で必要な引数への参照を保持しています。
これにより、delegateは、インスタンスメソッドが存在し、さらにGCのために参照を管理しなければならないC#における「安全な関数ポインタ」としての役割を果たすのです。

ところが、delegateは主にインスタンスメソッドの呼び出しの方に最適化されており、静的関数の呼び出しが遅いです。
本来なら余分な処理が入って複雑なインスタンスメソッドの呼び出しより、静的関数の呼び出しのほうが遅いです。
このあたりはすでに記事が出ているので詳細は譲るとして、これでは不便だという声も出てきました。

IL的には、関数のアドレスをスタックにロードするldftn命令と、スタック上の関数ポインタを介して関数を呼び出すcalli命令があり
対応するC#構文さえあれば、delegateでない本当の関数ポインタを使用するのは可能でした。
(実際、C#9以前もILを手打ちすれば可能。)

構文

新しく関数ポインタ型が導入されました。型名は
delegate* <TArg0,TArg1...,TResult>
です。TArgには引数の型を順番に、TResultは戻り値の型に置き換えてください。
関数ポインタもポインタですので、unsafeキーワードとコンパイルオプションが必須です。

キーワードを使い回していますが、delegateでもジェネリック型でもなく、ただのポインタです。
そのため、void*nint(IntPtr)へのキャストが可能です。

関数のアドレスは、&(関数)で取れます。例えば

delegate* <double,double> sin = &Math.Sin;

のように使います。前述したように、インスタンスメソッドの呼び出しは単純ではないので、今回の関数ポインタ構文ではstaticな関数のアドレスしか取得できません。そもそも関数ポインタが欲しかったのが静的メソッド用ですから、いい妥協点でしょう。
呼び出しは関数ポインタ型の変数に({引数})です。

var result = sin(0.5);

先ほどの例で作成した関数ポインタを呼び出す場合は上記のようになります。

リフレクションから関数ポインタ

リフレクションで取得したMethodInfoからも関数のアドレスが取得できます。

static void Hoge<T0,T1,T2,TRes>()
{
    Assembly asm = typeof(Microsoft.JSInterop.WebAssembly.WebAssemblyJSRuntime).Assembly;
    var method = asm.GetType("WebAssembly.JSInterop.InternalCalls")?.GetMethod("InvokeJS", BindingFlags.Public | BindingFlags.Static);

    var del = (delegate*<IntPtr, IntPtr, T0, T1, T2, TRes>)methodInfo.MakeGenericMethod(typeof(T0), typeof(T1), typeof(T2), typeof(TRes))
                    .MethodHandle.GetFunctionPointer();
}

この例では、internalなクラスに定義されていて通常アクセスできない関数への関数ポインタを無理やり作成しています。
MethodInfoMethodHandleプロパティからMethodHandleを取得し、さらにGetFunctionPointerメソッドを呼ぶことで関数のアドレスが取れます。ジェネリックメソッドの場合はあらかじめMakeGenericMethodしないとエラーになります。
その後、アドレスをキャスト演算子でキャストすれば目的の関数ポインタが得られます。
最初に関数ポインタさえとってしまえば、そのあとの呼び出しのコストが最小限に抑えらえるので、静的関数がターゲットならば実用的ではないでしょうか。

20
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
22