Code, types de pointeurs et pointeurs de fonction non sécurisés

La plupart du code C# que vous écrivez est un « code sûr vérifiable ». Le code vérifiable sécurisé signifie que les outils .NET peuvent vérifier que le code est sécurisé. En général, le code sécurisé n’accède pas directement à la mémoire à l’aide de pointeurs. Il n’alloue pas non plus de mémoire brute. Il crée des objets managés à la place.

C# prend en charge un contexte unsafe dans lequel vous pouvez écrire du code non vérifiable . Dans un contexte unsafe, le code peut utiliser des pointeurs, allouer et libérer des blocs de mémoire, et appeler des méthodes à l’aide de pointeurs de fonction. Le code non sécurisé en C# n’est pas nécessairement dangereux : il s’agit simplement de code dont la sécurité ne peut pas être vérifiée.

Le code non sécurisé a les propriétés suivantes :

  • Les méthodes, les types et les blocs de code peuvent être définis comme non sécurisés.
  • Dans certains cas, le code non sécurisé peut augmenter les performances d’une application en supprimant les vérifications des limites des tableaux.
  • Du code non sécurisé est obligatoire quand vous appelez des fonctions natives nécessitant des pointeurs.
  • L’utilisation de code non sécurisé introduit des risques pour la sécurité et la stabilité.
  • Le code qui contient des blocs unsafe doit être compilé avec l’option de compilateur AllowUnsafeBlocks.

Types de pointeur

Dans un contexte non sécurisé, un type peut être un type pointeur, en plus d’un type valeur ou d’un type référence. La déclaration d'un type pointeur peut prendre l'une des formes suivantes :

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

Le type spécifié avant * dans un type de pointeur est appelé type référent. Seul un type non managé peut être un type référent.

Les types pointeur n’héritent pas de object, et aucune conversion n’est possible entre les types pointeur et object. Par ailleurs, le boxing et l'unboxing ne prennent pas en charge les pointeurs. Cependant, vous pouvez effectuer des conversions entre différents types pointeur ainsi qu'entre des types pointeur et des types intégraux.

Lorsque vous déclarez plusieurs pointeurs dans la même déclaration, vous écrivez l’astérisque (*) avec le type sous-jacent uniquement. Il n’est pas utilisé comme préfixe de chaque nom de pointeur. Par exemple :

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

Un pointeur ne peut pas pointer vers une référence ou vers un struct qui contient des références, car une référence d’objet peut être collectée par le récupérateur de mémoire, même si un pointeur pointe vers elle. Le récupérateur de mémoire ne se préoccupe pas de savoir si un objet est pointé par des types pointeur.

La valeur de la variable pointeur de type MyType* est l'adresse d'une variable de type MyType. Les éléments suivants sont des exemples de déclarations de type pointeur :

  • int* p: p est un pointeur vers un entier.
  • int** p: p est un pointeur vers un pointeur vers un entier.
  • int*[] p: p est un tableau unidimensionnel de pointeurs vers des entiers.
  • char* p: p est un pointeur vers un caractère.
  • void* p: p est un pointeur vers un type inconnu.

L'opérateur d'indirection de pointeur * peut être utilisé pour accéder au contenu à l'emplacement vers lequel pointe la variable pointeur. Observez par exemple la déclaration suivante :

int* myVariable;

L'expression *myVariable désigne la variable int trouvée à l'adresse contenue dans myVariable.

Il existe plusieurs exemples de pointeurs dans les articles sur l’instructionfixed. L’exemple suivant utilise le mot clé unsafe et les instructions fixed, et montre comment incrémenter un pointeur intérieur. Vous pouvez coller ce code dans la fonction Main d'une application console pour l'exécuter. Ces exemples doivent être compilés avec l’ensemble d’options de compilateur AllowUnsafeBlocks.

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

L'opérateur d'indirection ne peut pas être appliqué à un pointeur de type void*. Toutefois, vous pouvez utiliser un cast pour convertir un pointeur void en n'importe quel autre type pointeur, et inversement.

Un pointeur peut être null. Le fait d'appliquer un opérateur d'indirection à un pointeur Null donne lieu à un comportement défini par l'implémentation.

