歴史
C#9でひっそり関数ポインタがC#に追加されました。
別に関数ポインタ自体は新しい機能ではなく、大昔からあります。ただし、C#から扱う構文が追加されたのがC#9です。
C#の関数には、static
な関数と、非static
な関数(いわゆるメソッド)があります。
これらは、似て非なるものです。
class Program()
{
public static void Hoge()
{
}
public void Fuga()
{
}
}
上記の例では、Hoge
とFuga
はどちらも同じ引数の関数のように見えます。
しかし、実際には(低レベルには)Fuga
関数はProgram
のインスタンスを引数として(暗黙的に)取っているのです。
よって、Fuga
関数を呼び出すには、関数ポインタの他に、Program
のインスタンスが必要です。
実際に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
なクラスに定義されていて通常アクセスできない関数への関数ポインタを無理やり作成しています。
MethodInfo
のMethodHandle
プロパティからMethodHandle
を取得し、さらにGetFunctionPointer
メソッドを呼ぶことで関数のアドレスが取れます。ジェネリックメソッドの場合はあらかじめMakeGenericMethod
しないとエラーになります。
その後、アドレスをキャスト演算子でキャストすれば目的の関数ポインタが得られます。
最初に関数ポインタさえとってしまえば、そのあとの呼び出しのコストが最小限に抑えらえるので、静的関数がターゲットならば実用的ではないでしょうか。