Gestion de la mémoire managée et non managée en C#

Le langage C# intègre un mécanisme de gestion automatique de la mémoire, le Garbage Collector (GC), qui alloue et désalloue la mémoire dite "managée". La majorité du temps, les développeurs C# évoluent dans cet environnement et n'ont pas à se soucier de la libération explicite des ressources. Cependant, dans certains scénarios, notamment lors de l'interaction avec des bibliothèques natives écrites en C++ ou d'autres langages bas niveau, il devient nécessaire de manipuler directement la mémoire non managée. Dans ces cas, la responsabilité de l'allocation et de la désallocation de cette mémoire incombe au développeur.

C# fournit des outils spécifiques pour faciliter la conversion entre la mémoire managée et non managée, ainsi que pour opérer sur cette dernière.

1. Conversion de la mémoire managée vers la mémoire non managée

Cette situation se présente typiquement lorsqu'une fonction d'une bibliothèque native attend un pointeur vers des données. Par exemple, une fonction C++ comme void func(float* data, int length) nécessite un bloc de mémoire non managée pour ses opérations. Passer directement de la mémoire managée C# pourrait entraîner des problèmes : le GC pourrait déplacer ou supprimer la mémoire pendant que la fonction native l'utilise, ou le pointeur pourrait devenir invalide.

Voici comment gérer cette conversion :


using System.Runtime.InteropServices;

// Données gérées en C#
float[] managedArray = { 1.0f, 2.5f, 3.7f };

// Alloue un handle pour le tableau managé, le rendant non déplaçable par le GC.
// GCHandleType.Pinned garantit que l'objet reste à la même adresse mémoire.
GCHandle pinnedHandle = GCHandle.Alloc(managedArray, GCHandleType.Pinned);

try
{
    // Récupère l'adresse mémoire fixe du tableau non managé.
    IntPtr unmanagedPointer = pinnedHandle.AddrOfPinnedObject();

    // Appelle la fonction native en passant le pointeur non managé et la taille.
    // Supposons que 'NativeLibrary.func' est une fonction déclarée via P/Invoke.
    // NativeLibrary.func(unmanagedPointer, managedArray.Length);
}
finally
{
    // Libère le handle GCHandle. Le tableau managé peut maintenant être géré par le GC.
    pinnedHandle.Free();
}

2. Conversion de la mémoire non managée vers la mémoire managée

Il est fréquent qu'une bibliothèque native alloue et retourne un bloc de mémoire qui doit ensuite être utilisé en C#. Une signature de fonction C++ comme int func(int** data), où data pointe vers le pointeur qui recevra l'adresse mémoire alouée, est un exemple typique.

La procédure pour lire ces données en C# est la suivante :


using System.Runtime.InteropServices;

// IntPtr sera utilisé pour recevoir le pointeur vers la mémoire non managée retournée par la fonction native.
IntPtr nativeBufferPtr = IntPtr.Zero;

// Appelle la fonction native qui alloue de la mémoire et retourne son pointeur.
// L'utilisation de 'out' ici permet de passer une référence à IntPtr, similaire à un double pointeur en C++.
// Supposons que 'NativeLibrary.allocateAndGetData' retourne la taille des données allouées.
int dataSize = NativeLibrary.allocateAndGetData(out nativeBufferPtr);

byte[] managedData = null;
if (dataSize > 0 && nativeBufferPtr != IntPtr.Zero)
{
    // Crée un tableau managé pour recevoir les données.
    managedData = new byte[dataSize];
    // Copie les données de la mémoire non managée vers le tableau managé.
    Marshal.Copy(nativeBufferPtr, managedData, 0, dataSize);

    // Libère la mémoire non managée allouée par la bibliothèque native.
    // Il est crucial de libérer cette mémoire pour éviter les fuites.
    Marshal.FreeHGlobal(nativeBufferPtr);
}

// 'managedData' contient maintenant les données accessibles en C#.

3. Allocation directe de mémoire non managée en C# pour usage natif

Dans certains cas, il est nécessaire d'allouer explicitement un bloc de mémoire non managée directement depuis C# pour le passer à une fonction native. Cette mémoire doit également être gérée explicitement.


using System.Runtime.InteropServices;

// Taille en octets de la mémoire non managée à allouer.
int bufferSize = 1024;

// Alloue un bloc de mémoire non managée de 'bufferSize' octets.
IntPtr directUnmanagedPtr = Marshal.AllocHGlobal(bufferSize);

try
{
    // Passe le pointeur vers la mémoire non managée à une fonction native.
    // Supposons que 'NativeLibrary.processMemory' utilise ce bloc de mémoire.
    // NativeLibrary.processMemory(directUnmanagedPtr, bufferSize);
}
finally
{
    // Libère la mémoire non managée allouée.
    Marshal.FreeHGlobal(directUnmanagedPtr);
}

La classe Marshal offre une panoplie d'autres méthodes pour interagir avec la mémoire non managée, comme la conversion entre différents types de données et les structures.

En résumé, la conversion entre mémoire managée et non managée en C# s'effectue principalement via les classes Marshal et GCHandle. La règle d'or est de toujours s'assurer de libérer explicitement toute mémoire non managée qui a été allouée, que ce soit par le code natif ou directement depuis C# via Marshal.AllocHGlobal.

Étiquettes: C# Gestion Mémoire mémoire managée mémoire non managée P/Invoke

Publié le 14 juillet à 16h53