Codice non sicuro, tipi di puntatore e puntatori a funzione

La maggior parte del codice C# scritto è "codice verificabile". Codice sicuro verificabile significa che gli strumenti .NET possono verificare che il codice sia sicuro. In generale, il codice sicuro non accede direttamente alla memoria usando i puntatori. Non alloca anche memoria non elaborata. Crea invece oggetti gestiti.

C# supporta un contesto unsafe in cui è possibile scrivere codice non verificabile. In un contesto unsafe, il codice può usare puntatori, allocare e liberare blocchi di memoria e chiamare metodi usando puntatori a funzione. Il codice non sicuro in C# non è necessariamente pericoloso; è solo codice la cui sicurezza non può essere verificata.

Il codice unsafe presenta le proprietà seguenti:

  • Metodi, tipi e blocchi di codice possono essere definiti come unsafe.
  • In alcuni casi il codice unsafe può migliorare le prestazioni di un'applicazione poiché vengono rimosse le verifiche dei limiti di matrice.
  • Il codice unsafe è necessario quando si chiamano funzioni native che richiedono i puntatori.
  • L'uso del codice unsafe implica rischi per la sicurezza e la stabilità.
  • Il codice che contiene blocchi non sicuri deve essere compilato con l'opzione AllowUnsafeBlocks del compilatore.

Tipi di puntatori

In un contesto non sicuro, un tipo può essere un tipo puntatore, oltre a un tipo valore o un tipo riferimento. La dichiarazione di un tipo di puntatore può assumere uno dei seguenti formati:

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

Il tipo specificato prima di * in un tipo di puntatore viene chiamato tipo referente. Solo un tipo non gestito può essere un tipo referente.

I tipi puntatore non ereditano da oggetto e non esistono conversioni tra tipi puntatore e object. Inoltre, le conversioni boxing e unboxing non supportano i puntatori. È tuttavia possibile eseguire conversioni tra tipi di puntatore diversi e tra tipi di puntatore e tipi integrali.

Quando si dichiarano più puntatori nella stessa dichiarazione, si scrive l'asterisco (*) insieme al solo tipo sottostante. Non viene usato come prefisso per ogni nome del puntatore. Ad esempio:

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

Un puntatore non può puntare a un riferimento o a uno struct che contiene riferimenti, perché un riferimento a un oggetto può essere sottoposto a processi di Garbage Collection anche se un puntatore punta a esso. Il Garbage Collector non tiene traccia degli altri tipi di puntatore che puntano all'oggetto.

Il valore della variabile del puntatore di tipo MyType* è l'indirizzo di una variabile di tipo MyType. Di seguito sono riportati alcuni esempi di dichiarazioni di tipi di puntatore:

  • int* p: p è un puntatore a un Integer.
  • int** p: p è un puntatore a un puntatore a un Integer.
  • int*[] p: p è una matrice unidimensionale di puntatori a Integer.
  • char* p: p è un puntatore a un carattere.
  • void* p: p è un puntatore a un tipo sconosciuto.

L'operatore di riferimento indiretto del puntatore * può essere usato per accedere al contenuto nella posizione a cui punta la variabile puntatore. Si consideri ad esempio la seguente dichiarazione:

int* myVariable;

L'espressione *myVariable indica la variabile int individuata all'indirizzo contenuto in myVariable.

Esistono diversi esempi di puntatori negli articoli sull'istruzione fixed. L'esempio seguente usa la parola chiave unsafe e l'istruzione fixed e mostra come incrementare un puntatore interno. È possibile incollare il codice nella funzione Main di un'applicazione console per eseguirlo. Questi esempi devono essere compilati con l'opzione del compilatore AllowUnsafeBlocks impostata.

