weidagang2046的专栏

物格而后知致
随笔 - 8, 文章 - 409, 评论 - 101, 引用 - 0
数据加载中……

GDI Resource Leaks

Using the techniques of Resource Management you can not only guarantee that your program will leak no memory, it will also leak no other resources. In a Windows program, GDI objects are an important class of resorces that should be protected using Resource Management techniques. In our own product, Code Co-op, we make extensive use of Resource Management, so we don't expect any leaks. Imagine our surprise when we found out that the Dispatcher, an important component of Code Co-op, turned out to be a voracious consumer of GDI resources.

First of all, how do you find out such a thing? It turns out that the good old Task Manager can display GDI resources consumed by any running process. Use the Task Manager's menu item View>Select Columns and click the GDI Objects checkbox. A new column GDI Object will appear in the Processes view. If a number displayed in that column keeps increasing with time, you know there is a leak.

By watching the Dispatcher process, we noticed that its allotment of GDI objects kept increasing every time the Dispatcher was checking email. Now we had to pinpoint exactly where in our code these resources were leaking. The way to do it is to count the GDI object usage before and after a given operation. The easiest (and most reliable) way to do it is to define an object which counts the resources first in its constructor and then in its destructor. If the numbers differ, it displays the difference. Here's the class we designed for this purpose.

class DbgGuiLeak
{
public:
    explicit DbgGuiLeak ()
    {
        _guiResCount = ::GetGuiResources (::GetCurrentProcess (),
                                          GR_GDIOBJECTS);
    }
    ~DbgGuiLeak ()
    {
        int leaks = ::GetGuiResources (::GetCurrentProcess (), 
                                       GR_GDIOBJECTS) - _guiResCount;
        if (leaks != 0)
        {
            std::cout << "Gui Resources Leaked: " << leaks << std::endl;
        }
    }
private:
    unsigned _guiResCount;
};

This kind of encapsulation has many advantages. Since we are using Resource Management religiously, all our resources are encapsulated in smart objects. The resource are automatically freed in those object's destructors. So if you define any smart object within the scope of the lifetime of your DbgGuiLeak, that object's destructor is guaranteed to be called before the destructor of DbgGuiLeak. C++ language guarantees this LIFO (last-in-first-out) ordering of lifetimes of stack objects. In other words, you won't see any spurious leaks resulting from the arbitrary order of resource deallocation. Compare the two examples below.

In the first example, the leak counter will see no leaks (the destructor of gidObj will free its GDI resource before the destructor of leakCounter is called).

{
    DbgGuiLeak leakCounter;
    SmartGdiObject gdiObj;
    // destructor of gdiObj
    // destructor of leakCounter
}

In the second example, a spurious leak will be displayed only because the implicit destructor of gidObj is called after the last explicit line of code is executed.

{ // bad code!
    unsigned gdiCount = ::GetGuiResources (::GetCurrentProcess (), 
                                           GR_GDIOBJECTS);
    SmartGdiObject gdiObj;
    int leaks = ::GetGuiResources (::GetCurrentProcess (),
                                   GR_GDIOBJECTS) - gdiCount;
    std::cout << "GDI Resources Leaked: " << leaks << std::endl;
    // destructor of gdiObj
}

Using a DLL

To get access to Simple MAPI, we have to use mapi32.dll, a dynamic-load library located in the Windows system directory. A DLL is a resource, so it has to be managed (in the RM sense, not the .NET sense). We define a simple class, Dll, to take care of loading and freeing any DLL.

To call a function defined in a DLL, you have to retrieve a function pointer either by ordinal or, as we do it here, by name. You not only have to know the name of the function, but, because you would like to call it, you must know its signature. The name is passed to Dll::GetFunction as a string argument, but the signature (which is a type) must be passed as a template argument. Internally, we cast the typeless pointer returned by the API to a function pointer of the appropriate signature.

class Dll
{
public:
    Dll (std::string const & filename);
    ~Dll ();

    std::string const & GetFilename () const { return _filename; }

    template <class T>
    void GetFunction (std::string const & funcName, T & funPointer)
    {
        funPointer = static_cast<T> (GetFunction (funcName));
    }

private:
    void * GetFunction (std::string const & funcName) const;

    std::string const _filename;
    HINSTANCE _h;
};