Le passage de pointeurs entre méthodes peut engendrer un comportement non défini. Supposons une méthode qui retourne un pointeur à une variable locale par le biais d’un paramètre in, out ou ref, ou comme résultat de fonction. Si le pointeur a été défini dans un bloc fixed, la variable vers laquelle il pointe peut ne plus être fixed.

Le tableau suivant répertorie les opérateurs et les instructions qui peuvent fonctionner sur des pointeurs dans un contexte unsafe :

Opérateur/Instruction Utilisation
* Exécute l'indirection de pointeur.
-> Accède à un membre d'un struct via un pointeur.
[] Indexe un pointeur.
& Obtient l'adresse d'une variable.
++ et -- Incrémente et décrémente les pointeurs.
+ et - Exécute des opérations arithmétiques sur les pointeurs.
==, !=, <, >, <= et >= Compare des pointeurs.
stackalloc Alloue de la mémoire sur la pile.
instruction fixed Résout temporairement une variable afin de pouvoir rechercher son adresse.

Pour plus d’informations sur les opérateurs associés au pointeur, consultez Opérateurs associés au pointeur.

N’importe quel type de pointeur peut être converti implicitement en type void*. La valeur null peut être attribuée à n’importe quel type de pointeur. N’importe quel type de pointeur peut être explicitement converti en n’importe quel autre type de pointeur à l’aide d’une expression cast. Vous pouvez également convertir n’importe quel type intégral en type de pointeur ou n’importe quel type de pointeur en type intégral. Ces conversions nécessitent un cast explicite.

L’exemple suivant convertit un int* en byte*. Notez que le pointeur pointe vers l’octet traité le plus faible de la variable. Quand vous incrémentez successivement le résultat, jusqu’à la taille de int (4 octets), vous pouvez afficher les octets restants de la variable.

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

Mémoires tampons de taille fixe

Vous pouvez utiliser le mot clé fixed pour créer une mémoire tampon avec un tableau de taille fixe dans une structure de données. Les mémoires tampons de taille fixe sont utiles quand vous écrivez des méthodes qui sont interopérables avec les sources de données d’autres langages ou plateformes. La mémoire tampon fixe peut accepter tous les attributs ou modificateurs qui sont autorisés pour les membres de structures régulières. La seule restriction est que le tableau doit être de type bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float ou double.

private fixed char name[30];

Dans du code sûr, un struct C# qui contient un tableau ne contient pas les éléments du tableau. Le struct contient une référence aux éléments du tableau à la place. Vous pouvez incorporer un tableau de taille fixe dans un struct quand il est utilisé dans un bloc de code unsafe.

La taille des éléments suivants struct ne dépend pas du nombre d’éléments dans le tableau, car pathName il s’agit d’une référence :

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

Un struct peut contenir un tableau incorporé dans du code unsafe. Dans l’exemple suivant, le tableau fixedBuffer a une taille fixe. Vous utilisez une fixed instruction pour obtenir un pointeur vers le premier élément. Accédez aux éléments du tableau par le biais de ce pointeur. L’instruction fixed épingle le champ de l’instance fixedBuffer à un emplacement spécifique de la mémoire.

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

La taille du tableau char de 128 éléments est de 256 octets. Les mémoires tampons char de taille fixe acceptent toujours 2 octets par caractère, quel que soit l’encodage. Cette taille de tableau est la même lorsque les mémoires tampons char sont marshalées vers des méthodes ou des structs d’API avec CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Pour plus d’informations, consultez CharSet.

L’exemple précédent illustre l’accès aux champs fixed sans épinglage. Un autre tableau courant de taille fixe est le tableau bool. Les éléments d’un tableau bool sont toujours d’un octet. Les tableaux bool ne conviennent pas à la création de tableaux d’octets ou de mémoires tampons.

Les mémoires tampons de taille fixe sont compilées avec System.Runtime.CompilerServices.UnsafeValueTypeAttribute qui indique au CLR qu’un type contient un tableau non managé susceptible de dépasser sa capacité. La mémoire allouée à l’aide de stackalloc active également automatiquement les fonctionnalités de détection de dépassement de mémoire tampon dans le CLR. L’exemple précédent montre comment une mémoire tampon de taille fixe peut exister dans un unsafe struct.

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

Le C# généré par le compilateur pour Buffer est attribué comme suit :

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

Les mémoires tampons fixes diffèrent des tableaux normaux des manières suivantes :

  • Ne peuvent être utilisées que dans un contexte unsafe.
  • Il ne peut s’agir que de champs d’instance de structs.
  • Ce sont toujours des vecteurs ou des tableaux unidimensionnels.
  • La déclaration doit inclure la longueur, par exemple fixed char id[8]. Vous ne pouvez pas utiliser fixed char id[].

Comment utiliser des pointeurs pour copier un tableau d’octets

L’exemple suivant utilise des pointeurs pour copier des octets d’un tableau à un autre.

Cet exemple utilise le mot clé unsafe, qui vous permet d’utiliser des pointeurs dans la méthode Copy. L’instruction fixed permet de déclarer des pointeurs vers les tableaux source et de destination. L’instruction fixedépingle l’emplacement des tableaux source et de destination en mémoire pour que ceux-ci ne soient pas déplacés par l’opération de garbage collection. Les blocs de mémoire des tableaux sont libérés quand le bloc fixed est effectué. Comme la méthode Copy dans cet exemple utilise le mot clé unsafe, elle doit être compilée avec l’option de compilateur AllowUnsafeBlocks.

Cet exemple accède aux éléments des deux tableaux au moyen d’index et non au moyen d’un second pointeur non managé. La déclaration des pointeurs pSource et pTarget épingle les tableaux.

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

Pointeurs fonction

C# fournit des types delegate pour définir des objets pointeurs de fonction sécurisés. L’appel d’un délégué implique l’instanciation d’un type dérivé de System.Delegate et l’appel d’une méthode virtuelle à sa méthode Invoke. Cet appel virtuel utilise l’instruction IL callvirt. Dans les chemins du code critiques pour les performances, l’utilisation de l’instruction IL calli est plus efficace.

Vous pouvez définir un pointeur de fonction à l’aide de la syntaxe delegate*. Le compilateur appelle la fonction à l’aide de l’instruction calli plutôt que d’instancier un objet delegate et d’appeler Invoke. Le code suivant déclare deux méthodes qui utilisent un delegate ou un delegate* pour combiner deux objets du même type. La première méthode utilise un type délégué System.Func<T1,T2,TResult>. La deuxième méthode utilise une déclaration delegate* avec les mêmes paramètres et le même type de retour :

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

Le code suivant montre comment déclarer une fonction locale statique et appeler la méthode à l’aide d’un pointeur UnsafeCombine vers cette fonction locale :

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

Le code précédent illustre plusieurs des règles sur la fonction accessible en tant que pointeur de fonction :

  • Les pointeurs de fonction ne peuvent être déclarés que dans un contexte unsafe.
  • Les méthodes qui prennent un delegate* (ou retournent un delegate*) ne peuvent être appelées que dans un contexte unsafe.
  • L’opérateur & permettant d’obtenir l’adresse d’une fonction est autorisé uniquement sur les fonctions static. (Cette règle s’applique aux fonctions membres et aux fonctions locales).

La syntaxe comporte des parallèles avec la déclaration de types delegate et l’utilisation de pointeurs. Le suffixe * sur delegate indique que la déclaration est un pointeur de fonction. L’affectation d’un groupe de méthodes à un pointeur de fonction indique que l’opération & prend l’adresse de la méthode.

Vous pouvez spécifier la convention d’appel pour un delegate* à l’aide des mots clés managed et unmanaged. En outre, pour les pointeurs de fonction unmanaged, vous pouvez spécifier la convention d’appel. Les déclarations suivantes présentent des exemples de chacune d’elles. La première déclaration utilise la convention d’appel managed, qui est la valeur par défaut. Les quatre suivantes utilisent une convention d’appel unmanaged. Chacune spécifie l’une des conventions d’appel ECMA 335 : Cdecl, Stdcall, Fastcall ou Thiscall. La dernière déclaration utilise la convention d'appel unmanaged, ce qui indique au CLR de choisir la convention d’appel par défaut pour la plateforme. Le CLR choisit la convention d’appel au moment de l’exécution.

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

Pour en savoir plus sur les pointeurs de fonction, consultez la spécification des fonctionnalités des Pointeurs de fonction.

spécification du langage C#

Pour en savoir plus, consultez le chapitre Code unsafe de la spécification du langage C#.