Movatterモバイル変換


[0]ホーム

URL:


Logo

目次

概要

Ver. 9

関数ポインターとは、メモリ上でメソッドなどの命令列が入ってるアドレスを指すポインターで、「そのアドレスにジャンプすることでメソッド呼び出しが実現されている」みたいなものです。

.NET の内部的にはこれまでも関数ポインターがあったんですが、それを C# から効率的に呼ぶ手段がありませんでした。これに対して、C# 9 ではdelegate* という記法で関数ポインターを扱えるようになりました。(unsafe コンテキスト内限定で使えます。)

以前からある関数ポインター

関数ポインター自体は .NET には昔からあって、例えば、関数ポインターの値をIntPtr (nint) で取得する手段は .NET Framework 1.0 (初代。2002年リリース)の頃からありました。

ただ、関数ポインターを使ったメソッド呼び出しの側は、C# には関連機能が一切なく、一度デリゲート化するひと手間が必要でした。

using System.Runtime.InteropServices;varm=typeof(A).GetMethod("M")!;// GetFunctionPointer で、メソッド M の関数ポインターが取れる。nintptr=m.MethodHandle.GetFunctionPointer();// かつてはこれを直接呼ぶ手段はなくて、デリゲート化のひと手間が必要だった。vara=Marshal.GetDelegateForFunctionPointer<Action>(ptr);// これで A.M を間接的に呼べる。a();classA{publicstaticvoidM()=>Console.WriteLine("A.M");}

ネイティブ コード呼び出し

まあ、C# で完結している分には役に立ちません。C# で書いたメソッドを C# のデリゲートで受け取るんなら、直接代入するだけでデリゲート化できます。前節の例も、単に以下のように書けます。

// C# で書いたメソッドを C# のデリゲートで受け取るんなら、単に代入でできるわけで、// 関数ポインターを介する意味は全くなく。Actiona=A.M;// これで A.M を間接的に呼べる。a();classA{publicstaticvoidM()=>Console.WriteLine("A.M");}

実際に関数ポインターを使う場面があるのはネイティブ コード呼び出しになります。

ネイティブ コード呼び出しも、DllImport 属性(.NET 7 以降であればLibraryImport 属性)を使えば普通の、安全な C# コードだけで呼び出し可能ではあります。例えば、LibraryImport 属性を使って kernel32.dll 中のBeep メソッドを呼ぶコードは以下のように書けます。

using System.Runtime.InteropServices;// 呼び出し側。Native.Beep(440,1000);partialclassNative{// こんな感じで属性を付けておけば、 .NET ランタイム内でなんかよろしくやってくれてネイティブ コードを呼べる。    [LibraryImport("kernel32.dll")]    [return:MarshalAs(UnmanagedType.Bool)]publicstaticpartialboolBeep(uintfrequency,uintduration);}