// Normal pointer to an object.
int[] a = [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
*/

Non è possibile applicare l'operatore di riferimento indiretto a un puntatore di tipo void*. È tuttavia possibile eseguire un cast per convertire un puntatore void in qualsiasi altro tipo e viceversa.

Un puntatore può essere null. Se l'operatore di riferimento indiretto viene applicato a un puntatore Null, si otterrà un comportamento definito dall'implementazione.

Tenere presente che il passaggio di puntatori tra metodi può generare un comportamento non definito. Prendere in considerazione un metodo che restituisce un puntatore a una variabile locale tramite un parametro in, out o ref oppure come risultato della funzione. Se il puntatore è stato impostato in un blocco fisso, la variabile a cui punta potrebbe non essere più fissa.

Nella tabella riportata di seguito sono elencati gli operatori e le istruzioni che è possibile utilizzare con i puntatori in un contesto unsafe:

Operatore/istruzione Utilizzo
* Esegue il riferimento indiretto al puntatore.
-> Accede a un membro di struct tramite un puntatore.
[] Indicizza un puntatore.
& Ottiene l'indirizzo di una variabile.
++ e -- Incrementa e decrementa puntatori.
+ e - Utilizza l'aritmetica dei puntatori.
==, !=, <, >, <= e >= Confronta puntatori.
stackalloc Alloca memoria nello stack.
Istruzione fixed Corregge temporaneamente una variabile per consentire di trovarne l'indirizzo.

Per altre informazioni sugli operatori correlati al puntatore, vedere operatori correlati al puntatore.

Qualsiasi tipo di puntatore può essere convertito in modo implicito in un tipo void*. A qualsiasi tipo di puntatore è possibile assegnare il valore null. Qualsiasi tipo di puntatore può essere convertito in modo esplicito in qualsiasi altro tipo di puntatore usando un'espressione cast. È anche possibile convertire qualsiasi tipo integrale in un tipo puntatore o qualsiasi tipo di puntatore in un tipo integrale. Queste conversioni richiedono un cast esplicito.

Nell'esempio seguente un oggetto int* viene convertito in byte*. Osservare come il puntatore punti al byte della variabile con l'indirizzo più basso. Quando si incrementa successivamente il risultato, fino a raggiungere la dimensione di int (4 byte), è possibile visualizzare i byte rimanenti della variabile.

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
    */
}

Buffer a dimensione fissa

È possibile usare la parola chiave fixed per creare un buffer con una matrice a dimensione fissa in una struttura di dati. I buffer a dimensione fissa sono utili quando si scrivono metodi che interagiscono con le origini dati di altre lingue o piattaforme. Il buffer a dimensione fissa può accettare qualsiasi attributo o modificatori consentiti per i normali membri dello struct. L'unica restrizione è rappresentata dal fatto che il tipo di matrice deve essere bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float o double.

private fixed char name[30];

Nel codice sicuro, uno struct C# che contiene una matrice non contiene gli elementi della matrice. Lo struct contiene invece un riferimento agli elementi. È possibile incorporare una matrice di dimensioni fisse in uno struct quando viene usata in un blocco di codice non sicuro.

Le dimensioni delle struct seguenti non dipendono dal numero di elementi nella matrice, perché pathName è un riferimento:

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

Uno struct può contenere una matrice incorporata nel codice non sicuro. Nell'esempio seguente la matrice fixedBuffer è di dimensioni fisse. Usare un'istruzione fixed per ottenere un puntatore al primo elemento. Gli elementi della matrice sono accessibili tramite il puntatore. L'istruzione fixed blocca un'istanza di fixedBuffer in un percorso specifico nella memoria.

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]);
    }
}

Le dimensioni della matrice char a 128 elementi sono di 256 byte. I buffer char a dimensione fissa accettano sempre 2 byte per carattere, indipendentemente dalla codifica. Questa dimensione della matrice è la stessa anche quando i buffer char vengono sottoposti a marshalling in metodi o struct API con CharSet = CharSet.Auto o CharSet = CharSet.Ansi. Per ulteriori informazioni, vedere CharSet.

Nell'esempio precedente viene illustrato l'accesso ai campi senza l'aggiunta di fixed. Un'altra matrice a dimensione fissa comune è la matrice bool. Gli elementi di una matrice di bool sono sempre di 1 byte. Le matrici bool non sono appropriate per la creazione di matrici di bit o buffer.

I buffer a dimensione fissa vengono compilati con System.Runtime.CompilerServices.UnsafeValueTypeAttribute, che indica a Common Language Runtime (CLR) che un tipo contiene una matrice non gestita che può potenzialmente andare in overflow. La memoria allocata tramite stackalloc abilita automaticamente anche le funzionalità di rilevamento dell'overrun del buffer in CLR. Nell'esempio precedente viene illustrato come può esistere un buffer a dimensione fissa in un oggetto unsafe struct.

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

L'attributo C# generato dal compilatore per Buffer è il seguente:

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;
}

