C# 関数ポインタとは?delegateとの違い・使い方・unsafeの注意点を解説
はじめに
C#で高パフォーマンスな処理やネイティブコード連携を扱っていると、「関数ポインタ」という言葉を目にすることがあります。
C#の関数ポインタは、メソッドのアドレスを直接扱い、delegateよりも低レベルにメソッド呼び出しを行うための機能です。C#ではC# 9.0からdelegate*構文によって関数ポインタを利用できるようになりました。
ただし、C#の関数ポインタは通常のアプリケーション開発で頻繁に使う機能ではありません。unsafeコードが必要であり、型安全性やメモリ安全性に関する注意点もあります。そのため、何となくdelegateの代わりに使うのではなく、仕組み・使い方・リスクを理解したうえで採用することが重要です。
この記事では、C# 関数ポインタの基本構文、delegateとの違い、unsafeが必要な理由、ネイティブ連携での使い方、よくあるエラー、ベストプラクティスまで詳しく解説します。
1. C#の関数ポインタとは?
1-1. 関数ポインタの基本概念
関数ポインタとは、関数やメソッドそのものを直接呼び出すために、その実体のアドレスを保持するポインタのことです。
通常、C#でメソッドを変数のように扱う場合はdelegateやラムダ式を使います。
C#Func<int, int, int> add = (x, y) => x + y;
int result = add(1, 2);
一方、関数ポインタでは、メソッドのアドレスを取得して、そのアドレス経由でメソッドを呼び出します。
C#unsafe
{
delegate*<int, int, int> ptr = &Add;
int result = ptr(1, 2);
}
static int Add(int x, int y) => x + y;
このように、C#の関数ポインタはdelegate*という構文で表現されます。
delegate*<int, int, int>は、「int型の引数を2つ受け取り、int型を返す関数ポインタ」を意味します。
1-2. C#で関数ポインタが使えるようになった背景
C#はもともと、安全性や生産性を重視した言語です。そのため、関数を値として扱う場合はdelegateやFunc<T>、Action<T>を使うのが一般的でした。
しかし、次のような場面では、より低レベルな関数呼び出しが必要になります。
高パフォーマンスな処理を実装したい場合、ネイティブライブラリと連携したい場合、ランタイムのオーバーヘッドをできるだけ減らしたい場合、AOTコンパイルや低レベルAPIで効率的な呼び出しを行いたい場合などです。
delegateは便利ですが、オブジェクトとしての管理、間接呼び出し、場合によってはGCや割り当ての影響を受けることがあります。
そこでC# 9.0以降では、delegate*による関数ポインタが導入され、より直接的にメソッドを呼び出せるようになりました。
1-3. 関数ポインタでできること
C#の関数ポインタを使うと、主に次のようなことができます。
静的メソッドのアドレスを取得して直接呼び出す、関数ポインタを変数に保持する、関数ポインタを別のメソッドへ引数として渡す、ネイティブ関数ポインタを扱う、低レベルAPIやコールバック処理に利用する、といった使い方です。
たとえば、処理内容を関数ポインタとして受け取り、ループ内で繰り返し呼び出すようなコードを書くことができます。
C#unsafe
{
delegate*<int, int, int> operation = □
int result = Execute(5, operation);
Console.WriteLine(result);
}
static int Square(int value) => value * value;
unsafe static int Execute(int value, delegate*<int, int> func)
{
return func(value);
}
この例では、Squareメソッドのアドレスを関数ポインタとしてExecuteメソッドに渡しています。
1-4. delegateやラムダ式と何が違うのか
delegateやラムダ式は、C#らしい安全で扱いやすい仕組みです。
C#Func<int, int> square = x => x * x;
int result = square(5);
この書き方は簡潔で、通常の業務アプリケーションでは十分です。
一方、関数ポインタは次のように書きます。
C#unsafe
{
delegate*<int, int> square = □
int result = square(5);
}
static int Square(int x) => x * x;
関数ポインタはラムダ式のようにクロージャを持てません。インスタンスの状態を自然にキャプチャすることもできません。また、unsafeコンテキストが必要です。
その代わり、delegateよりも低レベルで、余計なオブジェクト生成を避けやすく、ネイティブコードとの連携にも向いています。
つまり、delegateやラムダ式は「安全で便利な抽象化」、関数ポインタは「高速・低レベル・unsafeな呼び出し手段」と考えると理解しやすいです。
2. C#関数ポインタの基本構文
2-1. delegate*の基本的な書き方
C#の関数ポインタは、delegate*を使って宣言します。
基本構文は次のとおりです。
C#delegate*<引数型1, 引数型2, 戻り値型> 変数名;
たとえば、intを2つ受け取り、intを返す関数ポインタは次のように書きます。
C#delegate*<int, int, int> ptr;
最後に書かれた型が戻り値の型です。その前に並ぶ型が引数の型です。
引数が1つで戻り値がintの場合は、次のようになります。
C#delegate*<int, int> ptr;
この場合、最初のintが引数、最後のintが戻り値です。
引数がなく、戻り値がvoidの場合は次のように書きます。
C#delegate*<void> ptr;
2-2. 戻り値と引数の指定方法
関数ポインタの型指定では、最後の型が戻り値です。
C#delegate*<int, double, string, bool> ptr;
この例では、次のようなメソッドを指すことができます。
C#static bool Method(int x, double y, string text)
{
return text.Length > x + y;
}
つまり、delegate*<int, double, string, bool>は次のシグネチャに対応します。
C#bool Method(int x, double y, string text)
戻り値がない場合は、最後にvoidを指定します。
C#delegate*<int, string, void> ptr;
これは次のようなメソッドに対応します。
C#static void Print(int count, string message)
{
Console.WriteLine($"{count}: {message}");
}
2-3. managedとunmanagedの違い
C#の関数ポインタには、managedとunmanagedがあります。
managedは、.NETランタイム上のマネージドメソッドを指す関数ポインタです。
C#delegate* managed<int, int, int> ptr;
managedは省略可能です。そのため、次の2つは基本的に同じ意味です。
C#delegate* managed<int, int, int> ptr1;
delegate*<int, int, int> ptr2;
一方、unmanagedは、ネイティブコードなどアンマネージド関数を指す関数ポインタです。
C#delegate* unmanaged<int, int, int> ptr;
ネイティブライブラリの関数や、C言語側のコールバック関数とやり取りする場合は、unmanaged関数ポインタを使うことがあります。
2-4. calling conventionの指定方法
unmanaged関数ポインタでは、呼び出し規約を指定できます。呼び出し規約とは、関数呼び出し時に引数をどのように渡すか、スタックを誰が片付けるか、といった低レベルなルールのことです。
代表的な呼び出し規約には、Cdecl、Stdcall、Thiscall、Fastcallなどがあります。
C#では、次のように指定できます。
C#delegate* unmanaged[Cdecl]<int, int, int> ptr;
この例は、C言語のcdecl呼び出し規約を使う関数ポインタです。
Windows APIなどでstdcallを使う場合は、次のように書きます。
C#delegate* unmanaged[Stdcall]<int, int, int> ptr;
呼び出し規約が一致していないと、実行時にクラッシュしたり、スタックが破壊されたりする可能性があります。そのため、ネイティブ連携では関数の定義とC#側の宣言を正確に合わせる必要があります。
2-5. 関数ポインタを呼び出す基本コード例
C# 関数ポインタの基本的なコード例を見てみましょう。
C#using System;
unsafe
{
delegate*<int, int, int> addPtr = &Add;
int result = addPtr(10, 20);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
このコードでは、Addメソッドのアドレスを&Addで取得し、addPtrに代入しています。
その後、addPtr(10, 20)のように通常のメソッド呼び出しに近い形で実行しています。
関数ポインタを使うコードはunsafeコンテキスト内で書く必要があります。これは、関数のアドレスを直接扱うため、C#の通常の安全機構の外側にある操作だからです。
3. C#関数ポインタとdelegateの違い
3-1. delegateの仕組み
delegateは、メソッドを参照するための型安全なオブジェクトです。
たとえば、次のようなデリゲート型を定義できます。
C#delegate int Calculate(int x, int y);
このデリゲートには、同じシグネチャを持つメソッドを代入できます。
C#Calculate calc = Add;
int result = calc(1, 2);
static int Add(int x, int y) => x + y;
delegateはオブジェクトとして扱われるため、インスタンスメソッド、静的メソッド、ラムダ式、匿名メソッドなどを柔軟に扱えます。
また、複数のメソッドを登録できるマルチキャストデリゲートとしても使えます。
C#Action action = MethodA;
action += MethodB;
action();
このように、delegateはC#のイベント処理やコールバック処理の中心的な仕組みです。
3-2. 関数ポインタの仕組み
関数ポインタは、メソッドのアドレスを直接保持します。
C#unsafe
{
delegate*<int, int, int> ptr = &Add;
int result = ptr(1, 2);
}
static int Add(int x, int y) => x + y;
delegateのようなオブジェクトではなく、より低レベルなポインタとして扱われます。
関数ポインタは、呼び出し先のメソッドとシグネチャが一致している必要があります。また、ラムダ式やインスタンスメソッドをそのまま関数ポインタとして扱うことはできません。
そのため、柔軟性ではdelegateに劣りますが、低レベルで効率的な呼び出しが可能になります。
3-3. 型安全性の違い
delegateは、C#の型システムによって安全に扱えます。
C#Func<int, int, int> func = Add;
引数や戻り値の型が一致しない場合、コンパイル時にエラーになります。
関数ポインタもシグネチャのチェックは行われますが、ポインタである以上、通常のC#コードよりも危険な操作が可能です。特に、無効なアドレス、呼び出し規約の不一致、ネイティブコードとの型不一致などは、実行時クラッシュにつながる可能性があります。
つまり、delegateは高い安全性を持つ一方、関数ポインタは安全性よりも低レベル制御やパフォーマンスを重視した仕組みです。
3-4. パフォーマンスの違い
関数ポインタは、delegateよりも呼び出しオーバーヘッドを抑えられる場合があります。
delegateはオブジェクトであり、呼び出し時にデリゲート呼び出しの仕組みを通ります。状況によってはインスタンスの生成やキャプチャによる割り当てが発生することもあります。
一方、関数ポインタはメソッドのアドレスを直接使うため、より単純な呼び出しになります。
ただし、常に関数ポインタのほうが速いとは限りません。JIT最適化、インライン化、呼び出し回数、実行環境、コード構造によって結果は変わります。
そのため、パフォーマンス目的でC# 関数ポインタを使う場合は、必ずベンチマークで効果を確認するべきです。
3-5. GCやメモリ管理への影響
delegateはマネージドオブジェクトなので、GCの管理対象です。
C#Func<int, int> func = x => x * x;
このようなデリゲートは、通常の.NETオブジェクトと同じようにGCによって管理されます。
関数ポインタ自体はオブジェクトではなく、メソッドのアドレスを指すポインタです。そのため、デリゲートインスタンスのようなオブジェクト割り当てを避けられる場合があります。
ただし、関数ポインタを使えばGCのことを考えなくてよい、という意味ではありません。特にネイティブコードと連携する場合、マネージドオブジェクトの寿命、固定化、コールバックの有効期間などに注意が必要です。
GC管理下のオブジェクトをネイティブ側から参照する場合は、オブジェクトが移動または解放される可能性を考慮しなければなりません。
3-6. delegateと関数ポインタの使い分け
通常のC#開発では、まずdelegate、Func<T>、Action<T>、ラムダ式を使うのが基本です。
次のような場面ではdelegateが向いています。
イベント処理、LINQ、非同期処理、コールバック、UIアプリケーション、ビジネスロジック、可読性を重視するコードなどです。
一方、関数ポインタが向いているのは、次のような場面です。
呼び出し回数が非常に多くオーバーヘッドを削減したい場合、ネイティブAPIと関数ポインタで直接やり取りしたい場合、ライブラリ内部で低レベル最適化を行いたい場合、AOTやランタイム制約のある環境で効率的に呼び出したい場合などです。
判断基準としては、「通常のC#らしいコードで十分ならdelegate」「低レベル制御や明確な性能上の理由があるなら関数ポインタ」と考えるとよいでしょう。
4. C#関数ポインタの使い方
4-1. staticメソッドを関数ポインタとして扱う方法
C#の関数ポインタでは、基本的に静的メソッドのアドレスを取得して使います。
C#unsafe
{
delegate*<int, int> ptr = &Double;
int result = ptr(10);
Console.WriteLine(result);
}
static int Double(int value)
{
return value * 2;
}
&DoubleによってDoubleメソッドのアドレスを取得し、delegate*<int, int>型の変数に代入しています。
Doubleメソッドは、intを1つ受け取り、intを返すため、関数ポインタの型と一致しています。
4-2. &演算子でメソッドのアドレスを取得する方法
関数ポインタにメソッドを代入するには、&演算子を使います。
C#delegate*<int, int, int> ptr = &Add;
これは「Addメソッドのアドレスを取得して、ptrに代入する」という意味です。
完全なコード例は次のとおりです。
C#using System;
unsafe
{
delegate*<int, int, int> ptr = &Add;
Console.WriteLine(ptr(3, 4));
}
static int Add(int a, int b)
{
return a + b;
}
&を付けずに代入しようとすると、通常のメソッドグループ変換とは異なるため、関数ポインタとして扱えません。
4-3. 関数ポインタを変数に代入する方法
関数ポインタは変数として保持できます。
C#unsafe
{
delegate*<int, int, int> operation;
operation = &Add;
Console.WriteLine(operation(2, 3));
operation = &Multiply;
Console.WriteLine(operation(2, 3));
}
static int Add(int x, int y) => x + y;
static int Multiply(int x, int y) => x * y;
この例では、同じシグネチャを持つAddとMultiplyを、同じ関数ポインタ変数に代入しています。
関数ポインタの型とメソッドのシグネチャが一致していれば、このように呼び出し先を切り替えることができます。
4-4. 関数ポインタを引数として渡す方法
関数ポインタは、メソッドの引数として渡すこともできます。
C#using System;
unsafe
{
int result1 = Execute(10, &Double);
int result2 = Execute(10, &Triple);
Console.WriteLine(result1);
Console.WriteLine(result2);
}
static int Double(int value) => value * 2;
static int Triple(int value) => value * 3;
unsafe static int Execute(int value, delegate*<int, int> func)
{
return func(value);
}
この例では、Executeメソッドが関数ポインタを受け取り、渡された関数を実行しています。
delegateを使ったコールバックに似ていますが、関数ポインタはunsafeであり、静的メソッドのアドレスを直接渡している点が異なります。
4-5. 関数ポインタを構造体や低レベルAPIで使う例
関数ポインタは、構造体に保持して低レベルAPIのテーブルのように使うこともできます。
C#using System;
unsafe struct Operations
{
public delegate*<int, int, int> Add;
public delegate*<int, int, int> Multiply;
}
unsafe
{
Operations ops = new Operations
{
Add = &Add,
Multiply = &Multiply
};
Console.WriteLine(ops.Add(2, 3));
Console.WriteLine(ops.Multiply(2, 3));
}
static int Add(int x, int y) => x + y;
static int Multiply(int x, int y) => x * y;
このような書き方は、ネイティブAPIの関数テーブルや、低レベルなプラグイン機構、パフォーマンス重視の内部処理などで使われることがあります。
ただし、可読性は下がりやすいため、一般的なアプリケーションコードで多用するべきではありません。
5. unsafeが必要な理由と注意点
5-1. なぜC#関数ポインタにはunsafeが必要なのか
C#の関数ポインタは、メソッドのアドレスを直接扱います。
通常のC#では、メモリの安全性や型安全性をランタイムとコンパイラが強く守ってくれます。しかし、ポインタを使う操作は、その安全機構の外側にあります。
関数ポインタで問題が起きると、単なる例外では済まないことがあります。無効なアドレスを呼び出した場合、アプリケーションがクラッシュする可能性があります。呼び出し規約を間違えると、スタックが壊れる可能性があります。型を間違えると、予期しない値が渡される可能性があります。
そのため、C#では関数ポインタをunsafe機能として扱っています。
5-2. unsafeコードを有効化する方法
C#でunsafeコードを使うには、プロジェクトでunsafeを許可する必要があります。
.csprojに次の設定を追加します。
XML<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
また、コンパイラオプションで/unsafeを指定する方法もあります。
.NET SDK形式のプロジェクトであれば、通常は.csprojにAllowUnsafeBlocksを設定するのが分かりやすい方法です。
5-3. unsafeブロックの書き方
unsafeコードは、ブロック単位で書くことができます。
C#unsafe
{
delegate*<int, int> ptr = &Double;
int result = ptr(10);
}
メソッド全体をunsafeにすることもできます。
C#unsafe static int Execute(int value, delegate*<int, int> func)
{
return func(value);
}
クラスや構造体にunsafeを付けることもできます。
C#unsafe class FunctionPointerSample
{
private delegate*<int, int> _func;
}
ただし、unsafeの範囲はできるだけ狭くするべきです。必要な部分だけをunsafeブロックに閉じ込めることで、危険なコードの影響範囲を小さくできます。
5-4. 型安全性が失われるリスク
関数ポインタでは、型の不一致が大きな問題になります。
たとえば、本来はintを2つ受け取る関数なのに、別の型として扱うと、実行時に不正な動作を引き起こす可能性があります。
C#unsafe
{
delegate*<int, int, int> ptr = &Add;
int result = ptr(1, 2);
}
static int Add(int x, int y) => x + y;
このように正しく型が一致していれば問題ありません。
しかし、ネイティブコードから取得したアドレスを誤ったシグネチャで扱うと、コンパイラが完全には守ってくれない場合があります。
特にvoid*やIntPtrなどを経由して関数ポインタに変換する場合は、型の整合性を開発者自身が保証する必要があります。
5-5. 実行時エラーやクラッシュを防ぐ注意点
C# 関数ポインタを安全に使うには、次の点に注意する必要があります。
まず、関数ポインタの型と呼び出し先メソッドのシグネチャを完全に一致させることです。引数の数、引数の型、戻り値の型が一致していなければなりません。
次に、ネイティブ関数を呼び出す場合は、呼び出し規約を一致させる必要があります。CdeclなのかStdcallなのかを間違えると、深刻な不具合につながります。
また、関数ポインタがnullでないことを確認してから呼び出すことも重要です。
C#unsafe
{
delegate*<int, int> ptr = null;
if (ptr != null)
{
int result = ptr(10);
}
}
さらに、ネイティブ側に渡した関数ポインタやコールバックの有効期間にも注意が必要です。すでに無効になったアドレスを呼び出すと、アプリケーションがクラッシュする可能性があります。
5-6. unsafeを使うべきでないケース
次のようなケースでは、関数ポインタやunsafeを使うべきではありません。
通常の業務ロジックを書いている場合、イベント処理やコールバックを簡単に実装したい場合、LINQやラムダ式で十分な場合、パフォーマンス上の問題が確認されていない場合、チームメンバーが低レベルコードに慣れていない場合などです。
unsafeを使うと、コードレビューや保守の難易度が上がります。また、バグが発生したときの原因調査も難しくなりがちです。
C# 関数ポインタは便利な機能ですが、基本的には「必要なときだけ使う上級者向けの機能」と考えるべきです。
6. C#関数ポインタの主な利用シーン
6-1. パフォーマンスが重要な処理
関数ポインタは、パフォーマンスが非常に重要な処理で使われることがあります。
たとえば、数値計算、画像処理、ゲームエンジン、リアルタイム処理、シリアライザ、パーサー、ランタイム内部処理などです。
大量のデータに対して同じ関数を何度も呼び出す場合、呼び出しオーバーヘッドが積み重なります。そのような場面では、delegateではなく関数ポインタを使うことで、オーバーヘッドを削減できる可能性があります。
ただし、実際に速くなるかどうかはコードや環境によります。最適化目的で使う場合は、感覚ではなくベンチマークで判断することが重要です。
6-2. ネイティブコードとの相互運用
C# 関数ポインタは、ネイティブコードとの相互運用で特に役立ちます。
CやC++のライブラリでは、関数ポインタを引数として受け取るAPIがよくあります。たとえば、コールバック関数、比較関数、イベント通知関数、フック関数などです。
C#側でdelegate* unmanagedを使うと、ネイティブ側の関数ポインタに近い形で扱えます。
C#delegate* unmanaged[Cdecl]<int, int, int> nativeFunc;
このように呼び出し規約を含めて型を定義できるため、ネイティブAPIとの境界で明示的なコードを書けます。
6-3. コールバック処理
関数ポインタは、コールバック処理にも使えます。
コールバックとは、ある処理の途中で別の関数を呼び出してもらう仕組みです。C#では通常、コールバックにはdelegateやActionを使います。
C#void Execute(Action callback)
{
callback();
}
しかし、ネイティブライブラリにコールバックを渡す場合や、低レベルな実装でコールバック呼び出しのオーバーヘッドを抑えたい場合は、関数ポインタが選択肢になります。
C#unsafe static void Execute(delegate*<void> callback)
{
callback();
}
ただし、関数ポインタのコールバックは状態を持ちにくく、ラムダ式のように変数をキャプチャできません。そのため、柔軟性よりも性能や低レベル連携を優先する場面で使います。
6-4. AOTや低レベル最適化が必要な場面
AOTコンパイルや低レベル最適化が必要な環境でも、関数ポインタが役立つことがあります。
AOTでは、実行時の動的なコード生成やリフレクションに制限がある場合があります。そのような環境では、呼び出し先を明示的に指定できる関数ポインタが有効なケースがあります。
また、ランタイムやフレームワークの内部実装では、通常のアプリケーションコードよりも低レベルな制御が必要になることがあります。
関数ポインタは、そうした場面で「C#で書きながら、CやC++に近い低レベルな呼び出しを行う」ための手段になります。
6-5. ライブラリ・フレームワーク開発での活用
C# 関数ポインタは、一般的なアプリケーションよりも、ライブラリやフレームワークの内部で使われることが多い機能です。
たとえば、次のような実装で利用される可能性があります。
高速なコールバック機構、ネイティブAPIラッパー、シリアライゼーションライブラリ、数値計算ライブラリ、ゲームエンジン、低レベルI/O処理、ランタイム補助機能などです。
ライブラリ開発では、利用者に見えるAPIは安全なdelegateや通常のメソッドとして提供し、内部実装でのみ関数ポインタを使う設計が有効です。
これにより、外部には使いやすく安全なインターフェースを提供しつつ、内部ではパフォーマンスを追求できます。
7. 関数ポインタとネイティブ連携
7-1. unmanaged関数ポインタとは
unmanaged関数ポインタは、アンマネージドコードの関数を指すための関数ポインタです。
C#delegate* unmanaged<int, int, int> ptr;
これは、ネイティブ関数を呼び出すために使われます。
呼び出し規約を指定する場合は、次のように書きます。
C#delegate* unmanaged[Cdecl]<int, int, int> ptr;
unmanagedを指定すると、C#側では「この関数ポインタは.NETの通常のマネージドメソッドではなく、ネイティブ側の関数である」と明示できます。
7-2. C言語の関数ポインタとの関係
C言語では、関数ポインタは古くから使われている機能です。
たとえば、C言語では次のように書きます。
Cint (*func)(int, int);
これは、intを2つ受け取り、intを返す関数へのポインタです。
C#では、同じような意味を次のように表現します。
C#delegate* unmanaged[Cdecl]<int, int, int> func;
C#のdelegate*は、C言語の関数ポインタに近い役割を持ちます。ただし、C#では型指定の方法やunsafeコンテキスト、マネージドとアンマネージドの区別など、.NETならではのルールがあります。
7-3. P/Invokeとの違い
P/Invokeは、C#からネイティブライブラリの関数を呼び出すための一般的な仕組みです。
C#using System.Runtime.InteropServices;
[DllImport("NativeLibrary")]
private static extern int Add(int x, int y);
P/Invokeでは、外部関数をC#の静的メソッドのように宣言して呼び出します。
一方、関数ポインタは、関数のアドレスを直接扱います。
C#delegate* unmanaged[Cdecl]<int, int, int> ptr;
P/Invokeは宣言が分かりやすく、通常のネイティブ呼び出しでは使いやすい方法です。関数ポインタは、ネイティブ側から取得した関数アドレスを呼び出したい場合や、関数ポインタをコールバックとして渡したい場合などに向いています。
つまり、固定されたネイティブ関数を呼び出すならP/Invoke、関数アドレスを動的に扱う必要があるなら関数ポインタ、という使い分けになります。
7-4. UnmanagedCallersOnly属性との関係
UnmanagedCallersOnly属性は、マネージドメソッドをアンマネージドコードから直接呼び出せるようにするための属性です。
C#using System.Runtime.InteropServices;
public static class CallbackHolder
{
[UnmanagedCallersOnly]
public static int Callback(int x)
{
return x * 2;
}
}
この属性を付けたメソッドは、ネイティブコードから呼び出されることを想定したメソッドになります。
関数ポインタと組み合わせると、C#で定義した静的メソッドのアドレスをネイティブ側へ渡すような設計が可能になります。
ただし、UnmanagedCallersOnlyを付けたメソッドには制約があります。たとえば、基本的に静的メソッドである必要があり、通常のマネージド呼び出しとは異なる扱いになります。また、引数や戻り値にはアンマネージド境界で安全に扱える型を選ぶ必要があります。
7-5. ネイティブコールバックを扱う際の注意点
ネイティブコールバックを扱うときは、特に慎重になる必要があります。
まず、呼び出し規約を必ず一致させます。C側がcdeclなのにC#側でstdcallとして宣言すると、スタックの扱いがずれてクラッシュする可能性があります。
次に、引数と戻り値の型を正確に対応させます。Cのint、long、ポインタ、構造体などは、プラットフォームによってサイズや表現が異なる場合があります。
また、マネージドオブジェクトを直接ネイティブ側に渡す場合は、GCによる移動や寿命に注意が必要です。必要に応じてGCHandleや固定化を検討します。
さらに、ネイティブ側が関数ポインタを長期間保持する場合、その関数ポインタが有効であり続けることを保証しなければなりません。
ネイティブ連携では、C#側だけでなく、C/C++側の関数定義も正確に確認することが重要です。
8. C#関数ポインタでよくあるエラーと対処法
8-1. unsafeコンテキストがない場合のエラー
関数ポインタをunsafeなしで使おうとすると、コンパイルエラーになります。
C#delegate*<int, int> ptr = &Double;
このようなコードは、unsafeコンテキスト外では使用できません。
対処法は、unsafeブロックまたはunsafeメソッドの中に書くことです。
C#unsafe
{
delegate*<int, int> ptr = &Double;
}
また、プロジェクトでunsafeコードを許可する必要があります。
XML<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
8-2. static以外のメソッドを指定した場合のエラー
関数ポインタでは、通常、静的メソッドのアドレスを取得します。
次のようなインスタンスメソッドをそのまま関数ポインタにすることはできません。
C#class Calculator
{
public int Add(int x, int y) => x + y;
}
インスタンスメソッドは、内部的に対象インスタンスであるthisを必要とします。関数ポインタはdelegateのように対象オブジェクトを保持しないため、インスタンスメソッドを自然に扱えません。
対処法としては、静的メソッドにする、状態を明示的に引数として渡す、またはdelegateを使う、といった方法があります。
C#static int Add(int x, int y) => x + y;
インスタンス状態を扱いたい場合は、無理に関数ポインタを使わず、Func<T>やAction<T>を使うほうが安全で分かりやすいです。
8-3. 引数や戻り値の型が一致しない場合
関数ポインタの型とメソッドのシグネチャが一致していない場合、コンパイルエラーになります。
C#unsafe
{
delegate*<int, int> ptr = &Add;
}
static int Add(int x, int y) => x + y;
この例では、delegate*<int, int>は「intを1つ受け取り、intを返す」関数ポインタです。しかし、Addはintを2つ受け取るため一致しません。
正しくは次のように書きます。
C#unsafe
{
delegate*<int, int, int> ptr = &Add;
}
static int Add(int x, int y) => x + y;
関数ポインタでは、最後の型が戻り値であることを忘れないようにしましょう。
8-4. calling conventionの不一致による問題
ネイティブ関数を呼び出す場合、calling conventionの不一致は重大な問題になります。
C#delegate* unmanaged[Cdecl]<int, int, int> ptr;
C側の関数がcdeclで定義されているなら、C#側もCdeclとして宣言する必要があります。
もしC側がstdcallなのにC#側でCdeclとして扱うと、引数の渡し方やスタックの後始末がずれ、クラッシュする可能性があります。
対処法は、ネイティブ関数のヘッダーファイルやドキュメントを確認し、C#側の宣言を正確に合わせることです。
特にWindows API、Cライブラリ、C++ライブラリ、プラットフォーム依存APIを扱う場合は注意が必要です。
8-5. null参照や無効なアドレスを呼び出す危険性
関数ポインタはnullになることがあります。
C#unsafe
{
delegate*<int, int> ptr = null;
int result = ptr(10);
}
このようにnullの関数ポインタを呼び出すと、実行時に重大なエラーが発生する可能性があります。
呼び出す前にnullチェックを行いましょう。
C#unsafe
{
delegate*<int, int> ptr = &Double;
if (ptr != null)
{
int result = ptr(10);
Console.WriteLine(result);
}
}
static int Double(int value) => value * 2;
また、ネイティブ側から受け取った関数アドレスが無効になっていないかにも注意が必要です。
一度取得したアドレスが常に安全とは限りません。ライブラリのアンロード、オブジェクトの破棄、コールバックの寿命切れなどによって、無効なアドレスになる可能性があります。
9. C#関数ポインタを使う際のベストプラクティス
9-1. まずdelegateで十分かを検討する
C# 関数ポインタを使う前に、まずdelegateやFunc<T>、Action<T>で十分かを検討しましょう。
多くのケースでは、delegateのほうが安全で、読みやすく、保守しやすいです。
C#Func<int, int> doubleFunc = x => x * 2;
このような通常のコードで要件を満たせるなら、関数ポインタを使う必要はありません。
関数ポインタは、明確な理由がある場合にだけ使うべきです。たとえば、パフォーマンス計測でdelegate呼び出しがボトルネックになっていることが確認された場合や、ネイティブAPIとの連携で関数ポインタが必要な場合です。
9-2. unsafe範囲を最小限にする
unsafeコードは、できるだけ狭い範囲に限定しましょう。
悪い例は、クラス全体を安易にunsafeにすることです。
C#unsafe class Processor
{
// すべてがunsafeコンテキストになる
}
必要なメソッドやブロックだけをunsafeにするほうが安全です。
C#class Processor
{
public void Run()
{
unsafe
{
delegate*<int, int> ptr = &Double;
int result = ptr(10);
}
}
private static int Double(int value) => value * 2;
}
unsafe範囲を小さくすると、危険なコードの場所が明確になり、レビューやテストもしやすくなります。
9-3. 呼び出し規約と型を明確にする
関数ポインタを使うときは、型と呼び出し規約を明確にしましょう。
C#delegate* unmanaged[Cdecl]<int, int, int> ptr;
このように書けば、Cdeclで、intを2つ受け取り、intを返す関数であることが分かります。
ネイティブ連携では、C/C++側の宣言をコメントとして残しておくと、保守しやすくなります。
C#// C: int add(int x, int y);
delegate* unmanaged[Cdecl]<int, int, int> addPtr;
型や呼び出し規約が曖昧なまま関数ポインタを扱うと、後から不具合の原因を追うのが難しくなります。
9-4. 可読性より速度を優先してよい場面を見極める
関数ポインタは、可読性を犠牲にしやすい機能です。
C#delegate*<int, int, int> op = &Add;
このコードは、慣れていない開発者にとってはFunc<int, int, int>より分かりにくい場合があります。
そのため、可読性よりも速度や低レベル制御を優先してよい場面かどうかを見極める必要があります。
たとえば、アプリケーション全体でほとんど実行されない処理に関数ポインタを使っても、得られるメリットは小さいです。一方、1秒間に何百万回も呼び出されるホットパスであれば、関数ポインタを検討する価値があります。
大切なのは、「速そうだから使う」のではなく、「測定した結果、必要だから使う」という判断です。
9-5. ベンチマークで効果を確認する
パフォーマンス改善を目的にC# 関数ポインタを使う場合は、必ずベンチマークを取りましょう。
delegate、直接呼び出し、関数ポインタ呼び出しを比較すると、実際の差が分かります。
簡単なイメージは次のようになります。
C#unsafe
{
delegate*<int, int> ptr = &Double;
Func<int, int> func = Double;
int a = Double(10);
int b = func(10);
int c = ptr(10);
}
static int Double(int value) => value * 2;
実際の性能差は、JIT最適化、インライン化、CPU、.NETのバージョン、コード構造によって変わります。
そのため、実運用に近い条件で測定することが大切です。
関数ポインタを導入するとコードが複雑になるため、その複雑さに見合う効果があるかを確認しましょう。
10. C#関数ポインタに関するFAQ
10-1. 関数ポインタは通常のC#開発で必要か
通常のC#開発では、関数ポインタが必要になる場面は多くありません。
Webアプリケーション、業務システム、デスクトップアプリ、一般的なAPI開発では、delegate、Func<T>、Action<T>、ラムダ式で十分なことがほとんどです。
C# 関数ポインタが必要になるのは、主に高パフォーマンス処理、ネイティブ連携、ライブラリ内部実装、低レベルAPIを扱う場合です。
初心者や中級者が通常のアプリケーションを作る場合は、まずdelegateやラムダ式をしっかり理解するほうが重要です。
10-2. ラムダ式を関数ポインタにできるか
一般的なラムダ式をそのまま関数ポインタにすることはできません。
C#Func<int, int> func = x => x * 2;
このようなラムダ式はdelegateとして扱われます。
特に、外側の変数をキャプチャするラムダ式は、内部的に状態を持つため、関数ポインタには向いていません。
C#int factor = 2;
Func<int, int> func = x => x * factor;
関数ポインタは、状態を持たない静的メソッドのアドレスを直接扱う仕組みです。
ラムダ式の柔軟性を使いたい場合は、関数ポインタではなくdelegateやFunc<T>を使いましょう。
10-3. インスタンスメソッドは関数ポインタにできるか
通常のインスタンスメソッドをそのまま関数ポインタにすることはできません。
インスタンスメソッドは、呼び出し対象のオブジェクト、つまりthisを必要とします。
C#class Calculator
{
public int Add(int x, int y) => x + y;
}
delegateであれば、対象インスタンスとメソッドをまとめて保持できます。
C#var calculator = new Calculator();
Func<int, int, int> func = calculator.Add;
しかし、関数ポインタは単なるメソッドアドレスであり、対象インスタンスを保持しません。
インスタンス状態が必要な場合は、関数ポインタではなくdelegateを使うのが自然です。
10-4. delegateの代わりに常に関数ポインタを使うべきか
いいえ。delegateの代わりに常に関数ポインタを使うべきではありません。
delegateは安全で柔軟で、C#の標準的な設計に合っています。イベント、コールバック、LINQ、非同期処理、ラムダ式など、多くの場面で自然に使えます。
関数ポインタは、unsafeが必要で、可読性も下がりやすく、誤るとクラッシュにつながる可能性があります。
そのため、通常はdelegateを使い、明確な理由がある場合だけ関数ポインタを使うべきです。
10-5. 初心者が関数ポインタを学ぶべきタイミング
C#初心者が最初から関数ポインタを学ぶ必要はありません。
まずは、メソッド、クラス、インターフェース、ジェネリクス、例外処理、delegate、ラムダ式、LINQ、非同期処理などを理解することが重要です。
その後、パフォーマンス最適化、ポインタ、unsafe、P/Invoke、ネイティブ連携に進む段階で、関数ポインタを学ぶと理解しやすくなります。
学習順としては、次の流れがおすすめです。
まず通常のメソッド呼び出しを理解し、次にdelegateとラムダ式を学びます。その後、ポインタとunsafeの基礎を学び、最後にC# 関数ポインタを理解するとスムーズです。
まとめ
C# 関数ポインタは、delegate*構文を使ってメソッドのアドレスを直接扱うための機能です。
delegateやラムダ式と比べると、低レベルで、unsafeが必要で、扱いも難しくなります。その一方で、呼び出しオーバーヘッドを抑えたい場面や、ネイティブコードとの相互運用、AOTや低レベル最適化が必要な場面では有効な選択肢になります。
基本構文は次の形です。
C#delegate*<引数型1, 引数型2, 戻り値型> ptr;
静的メソッドのアドレスは&演算子で取得できます。
C#unsafe
{
delegate*<int, int, int> ptr = &Add;
int result = ptr(1, 2);
}
static int Add(int x, int y) => x + y;
ただし、C# 関数ポインタは通常のアプリケーション開発で必須の機能ではありません。多くの場合、delegate、Func<T>、Action<T>、ラムダ式を使うほうが安全で分かりやすいです。
関数ポインタを使うべきなのは、明確な性能上の理由がある場合、ネイティブ連携で必要な場合、低レベルAPIやライブラリ内部で制御が必要な場合です。
使う際は、unsafe範囲を最小限にし、型と呼び出し規約を正確に合わせ、nullや無効なアドレスの呼び出しを避け、必ずベンチマークで効果を確認しましょう。

