アンセーフ コード、ポインター型、関数ポインター

記述する C# コードのほとんどは、"検証可能なセーフ コード" です。"検証可能なセーフ コード" は、コードが安全であることを .NET ツールによって検証できることを意味します。 一般に、セーフ コードはポインターを使用してメモリに直接アクセスしません。 また、生のメモリも割り当てられません。 代わりに、マネージド オブジェクトが作成されます。

C# では、"unsafe" なコードを記述できる unsafe コンテキストがサポートされています。 unsafe コンテキストでは、コードがポインターの使用、メモリ ブロックの割り当てと解放、関数ポインターを使用したメソッドの呼び出しを行うことができます。 C# のアンセーフ コードは、必ずしも危険ではありません。ただ、安全性を確認できないコードであるというだけです。

アンセーフ コードには次の特徴があります。

  • メソッド、型、およびコード ブロックをアンセーフとして定義できます。
  • アンセーフ コードでアプリケーションのパフォーマンスが向上することがあります。これは、配列のバインド チェックが削除されるためです。
  • アンセーフ コードは、ポインターを必要とするネイティブ関数を呼び出すときに必要です。
  • アンセーフ コードを使用すると、セキュリティと安定性の面でリスクが生じます。
  • unsafe ブロックを含むコードは、AllowUnsafeBlocks コンパイラ オプションを使ってコンパイルする必要があります。

ポインター型

unsafe コンテキストの型には、値の型に加えてポインター型、または参照型を設定できます。 ポインター型の宣言は、次のいずれかの形式になります。

type* identifier;
void* identifier; //allowed but not recommended

ポインター型の * の前に指定された型は、*と呼ばれます。 参照型にできるのはアンマネージド型だけです。

ポインター型は object を継承せず、ポインター型と の間で変換を行う方法はありません。 また、ボックス化とボックス化解除もポインターをサポートしません。 ただし、異なるポインター型の間で変換したり、ポインター型と整数型の間で変換したりすることはできます。

同じ宣言で複数のポインターを宣言する場合、アスタリスク (*) は基底の型のみと一緒に記述します。 各ポインター名のプレフィックスとしては使用されません。 次に例を示します。

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

オブジェクト参照は、それを指すポインターがあってもガベージ コレクションされる可能性があるため、ポインターによって参照や、参照を含む struct を指すことはできません。 ガベージ コレクターは、オブジェクトを指すポインター型があるかどうかを追跡しません。

MyType* 型のポインター変数の値は、MyType 型の変数のアドレスです。 ポインター型の宣言の例を次に示します。

  • int* p: p は、整数へのポインターです。
  • int** p: p は、整数へのポインターのポインターです。
  • int*[] p: p は、整数へのポインターの 1 次元配列です。
  • char* p: p は、char へのポインターです。
  • void* p: p は、未知の型へのポインターです。

ポインター間接参照演算子 * を使用すると、ポインター変数が指す位置にあるコンテンツにアクセスできます。 たとえば、次のような宣言があるとします。

int* myVariable;

この例の式 *myVariable は、int に含まれているアドレスの位置にある myVariable 変数を示しています。

ステートメントの記事に、いくつかのポインターの例があります。 次の例は、unsafe キーワードと fixed ステートメントの使用例と、内部ポインターのインクリメント方法を示しています。 このコードは、コンソール アプリケーションの Main 関数に貼り付けて実行することができます これらの例は、AllowUnsafeBlocks コンパイラ オプションを設定してコンパイルする必要があります。

// Normal pointer to an object.
int[] a = new int[5] { 10, 20, 30, 40, 50 };
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

間接参照演算子は、void* 型のポインターに適用できません。 ただし、void ポインターと他のポインター型はキャストを使用して相互に変換できます。

ポインターは、null にできます。 null ポインターに間接演算子を適用すると、実装で定義されている動作が発生します。

ポインターをメソッド間で引き渡すと、未定義の動作が発生する可能性があります。 たとえば、inout または ref パラメーターを介してポインターをローカル変数に返したり、関数の結果として返したりするメソッドがあるとします。 ポインターが固定ブロックに設定されていた場合は、そのポインターが指す変数が既に固定されていない可能性があります。

次の表は、unsafe コンテキストでポインターに使用できる演算子とステートメントの一覧を示しています。

演算子/ステートメント 使用
* ポインターの間接参照を実行します。
-> ポインター経由で構造体のメンバーにアクセスします。
[] ポインターにインデックスを付けます。
& 変数のアドレスを取得します。
++ および -- ポインターをインクリメントおよびデクリメントします。
+ および - ポインター演算を実行します。
==!=<><=>= ポインターを比較します。
stackalloc スタックにメモリを割り当てます。
fixed ステートメント 変数を一時的に固定して、そのアドレスを取得できるようにします。

ポインター関連の演算子について詳しくは、「ポインターに関連する演算子」をご覧ください。

どのポインター型も、暗黙的に void* 型に変換できます。 どのポインター型にも、値 null を割り当てることができます。 どのポインター型も、キャスト式を使用して、他のポインター型に明示的に変換できます。 また、任意の整数型をポインター型に、または任意のポインター型を整数型に変換することもできます。 これらの変換には、明示的なキャストが必要です。

次の例では、int*byte* に変換しています。 ポインターは、変数の最下位バイト アドレスを指すことに注意してください。 int のサイズ (4 バイト) まで結果を連続してインクリメントする場合、変数の残りのバイトを表示することができます。

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine("The value of the integer: {0}", number);

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

固定サイズ バッファー

fixed キーワードを使って、データの構造体に固定サイズの配列を持ったバッファーを作成できます。 固定サイズのバッファーは、他の言語またはプラットフォームのデータ ソースと相互運用するメソッドを作成するときに便利です。 この固定サイズのバッファーは、通常の構造体メンバーに許容されている任意の属性や修飾子を取ることができます。 ただし配列の型は boolbytecharshortintlongsbyteushortuintulongfloatdouble のいずれかに該当する必要があり、それが唯一の制限となります。

private fixed char name[30];

セーフ コードでは、配列を含む C# 構造体に配列要素が含まれません。 代わりに、構造体には、要素の参照が格納されます。 unsafe のコード ブロックで使われている struct に、固定サイズの配列を埋め込むことができます。

pathName が参照であるため、次の struct のサイズは配列内の要素の数に依存しません。

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

アンセーフ コードでは、構造体に埋め込み配列を含めることができます。 以下の例の fixedBuffer 配列は固定サイズです。 fixed ステートメントを使って、先頭要素へのポインターを取得します。 このポインターを使用して配列の要素にアクセスします。 fixed ステートメントによって、fixedBuffer インスタンス フィールドがメモリ内の特定の位置に固定されます。

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

要素数 128 の char 配列のサイズは 256 バイトです。 固定サイズの char バッファーは、エンコードに関係なく常に、1 文字あたり 2 バイトを消費します。 この配列サイズは、char のバッファーが、CharSet = CharSet.Auto または CharSet = CharSet.Ansi で API メソッドや構造体にマーシャリングされたときにも当てはまります。 詳細については、「CharSet」を参照してください。

上記の例は、固定せずに fixed フィールドにアクセスする方法を示しています。この方法は C# 7.3 以降から使用できます。

一般的な固定サイズの配列としては、他にも bool 配列があります。 bool 配列内の要素のサイズは常に 1 バイトです。 bool 配列は、ビット配列やバッファーの作成には適していません。

固定サイズ バッファーは System.Runtime.CompilerServices.UnsafeValueTypeAttribute を使ってコンパイルされます。これにより、オーバーフローする可能性があるアンマネージド配列が型に含まれていることが共通言語ランタイム (CLR) に指示されます。 Stackalloc を使用して割り当てられたメモリによって、CLR のバッファー オーバーラン検出機能も自動的に有効になります。 前の例では、固定サイズ バッファーを unsafe struct の中に含める方法を示しています。

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

Buffer 用にコンパイラで生成された C# は、次のように属性が付けられます。

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

