Monday, November 08, 2004

 

GDI and GDI+ leaks

1.

To tell the truth, I am not very sure whether this is a good way or not.

原文出处 http://www.relisoft.com/Win32/GdiLeaks.html
翻译出处 未知

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 resources 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.
使用 资源管理 技术,你不能仅仅保证你的程序无内存泄漏,还要保证没有其它资源泄漏. 在一个 Windows 程序内, GDI 物件是资源中的重要一类,是需要资源管理技术保护的. 在我们自己的项目中, Code Co-op, 我们广泛使用资源管理, 因此我们不希望有任何泄漏. 设想一下, 当我们发现调度器--Code Co-op的一个重要组件--成了个 GDI 资源的消耗大户时, 我们相当惊讶.

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.
首先, 你怎样发现这种事情? 这就体现出了旧的任务管理器可显示任何正在运行的进程占用的资源的好处了. 使用任务管理器的的 查看>选择列...选单项并且单击 GDI 对象检查框. 一个新的纵列GDI 对象会出现在进程视图内.如果显示在这一列的某个数字随着时间增长, 你就知道有资源泄漏了.

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.
观察调度器进程, 我们注意到,在每次调度器检查 email 时,它对GDI物件的分配就会增加. 现在我们就得精确查找我们的代码在哪里发生了资源泄漏. 做到这一点的方法就是在一个指定的操作之前和之后统计 GDI 物件的使用. 最容易(并且是最可靠的)的方法就是资源的构造器和析构器内定义一个计数器物件, 如果数值不一样, 它就显示差别. 下面就是我们为这个目的设计的一个类:

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.
这种封装很有用处. 自从我们必恭必敬的使用资源管理时, 我就将所有资源都封装进智能物件. 在这些物件的析构器里,资源自动释放. 因此如果你在DbgGuiLeak的生存期内定义任何智能物件, 在 DbgGuiLeak的析构器之前, 物件的析构器可保证被调用. C++ 语言能保证这种后进先出的栈物件的排列方式. 换句话说, 从资源释放的任意顺序上, 你不会看到任何虚假的泄漏结果. 比较下边的两个例子.

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).
在第一个例子内, 泄漏计数器会看到没有泄漏.(gidObj的析构器将在leakCounter的析构器调用之前释放它的GDI资源)

