shared_ptr ou comment éviter les fuites mémoires en C++ (part 2/3)

Dans ma série de mes découvertes sur les nouveautés de C++ avec Visual Studio 2008 (SP1) et 2010 j’ai décidé de vous faire un post supplémentaire sur le shared_ptr. En effet cette petite classe est bien sympathique et fourni des “mécaniques” pratiques notamment sur la destruction “personnalisée” de vos pointeurs.

Il est relativement courant d’utiliser des factory pour instancier des objets de manières un peu particulières et en général dans ce cas on a aussi des méthodes particulières pour détruire ces objets. C’est assez typique quand on utilise des méthodes de l’API de windows où il faut appeler d’autres méthodes pour libérer des ressources. J’ai cherché un cas concret et c’est vrai que ce qui me plais le plus avec le c++ de visual studio aujourd’hui c’est sa faculté à gérer le C++ natif et le C++ managé en même temps. L’interop quoi ! 🙂

Donc ce cas concret le voilà. J’ai une application à développer qui doit répondre à un modèle particulier “natif”. C’est à dire que pour s’intégrer dans l’application globale mon projet doit implémenter un certain nombre de méthodes native. Et bien sûr moi à l’intérieur de tout ça j’utilise du managé ! Mais cette API ne me permet de faire transiter que des pointeurs de type (void*) pour récupérer mes billes… Donc je passe mon temps à stocker mes object managés en pointeurs natifs (via GCHandle et IntPtr) ou l’inverse. Et bien sûr ici j’ai déjà un risque de fuite mémoires.

Trêve de blablas, voilà un exemple de code qui sera plus parlant. J’ai fait 2 “helper” méthodes pour gérer cette conversion :

void* GetAsNativePointer(Object^ managedObj)
        {
        GCHandle gcPtr = GCHandle::Alloc(managedObj);
        IntPtr ptr = GCHandle::ToIntPtr(gcPtr);
        return ptr.ToPointer();
        }

        Object^ GetAsManagedPointer(void* nativeObj)
        {
        IntPtr ptr = IntPtr(nativeObj);
        GCHandle gcPtr = GCHandle::FromIntPtr(ptr);
        if(gcPtr.IsAllocated==false)
        return  nullptr;
        Object^ toReturn = gcPtr.Target;
        return toReturn;
        }

Ici j’ai tout de suite une fuite mémoire si j’appelle régulièrement ces méthodes sans jamais désallouer le GCHandle qui est crée par la 1er méthode. Par exemple le main ci dessous et le résultat sur perfmon :

int main(array<System::String ^> ^args)
        {
        Console::WriteLine(L"Hello Leaks !");
        CMyObject^ obj = gcnew CMyObject();
        obj->Data = "String datas to fill a bit of memory";

        for(int a=0;a<99999999;a++)
        {
        void* nativeObj = GetAsNativePointer(obj);
        Object^ managed = GetAsManagedPointer(nativeObj);
        Object^ managed2 = GetAsManagedPointer(nativeObj);
        }
        Console::WriteLine(L"Leaks finished!");
        Console::ReadLine();
        return 0;
        }

image

Il faut donc que je modifie mon main pour utiliser une méthode de nettoyage du GCHandle. ci dessous la méthode en question (notez l’appel à Free() sur le GCHandle), le main et le résultat perfmon :

void FreeNativePointerPointingOnManagedObj(void* nativeObj)
        {
        IntPtr ptr = IntPtr(nativeObj);
        GCHandle gcPtr = GCHandle::FromIntPtr(ptr);
        if(gcPtr.IsAllocated)
        {
        gcPtr.Free();
        }
        }

        (...)

        int main(array<System::String ^> ^args)
        {
        Console::WriteLine(L"Hello Leaks !");
        CMyObject^ obj = gcnew CMyObject();
        obj->Data = "String datas to fill a bit of memory";

        for(int a=0;a<99999999;a++)
        {
        void* nativeObj = GetAsNativePointer(obj);
        Object^ managed = GetAsManagedPointer(nativeObj);
        Object^ managed2 = GetAsManagedPointer(nativeObj);

        //nettoyage
        FreeNativePointerPointingOnManagedObj(nativeObj);
        }

        Console::WriteLine(L"Leaks finished!");
        Console::ReadLine();
        return 0;
        }

image

Bon maintenant je pense que vous voyez où je veux en venir ! Cette méthode FreeNativePointerOnManagedObj est “pénible” à appeler, et encore une fois il faut que je prévois tout les cas de sortie de mon code pour être sûr de bien appeler cette méthode quoiqu’il arrive. Je vais donc modifier mon code pour utiliser un shared_ptr<void> au lieu d’un void*. En plus lors de la création de ce shared_ptr, je vais lui fournir la méthode FreeNativePointerOnManagedObj en tant que deleter. C’est à dire que lorsque le compteur de référence de mon shared_ptr sera à 0, au lieu d’appeler le destructeur du void contenu dedans, il appellera le deleter en question !

Je change donc la signature de mes méthodes pour faire transiter un shared_ptr de void, notez l’instanciation de ce pointeur avec un deleter : shared_ptr<void> p (ptr.ToPointer(),FreeNativePointerPointingOnManagedObj);

L’ensemble du code final :

//on garde poiur le deleter une signature qui renvoi void et
        //	prend le pointeur du même type que celui assigné au shared_ptr ici un void* donc
        void FreeNativePointerPointingOnManagedObj(void* nativeObj)
        {
        IntPtr ptr = IntPtr(nativeObj);
        GCHandle gcPtr = GCHandle::FromIntPtr(ptr);
        if(gcPtr.IsAllocated)
        {
        gcPtr.Free();
        }
        }

        //on remplace le type de retour par le shared_ptr
        shared_ptr<void> GetAsNativePointer(Object^ managedObj)
        {
        GCHandle gcPtr = GCHandle::Alloc(managedObj);
        IntPtr ptr = GCHandle::ToIntPtr(gcPtr);
        //création du pointeur en lui passant le deleter
        shared_ptr<void> p (ptr.ToPointer(),FreeNativePointerPointingOnManagedObj);
        return p;
        }

        //on change la signature du parametre
        Object^ GetAsManagedPointer(shared_ptr<void> nativeObj)
        {
        //on utilise la méthode get pour récupérer le void*
        IntPtr ptr = IntPtr(nativeObj.get());
        GCHandle gcPtr = GCHandle::FromIntPtr(ptr);
        if(gcPtr.IsAllocated==false)
        return  nullptr;
        Object^ toReturn = gcPtr.Target;
        return toReturn;
        }

        //dans le main on change juste le type de pointeur natif qu'on trimballe
        // et on a plus besoin de penser au nettoyage !
        int main(array<System::String ^> ^args)
        {
        Console::WriteLine(L"Hello Leaks !");
        CMyObject^ obj = gcnew CMyObject();
        obj->Data = "String datas to fill a bit of memory";

        for(int a=0;a<99999999;a++)
        {
        shared_ptr<void> nativeObj = GetAsNativePointer(obj);
        Object^ managed = GetAsManagedPointer(nativeObj);
        Object^ managed2 = GetAsManagedPointer(nativeObj);

        //ici plus besoin de nettoyage, le shared_ptr s'en charge
        }

        Console::WriteLine(L"Leaks finished!");
        Console::ReadLine();
        return 0;
        }

Bon je vous remet pas le graph perfmon, c’est le même que précédemment, c’est à dire calme plat sur la mémoire ! 😀

La prochaine étape, mettre des helper méthode sous forme d’une jolie classe dédiée à ce genre de mécanique en interop !…

voi aussi shared_ptr ou comment éviter les fuites mémoires en C++ 1/3

et shared_ptr ou comment éviter les fuites mémoires en C++ 3/3

Tagués avec : ,

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Verifions que vous êtes un humain * Time limit is exhausted. Please reload CAPTCHA.