固定サイズ バッファーは、次の点で通常の配列とは異なります。

  • unsafe のコンテキストでのみ使用できます。
  • 構造体のインスタンス フィールドのみを指定できます。
  • これらは常にベクター (1 次元配列) です。
  • 宣言には、fixed char id[8] などの長さを含める必要があります。 fixed char id[] は使用できません。

ポインターを使用してバイトの配列をコピーする方法

次の例では、ポインターを使って 1 つの配列から別の配列にバイトをコピーします。

この例では、unsafe キーワードを使います。このキーワードは、 メソッドでのポインターの使用を可能にします。 fixed ステートメントを使って、コピー元とコピー先の配列へのポインターを宣言します。 fixed ステートメントを使って、コピー元配列とコピー先配列のメモリ内での位置を "fixed" し、ガベージ コレクションによって移動されないようにします。 fixed ブロックが完了すると、これらの配列のメモリ ブロックは固定解除されます。 この例の Copy メソッドは unsafe キーワードを使っているので、Copy コンパイラ オプションを指定してコンパイルする必要があります。

この例では、2 番目のアンマネージド ポインターではなくインデックスを使って、両方の配列の要素にアクセスします。 pSourcepTarget のポインターの宣言を使って配列を固定します。 この機能は、C# 7.3 以降から使用できます。

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

関数ポインター

C# には、安全な関数ポインター オブジェクトを定義するための delegate 型が用意されています。 デリゲートを呼び出すには、System.Delegate から派生した型をインスタンス化し、その Invoke メソッドへの仮想メソッド呼び出しを行う必要があります。 この仮想呼び出しでは、callvirt IL 命令が使用されます。 パフォーマンスを重視するコード パスでは、calli IL 命令を使用する方が効率的です。

関数ポインターは、delegate* 構文を使用して定義できます。 コンパイラは、delegate オブジェクトをインスタンス化して Invoke を呼び出すのではなく、calli 命令を使用して関数を呼び出します。 次のコードでは、delegate または delegate* を使用する 2 つのメソッドを宣言して、同じ型の 2 つのオブジェクトを結合しています。 最初のメソッドは、System.Func<T1,T2,TResult> デリゲート型を使用します。 2 番目のメソッドは、同じパラメーターと戻り値の型を持つ delegate* 宣言を使用します。

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

次のコードは、静的ローカル関数を宣言し、そのローカル関数へのポインターを使用して UnsafeCombine メソッドを呼び出す方法を示しています。

static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);

上記のコードは、関数ポインターとしてアクセスされる関数に対するいくつかの規則を示しています。

  • 関数ポインターは、unsafe コンテキスト内でのみ宣言できます。
  • delegate* を受け取る (または delegate* を返す) メソッドは、unsafe コンテキストでのみ呼び出すことができます。
  • 関数のアドレスを取得する & 演算子は、static 関数でのみ使用できます。 (この規則は、メンバー関数とローカル関数の両方に適用されます)。

この構文は、delegate 型を宣言し、ポインターを使用する場合と似ています。 delegate* サフィックスは、宣言が "*" であることを示します。 メソッド グループを関数ポインターに割り当てるときの & は演算がメソッドのアドレスを受け取ることを示します。

delegate* の呼び出し規則はキーワード managedunmanaged を使用して指定することができます。 また、unmanaged 関数ポインターにも、呼び出し規則を指定できます。 次の宣言は、それぞれの例を示しています。 最初の宣言では、既定の managed の呼び出し規則が使用されます。 次の 4 つでは、unmanaged の呼び出し規則が使用されます。 それぞれ、ECMA 335 呼び出し規則のいずれか (CdeclStdcallFastcall、または Thiscall) を指定しています。 最後の宣言では、unmanaged の呼び出し規則を使用して、プラットフォームの既定の呼び出し規則を選択するように CLR に指示しています。 CLR は、実行時に呼び出し規則を選択します。

public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

関数ポインターの詳細については、C# 9.0 の関数ポインターに関する提案を参照してください。

C# 言語仕様

詳細については、C# 言語仕様アンセーフ コードに関する章を参照してください。