{
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.
第二个例子, 一个虚假的泄漏将会出现,只是因为gidObj的隐式的析构器是在最后的显式的代码行被执行之后才被调用. (按:这种情况在调试中常常出现,十分讨厌)

{ // 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
使用 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.
为了访问 MAPI 的例子, 我们得使用 mapi32.dll, 一个位于 Windows 系统目录的动态加载的库文件. 一个 DLL 是个资源, 因此它得被管理(资源管理的方式, 不是 .NET 的方式). 我们定义一个简单类, Dll, 来处理加载和释放任何 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.
调用一个定义于DLL内的函数, 无论是按照顺序还是像我们这里的做法,按照函数名,你必须先获得函数的指针. 你不能仅仅知道函数的名字, 而且, 因为你将调用它, 你还得知道它的函数原型. 函数名作为一个字符参数传递给Dll::GetFunction, 但是识别标志(一个类型)必须作为一个参数模板传给Dll::GetFunction. 实质上, 我们将 API 返回的无类型指针作为有适当参数识别标志的函数指针.

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

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

template
void GetFunction (std::string const & funcName, T & funPointer)
{
funPointer = static_cast (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的构造器调用LoadLibrary API 函数. (严格来讲, 我们不需要保存DLL的名字, 但可以方便我们显示更直观的错误报告. )

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 资源 ---- 调用 FreeLibrary API 函数.

Dll::~Dll ()
{
if (_h != 0)
{
::FreeLibrary (_h);
}
}
A (typeless) function pointer is retrieved from the DLL by calling the GetProcAddress API.
从 DLL 内获得一个(无类型)的函数指针要通过调用 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.
顺便说一下, 调用模板成员函数Dll::GetFunction时, 你不必明确的指定模板参数. 编译器会从第二个参数的类型推导出来的. 一会儿你会看到一个例子.

Simple MAPI
简单的 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).
为了我们的测试目的, 我们从简单MAPI中的操作都用日志的方式记录下来. 登录的结果是个事务. 既然事务与内部资源有关(包含某些 GDI 资源, 这正是我们感兴趣的部分), 它得用资源管理的方式被管理. 我们定义一个 SimpleMapi::Session 来封装这个资源. 在构造器内调用MAPILogon,在析构器内调用MAPILogoff. 这两个函数的原型定义识别标志定义在类的定义内部(这是我们需要它的唯一场合). Logon 和 Logoff 都是很特殊的指针到函数的类型. 这些 typedef 语句不止指出了返回和参数类型, 还标示出了调用约定(这个例子是 FAR PASCAL).
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.
这就是简易 MAPI 事务的构造器. 请留意我们调用 Dll类的GetFunction 模板过程, 但并不指定模板参数. 这是因为我们已经通过 logon参数的Logon类型(察看上面的 typedef 语句)为编译器提供了足够的资讯. (正确类型的)函数指针一旦初始化, 我们就能够通过它用普通的函数调用语法来做一个调用了.

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.
SimpleMapi::Session的析构器调用MAPILogoff函数, 因而关闭事务并(满怀希望地!)释放它自己分配的所有资源.

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.
这里是我们的简易测试. 首先我们创建一个 DbgGuiLeak 物件, 它会记得测试开始之前使用了多少 GUI 资源. 然后我们创建 Dll 物件, 它加载 mapi32.dll 到我们的地址空间. 最后, 我们创建 SimpleMapi::Session 物件,它展示给我们进入 MAPI 子系统的记录.
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.
接下来的事情就是 按构造时的相反顺序调用所有析构器. 因此 SimpleMapi::Session 析构器首先告诉我们已退出登陆并关闭事务. 然后 Dll 的析构器释放DLL. 最后, DbgGuiLeak 的析构器检测任何任何GUI资源是否发生了泄漏.

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).
我们程序的 main 函数调用 TestMapi 三次, 来获得任何可能的结果(不是我们想象的那样一切OK, 因为第一次登录有资源泄漏 -- 但它不应该是这样的灾难).

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
#include
#include

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.
现在你准备编译运行测试程序了. 结果与预期很不一样, 这有赖于你计算机上的 EMAIL 客户端安装数量和你从 Microsoft 下载的安全补丁的数量. 这里是已很好地升级了 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.
第一次登录泄漏了 16 个GDI资源, 很糟糕, 但并不是很严重. 真正糟糕的是每一次并发的登录/退出登录动作序列就泄漏额外的两个资源.

2.
GDI+ and MFC memory leak detection
By Zoltan Csizmadia

http://www.codeproject.com/vcpp/gdiplus/gdiplush.asp

3.
Leak Checking for GDI+
By Michael Mondry

Describes how to check your app for GDI+ leaks

http://www.codeproject.com/vcpp/gdiplus/leakchkgdiplus.asp

4.

1) CDC是基类
2) CPaintDC可以说是为WM_PAINT专用的,它比CDC多了一个PAINTSTRUCT 结构,它只能操作客户区。
有一点要注意:你调用了基类的OnPaint,那就不要再定义这种对象了,而改用其它。
3) CClientDC 只能操作客户区,实际它在构造中调用了GetDC,然后在析构中调用了ReleaseDC。
(所以我们可以定义一CDC指针,通过GetDC,便可看与CClientDC 一样的操作,因为CClientDC 并未增加任何方法。
但要记得用完后要调用ReleaseDC,否则资源泄漏!)
4) CWindowDC可以操作所有区域,如果要在非客户区操作,那就得使用它了,但实际上它也未增加任何方法。
只是构造时调用了CWindowDC,析构时调用ReleaseDC。


Still, not a good way to handle GDI and GDI+ leaks. We need to check and check to make sure everything is OK.



<< Home

This page is powered by Blogger. Isn't yours?