というか、かつてはネイティブ コードの関数ポインターを取る手段がありませんでした。(上記のNative.Beep に対してGetFunctionPointer すると、取れるのはあくまで「ネイティブ コード呼び出しを内部的によろしくやってくれる C# のメソッド」の関数ポインターになります。)

NativeLibrary クラス

「関数ポインターを取る手段がないから使い道がない」と「関数ポインターが指す先を呼び出す手段がないから取れてもしょうがない」で卵が先か鶏が先かみたいな話になるんですが、C# に関数ポインターは必要ありませんでした。

ところが、 .NET Core 3.0 (C# 8.0 と同世代)で、NativeLibary (System.Runtime.InteropServices 名前空間)というクラスが入って、ネイティブ コードの関数ポインターを取得する手段が提供されるようになりました。

using System.Runtime.InteropServices;// DLL のロード。nintkernel32=NativeLibrary.Load("kernel32.dll");// 所望の関数の関数ポインターを取得。nintp=NativeLibrary.GetExport(kernel32,"Beep");// ただ、C# 8.0 時点だと呼び出しには一度デリゲート化する必要あり。vara=Marshal.GetDelegateForFunctionPointer<BeepDelegate>(p);a(440,1000);// ちなみに、 NativeLibrary の利点として、DLL のアンロードが可能。NativeLibrary.Free(kernel32);// GetDelegateForFunctionPointer にはジェネリックな型は渡せないらしく、// Func<uint, uint, int> が使えないので同じ引数・戻り値のデリゲートを定義。delegateintBeepDelegate(uintfrequencey,uintduration);

NativeLibary は、DllImportLibararyImoprt と比べると煩雑ではありますが、動的にロード・アンロードしたりといった細やかな制御が可能です。

この例のように、C# 8.0 時点では一度デリゲート化する必要があります。ただ、このデリゲートを介する部分がペナルティになって、DllImport よりも低速になっていました。

関数ポインター構文

Ver. 9

問題はIntPtr (nint)でポインターを取れても、引数や戻り値に関する情報がなくなっていて、どうやって引数を渡して、どうやって戻り値を受け取ればいいかがわからないことです。

NativeLibary クラスも入ったことだし、C# でも関数ポインターを扱える構文が欲しいということになり、C# 9 で実際に導入されることになりました。記法としてはdelegate* を使います。先ほどのNativeLibrary を使ったBeep 呼び出しの例を関数ポインターで書き換えると以下のようになります。

using System.Runtime.InteropServices;// 関数ポインターを nint で取得。nintkernel32=NativeLibrary.Load("kernel32.dll");nintp=NativeLibrary.GetExport(kernel32,"Beep");unsafe{// 「関数ポインター型」にキャストして使う。// 構文的には delegate* から初めて、 <> の中に引数を戻り値の型を並べる。// (戻り値の型が最後。Func<> 風。)varfp= (delegate*<uint,uint,int>)p;fp(440,1000);}

delegate* から書き始めて、<> の中に引数と戻り値の型を並べます。

<> の中身は、最後の1個が必ず戻り値です。Func<>Action<> のように、戻り値の有無で型を分ける必要はなく、「戻り値がない場合は最後の1個をvoid にする」という仕様です。

unsafe{// 引数 int, 戻り値 intdelegate*<int,int>pf=&f;// 引数 int, 戻り値なし(void)delegate*<int,void>pa=&a;}// 同じようなコードでも、デリゲートだと Func/Action の分岐が必要。Func<int,int>df=f;Action<int>da=a;// (こっちも普通に delegate<int, void> とか書きたい気持ちあるものの、現状、そういう仕様はない。)staticintf(intx)=>x*x;staticvoida(intx) { }

ちなみに、IL には .NET Framework 1.0 の頃から関数ポインターの仕様がちゃんとあって、「引数がuint 2つ、戻り値がint」みたいなのを指定して関数ポインターが指す先を呼び出す命令(calli)がありました。あくまで C# 8 以前にはcalli を出力する能力がなかっただけです。

& 演算子

前節の例ですでに使っていますが、C# で書いたメソッドに対して& 演算子を使えます。& 演算子で、GetFunctionPointer などのリフレクション介さずにメソッドから直接関数ポインターを得ることができます。

unsafe{// & で A.M の関数ポインターを取得。delegate*<void>p=&A.M;// ちゃんと呼べる。p();}classA{publicstaticvoidM()=>Console.WriteLine("A.M");}

ただし、& 演算子で関数ポインターを取れるのは静的メソッドだけです。

unsafe{// 静的メソッドは OK。delegate*<void>p1=&A.Static;// インスタンス メソッドは A.Instance みたいな参照の仕方はできないし、delegate*<void>p2=&A.Instance;// デリゲートみたいに「インスタンス.メソッド」での参照も不可。delegate*<void>p3=&newA().Instance;vara=newA();delegate*<void>p4=&a.Instance;}classA{publicstaticvoidStatic() { }publicvoidInstance() { }}

ちなみに、取れる値(関数ポインターが指すアドレス)自体は、GetFunctionPointer と同じになります。ただし、Type 型やMethodInfo 型を介さなくていい分、& 演算子を使う方がパフォーマンスはいいそうです。

varp1=typeof(A).GetMethod("M")!.MethodHandle.GetFunctionPointer();Console.WriteLine(p1);unsafe{delegate*<void>p2=&A.M;Console.WriteLine((nint)p2);// p1 と同じ値が取れる。Console.WriteLine(p1== (nint)p2);// true。}classA{publicstaticvoidM() { }}

引数・戻り値の型

delegate*<T> という、一見するとジェネリック型(Func<T> とかAction<T> とか)と似たような構文ですが、関数ポインターの<> の中に書ける型は、ジェネリック型引数よりもだいぶ制限が緩いです。現状ではジェネリック型引数には書けない以下のような型も、関数ポインターの<> には普通に書けます。

  • ref T,out T,in T
  • ポインター型T*
  • ref struct な型
  • void
unsafe{// in, out, ref が書けるdelegate*<inint,outint,refstring>p1=null;// ポインターが書けるdelegate*<refint,int*>p2=null;// ref struct も書けるし、それのさらに ref も書けるdelegate*<Span<int>,refSpan<int>>p3=null;// 前述のとおり、戻り値がないときは voiddelegate*<void>p4=null;}

関数ポインターの入れ子も可能です。

unsafe{// 入れ子delegate*<delegate*<int,void>,int,delegate*<void>>p1=null;}

ちなみに、書ける型の制限が緩いので、Unsafe クラスですらできないことが関数ポインター使えば書けたり。

呼び出し規約

複数のプログラミング言語をまたいでやり取りする場合、呼び出し規約(calling convention)というものを気にする必要があります。

呼び出し規約は、引数や戻り値の受け渡しの仕方を呼ぶ側・呼ばれる側でそろえるためのルールです。1つのプログラミング言語で完結している分にはコンパイラー任せで大丈夫ですが、言語をまたぐときには明示が必要になります。(「C# から Windows API を呼ぶ分にはデフォルトの規約が同じ」みたいな理由で省略可能なことはあります。)

DllImport ではCallingConvention プロパティで、LibraryImport ではUnmanagedCallConv 属性で指定します。

using System.Runtime.CompilerServices;using System.Runtime.InteropServices;LibraryImports.Beep(440,1000);DllImports.Beep(440,1000);partialclassLibraryImports{// LibraryImport では UnmanagedCallConv 属性を付ける。    [LibraryImport("kernel32.dll")]    [UnmanagedCallConv(CallConvs=new[] {typeof(CallConvCdecl) })]    [return:MarshalAs(UnmanagedType.Bool)]publicstaticpartialboolBeep(uintfrequency,uintduration);}classDllImports{// DllImport では CallingConvention プロパティを指定する。    [DllImport("kernel32.dll",CallingConvention=CallingConvention.Cdecl)]    [return:MarshalAs(UnmanagedType.Bool)]publicstaticexternboolBeep(uintfrequency,uintduration);}

関数ポインターでは、delegate*<> の間に、managed もしくはunmanaged[] という修飾を付けます。

using System.Runtime.InteropServices;nintkernel32=NativeLibrary.Load("kernel32.dll");nintp=NativeLibrary.GetExport(kernel32,"Beep");unsafe{// 規約を省略。省略時のデフォルトは managed。delegate*<int,int,int>p1=&A.M;// managed 規約。C# で書いた普通のメソッドを呼ぶときに使う。// 要は「.NET ランタイム任せ」。delegate*managed<int,int,int>p2=&A.M;// unmanaged のみ指定。// 呼び出し規約はプラットフォーム依存で、// Windows では stdcall、他のプラットフォームでは cdecl になるっぽい。varp3= (delegate*unmanaged<uint,uint,int>)p;// unmanaged[] で呼び出し規約を明示。varp4= (delegate*unmanaged[Stdcall]<uint,uint,int>)p;}classA{publicstaticintM(intx,inty)=>x*y;}

更新履歴

更新:言語バージョンの指定

[C#]

ファイル ベース実行

[C#]

C# 14.0 の新機能

[C#]

更新:[雑記] オーバーロード解決

[C#]

型の分割定義 (partial)

[C#]

ブログ

C# 14 の破壊的変更点(First-class Span)

ファイナライザー

文字列リテラルを data セクションに UTF-8 で書き込む案

nameof(T<>)

First-class な Span 型


誤字等を見つけた場合や、ご意見・ご要望がございましたら、GitHub の Issues まで気兼ねなくご連絡ください。

[8]ページ先頭

©2009-2025 Movatter.jp