Code non sécurisé et pointeurs en C#
Apprenez le code non sécurisé et les pointeurs en C# pour manipuler la mémoire et les opérations bas niveau.
Bien que le langage C# dispose d’une gestion de mémoire sûre, certaines situations à haute performance
nécessitent un accès direct à la mémoire. Le code unsafe (non sécurisé) est utilisé dans ces cas.
Les blocs unsafe permettent d’accéder directement aux adresses mémoire via des pointeurs.
Cette approche offre un contrôle de bas niveau, mais elle doit être utilisée avec prudence,
car elle fonctionne en dehors de la protection du Garbage Collector (GC) de .NET.
Qu’est-ce que le code Unsafe ?
Le mot-clé unsafe indique au compilateur C# que le code va manipuler de la mémoire non managée. Dans ce type de code, le compilateur ne réalise pas les vérifications de sécurité mémoire. Pour utiliser le code unsafe, l’option « Allow unsafe code » doit être activée dans les paramètres du projet.
// Propriété à ajouter dans le fichier .csproj :
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Qu’est-ce qu’un pointeur ?
Un pointeur est une variable qui stocke l’adresse mémoire d’une autre variable.
En C#, les pointeurs ne peuvent être déclarés que dans des blocs unsafe et sont utilisés sur la pile (stack).
Le symbole * sert à définir un pointeur, tandis que l’opérateur & permet d’obtenir l’adresse d’une variable.
using System;
class Program
{
static unsafe void Main()
{
int x = 42;
int* ptr = &x; // obtenir l’adresse de x
Console.WriteLine($"Valeur de x : {*ptr}");
Console.WriteLine($"Adresse de x : {(ulong)ptr}");
}
}
L’expression *ptr signifie « lire la valeur à l’adresse pointée ».
Dans cet exemple, on accède à la fois à l’adresse et à la valeur de la variable.
Modifier une valeur via un pointeur
Vous pouvez modifier directement la valeur d’une variable à l’aide d’un pointeur. Cela fonctionne de la même manière qu’en C ou C++.
unsafe
{
int x = 10;
int* p = &x;
*p = 99; // modifier la valeur de x via le pointeur
Console.WriteLine($"Nouvelle valeur de x : {x}");
}
// Sortie :
// Nouvelle valeur de x : 99
Arithmétique des pointeurs
L’arithmétique des pointeurs permet de parcourir des tableaux en incrémentant les adresses mémoire.
Cependant, elle ne s’applique qu’aux types primitifs comme int, byte ou double.
unsafe
{
int[] nombres = { 10, 20, 30, 40 };
fixed (int* p = nombres)
{
for (int i = 0; i < nombres.Length; i++)
Console.WriteLine($"Adresse : {(ulong)(p + i)}, Valeur : {*(p + i)}");
}
}
Le mot-clé fixed fige le tableau en mémoire, empêchant le GC de le déplacer.
Ainsi, le pointeur pointe toujours vers la bonne adresse.
Le mot-clé fixed
fixed empêche un objet managé (par exemple un tableau ou une chaîne) d’être déplacé par le GC.
Il permet donc d’accéder en toute sécurité aux adresses mémoire via des pointeurs.
unsafe
{
string message = "Bonjour";
fixed (char* p = message)
{
for (int i = 0; i < message.Length; i++)
Console.Write(p[i]);
}
}
Remarque : une fois le bloc fixed terminé, l’objet peut de nouveau être déplacé.
L’utilisation du pointeur en dehors de ce bloc est donc risquée.
Accès mémoire avec struct et pointeurs
Il est également possible d’accéder directement à la mémoire des structures (struct) avec des pointeurs.
Cette technique est courante dans les scénarios critiques en performance (jeux, graphismes, interopérabilité native).
using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static unsafe void Main()
{
Point p = new Point { X = 5, Y = 10 };
Point* ptr = &p;
ptr->X = 20;
Console.WriteLine($"Point : X={ptr->X}, Y={ptr->Y}");
}
}
L’expression ptr->X est équivalente à la syntaxe d’accès aux structures en C/C++.
Allocation de mémoire sur la pile avec stackalloc
stackalloc alloue directement de la mémoire brute sur la pile.
Cette mémoire est automatiquement libérée à la fin du bloc.
unsafe
{
int* p = stackalloc int[5];
for (int i = 0; i < 5; i++)
p[i] = i * 2;
for (int i = 0; i < 5; i++)
Console.WriteLine(p[i]);
}
stackalloc est rapide, mais doit être utilisé uniquement pour de petites allocations.
Pour les données volumineuses, préférez l’allocation sur le tas (new).
Quand utiliser le code Unsafe ?
- Dans les scénarios critiques en performance (calculs mathématiques, moteurs de jeu, traitement d’image)
- Lors de l’interopérabilité avec du code C/C++ non managé (P/Invoke, Interop)
- Quand un accès direct à la mémoire sur de grands tableaux ou flux d’octets est nécessaire
- Dans des structures à taille fixe ou nécessitant un agencement mémoire personnalisé
Comparaison de performance
Le code unsafe avec accès direct à la mémoire peut améliorer les performances de 30 à 50 % en contournant le GC. Cependant, ce gain se fait au prix d’un risque accru. Un mauvais accès mémoire peut provoquer un crash ou une corruption de mémoire.
Points de vigilance
- Un accès à un pointeur null ou à une adresse invalide provoque une
AccessViolationException. - Ne pas utiliser de pointeur en dehors d’un bloc fixed (le GC pourrait déplacer l’objet).
- Le code unsafe est considéré comme non managé et doit être soigneusement vérifié.
- Le modèle de sécurité de l’application doit être réévalué si du code unsafe est utilisé.
- Le code unsafe ne fonctionne pas dans des environnements à confiance partielle.
Exemple : accès direct aux pixels d’une image
L’exemple suivant montre comment accéder directement aux pixels d’un bitmap avec un bloc unsafe. Cette méthode effectue les opérations de manipulation de pixels (par exemple inversion des couleurs) beaucoup plus rapidement que les API classiques.
using System;
using System.Drawing;
using System.Drawing.Imaging;
class Program
{
static unsafe void Main()
{
using Bitmap bmp = new Bitmap("image.png");
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
byte* premierPixel = (byte*)data.Scan0;
for (int y = 0; y < bmp.Height; y++)
{
byte* ligne = premierPixel + (y * data.Stride);
for (int x = 0; x < bmp.Width; x++)
{
byte* pixel = ligne + (x * bytesPerPixel);
pixel[0] = (byte)(255 - pixel[0]); // Inverser le bleu
pixel[1] = (byte)(255 - pixel[1]); // Inverser le vert
pixel[2] = (byte)(255 - pixel[2]); // Inverser le rouge
}
}
bmp.UnlockBits(data);
bmp.Save("output.png");
Console.WriteLine("L’image a été inversée !");
}
}
Cet exemple démontre la puissance du code unsafe pour les opérations de manipulation de pixels haute performance.
TL;DR
- Les blocs unsafe permettent un accès direct à la mémoire via des pointeurs en C#.
- Le mot-clé fixed empêche le GC de déplacer les objets en mémoire.
- stackalloc alloue une mémoire rapide et temporaire sur la pile.
- Offre des gains de performance, mais présente des risques de sécurité élevés.
- Utilisé principalement pour l’interopérabilité avec du code non managé (C/C++) ou la manipulation bas-niveau de la mémoire.
Articles connexes
Gestion de la mémoire et Garbage Collector en C#
Apprenez la gestion de la mémoire et le garbage collector en C# pour comprendre le cycle de vie des objets.
Optimisation des performances avec Span<T> et Memory<T> en C#
Apprenez l’optimisation des performances en C# avec Span<T> et Memory<T> pour une gestion mémoire efficace.
Structs en C# – Différences avec les classes
Découvrez les différences entre structs et classes en C#, notamment le modèle mémoire, l’héritage et les performances.
Types de données de base en C#
Types de données de base en C# : numériques, textuels, logiques, orientés objet et nullables.