I buffer a dimensione fissa differiscono dalle matrici normali nei modi seguenti:

  • Può essere usato solo in un contesto unsafe.
  • Può essere solo campi di istanza di struct.
  • Sono sempre vettori o matrici unidimensionali.
  • La dichiarazione deve includere la lunghezza, ad esempio fixed char id[8]. Non è possibile usare fixed char id[].

Come usare i puntatori per copiare una matrice di byte

Nell'esempio seguente vengono usati i puntatori per copiare i byte da una matrice a un'altra.

In questo esempio viene usata la parola chiave unsafe, che consente l'uso di puntatori nel metodo Copy. Per dichiarare i puntatori nelle matrici di origine e destinazione, viene usata l'istruzione fixed, L'istruzione fixedblocca la posizione delle matrici di origine e destinazione nella memoria in modo che non vengano rimosse da Garbage Collection. I blocchi di memoria per le matrici vengono rimossi quando il blocco fixed è completato. Poiché il metodo Copy in questo esempio usa la parola chiave unsafe, deve essere compilato con l'opzione del compilatore AllowUnsafeBlocks.

L'esempio accede agli elementi di entrambe le matrici usando gli indici invece di un secondo puntatore non gestito. La dichiarazione dei puntatori pSource e pTarget blocca le matrici.

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
    */
}

Puntatori funzione

C# fornisce tipi delegate per definire oggetti puntatore a funzione sicuri. La chiamata di un delegato comporta la creazione di un'istanza di un tipo derivato da System.Delegate e l'esecuzione di una chiamata di metodo virtuale al relativo metodo Invoke. Questa chiamata virtuale usa l'istruzione IL callvirt. Nei percorsi di codice critici per le prestazioni, l'uso dell'istruzione IL calli è più efficiente.

È possibile definire un puntatore a funzione usando la sintassi delegate*. Il compilatore chiamerà la funzione usando l'istruzione calli anziché creare un'istanza di un oggetto delegate e chiamando Invoke. Il codice seguente dichiara due metodi che usano un oggetto delegate o delegate* per combinare due oggetti dello stesso tipo. Il primo metodo usa un tipo delegato System.Func<T1,T2,TResult>. Il secondo metodo usa una dichiarazione delegate* con gli stessi parametri e tipo restituito:

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);

Il codice seguente illustra come dichiarare una funzione locale statica e richiamare il metodo UnsafeCombine usando un puntatore a tale funzione locale:

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

Il codice precedente illustra diverse regole sulla funzione a cui si accede come puntatore a funzione:

  • I puntatori a funzione possono essere dichiarati solo in un contesto unsafe.
  • I metodi che accettano un oggetto delegate* (o restituiscono un oggetto delegate*) possono essere chiamati solo in un contesto unsafe.
  • L’operatore & per ottenere l'indirizzo di una funzione è consentito solo sulle funzioni static. (Questa regola si applica sia alle funzioni membro che alle funzioni locali).

La sintassi ha paralleli con la dichiarazione dei tipi delegate e l'uso di puntatori. Il suffisso * su delegate indica che la dichiarazione è un puntatore a funzione. Il & quando si assegna un gruppo di metodi a un puntatore a funzione indica che l'operazione accetta l'indirizzo del metodo.

È possibile specificare la convenzione di chiamata per un oggetto delegate* usando le parole chiave managed e unmanaged. Inoltre, per i puntatori a funzione unmanaged, è possibile specificare la convenzione di chiamata. Le dichiarazioni seguenti mostrano esempi di ognuno di essi. La prima dichiarazione usa la convenzione di chiamata managed, ovvero l'impostazione predefinita. Le quattro successive usano una convenzione di chiamata unmanaged. Ognuno specifica una delle convenzioni di chiamata ECMA 335: Cdecl, Stdcall, Fastcall o Thiscall. L'ultima dichiarazione usa la convenzione di chiamata unmanaged, che indica a CLR di selezionare la convenzione di chiamata predefinita per la piattaforma. CLR sceglierà la convenzione di chiamata in fase di esecuzione.

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);

Per altre informazioni sui puntatori a funzione, vedere la specifica della funzionalità Puntatore a funzione.

Specifiche del linguaggio C#

Per altre informazioni, vedere il capitolo codice unsafe della specifica del linguaggio C#.