The constructor of Dll calls the LoadLibrary API. (Strictly speaking, we don't need to store the name of the DLL, but it might get handy in case we wanted to display meaningful error reports.)

				
						Dll::Dll
				 (std::string const & filename)
    : _filename (filename),
      _h (::LoadLibrary (filename.c_str ()))
      
{
    if (_h == 0)
        throw "Cannot load dynamic link library";
}

According to the rules of Resource Management, the destructor of the Dll object must free the DLL resource --it does it by calling FreeLibrary.

				
						Dll::~Dll
				 ()
{
    if (_h != 0)
    {
        ::FreeLibrary (_h);
    }
}

A (typeless) function pointer is retrieved from the DLL by calling the GetProcAddress API.

void * Dll::GetFunction (std::string const & funcName) const
{
    void * pFunction = ::GetProcAddress (_h, funcName.c_str ());
    if (pFunction == 0)
    {
        throw "Cannot find function in the dynamic link library";
    }
    return pFunction;
}

Incidentally, when calling the template member function Dll::GetFunction you don't have to explicitly specify the template argument. The complier will deduce it from the type of the second argument. You'll see an example soon.


Simple MAPI

For the purpose of our test, all we need from Simple MAPI is to log ourselves on and off. The result of a logon is a session. Since a session is associated with internal resources (including some GDI resources, which are of particular interest to us), it has to be managed in the RM sense. We define a SimpleMapi::Session to encapsulate this resource. It calls MAPILogon in its constructor and MAPILogoff in its destructor. The signatures of these two functions are typedef'd inside the class definition (that's the only place we will need them). Logon and Logoff are both very specific pointer-to-function types. Those typedefs not only specify the return- and argument types, but also the calling conventions (FAR PASCAL in this case).
namespace SimpleMapi
{
    class Session
    {
    public:
        Session (Dll & mapi);
        ~Session ();
    private:
        Dll        &_mapi;
        LHANDLE    _h;
    private:
        typedef ULONG (FAR PASCAL *Logon) (ULONG ulUIParam,
                                           LPTSTR lpszProfileName,
                                           LPTSTR lpszPassword,
                                           FLAGS flFlags,
                                           ULONG ulReserved,
                                           LPLHANDLE lplhSession);
        typedef ULONG (FAR PASCAL *Logoff) (LHANDLE session,
                                            ULONG ulUIParam,
                                            FLAGS flFlags,
                                            ULONG reserved);
    };
}

Here is the constructor of a Simple MAPI session. Note that we call the GetFunction template method of Dll without specifying the template parameter. That's because we are giving the compiler enough information through the type Logon (see the typedef above) of the logon argument. Once the (correctly typed) function pointer is initialized, we can make a call through it using the regular function-call syntax.

namespace SimpleMapi
{
    Session::Session (Dll & mapi)
        : _mapi (mapi)
    {
        Logon logon; // logon is a pointer to function
        _mapi.GetFunction ("MAPILogon", logon);
        std::cout << "Mapi Logon" << std::endl;
        ULONG rCode = logon (0, // Handle to window for MAPI dialogs
                            0,  // Use default profile
                            0,  // No password
                            0,  // No flags
                            0,  // Reserved -- must be zero
                            &_h); // Session handle
        if (rCode != SUCCESS_SUCCESS)
            throw "Logon failed";
    }
}

The destructor of SimpleMapi::Session calls MAPILogoff function thus closing the session and (hopefully!) freeing all the resources allocated on its behalf.

namespace SimpleMapi
{
    Session::~Session ()
    {
        Logoff logoff;
        _mapi.GetFunction ("MAPILogoff", logoff);
        std::cout << "Mapi Logoff" << std::endl;
        ULONG rCode = logoff (_h, 0, 0, 0);

        if (rCode != SUCCESS_SUCCESS)
            throw "Logoff failed";
    }
}

Testing for Leaks

Here's our simple test. First we create a DbgGuiLeak object which will remember how many GUI resources are in use before the test starts. Then we create the Dll object which loads mapi32.dll into our address space. Finally, we create a SimpleMapi::Session which logs us into the MAPI subsystem.

What happens next is all the destructors are called in the reverse order of creation. So first the destructor of SimpleMapi::Session logs us off and closes the session. Then the destructor of Dll frees the DLL. Finally, the destructor of DbgGuiLeak checks if any GUI resources have been leaked.

void TestMapi ()
{
    DbgGuiLeak leak;
    Dll mapi ("mapi32.dll");
    SimpleMapi::Session session (mapi);
    // Session destructor logs off
    // Dll destructor frees library
    // DbgGuiLeak destructor prints leak info
}

The main function of our program calls TestMapi three times, to get past any transcient effects (not that we think it's ok for the first logon to leak resources--but it wouldn't be such a disaster).

int main ()
{
    try
    {
        TestMapi ();
        TestMapi ();
        TestMapi ();
    }
    catch (char const * msg)
    {
        std::cerr << msg << std::endl;
    }
}

For completeness, these are the files you have to include in the test program for it to compile:

#include <windows.h>
#include <mapi.h>
#include <iostream>

Now you are ready to compile and run the test. Results may vary, depending on what email client is installed on your computer and what security updates you have downloaded from Microsoft. Here are some typical results for a well-updated system with Microsoft Outlook Express.

C:\Test\MapiCons\Release>mapicons
Mapi Logon
Mapi Logoff
Gui Resources Leaked: 16
Mapi Logon
Mapi Logoff
Gui Resources Leaked: 2
Mapi Logon
Mapi Logoff
Gui Resources Leaked: 2

The first logon leaks 16 GDI resources, which is bad, but not terminally so. What is really bad is that every subsequent logon/logoff sequence leaks two additional resources. There is no excuse for this kind of shabby programming.

Microsoft acknowledges having a similar problem with their Exchange Server

from: http://www.relisoft.com/win32/gdileaks.html

posted on 2006-10-02 09:59 weidagang2046 阅读(716) 评论(0)  编辑  收藏 所属分类: Windows


只有注册用户登录后才能发表评论。


网站导航: