Code for this article: June99VP.exe (88KB)
George Shepherd is a Senior Software Engineer at Stingray Software where he develops Visual CASE. He co-wrote MFC Internals (Addison-Wesley, 1996).|
|Q Visual Basic® is one of my favorite development tools because it lets me do so much in so little time. And when I develop COM code using Visual Basic, the environment is very forgiving and Visual Basic cleans up after me. Specifically, I can declare strings in Visual Basic and use them within some COM code and Visual Basic will handle them correctly. However, I don't rely on Visual Basic for everything and sometimes have to depend on another developer's piece of C++ code to get some work done. If I'm using a COM object built in C++, how can I trust that the COM object is handling BSTRs correctly (or anything else allocated using the task allocator)?
via the Internet
A This question aptly points out the differences between the C++ and Visual Basic environments. As a COM developer using Visual Basic, your life is considerably easier than a COM developer using C++. Visual Basic takes care of so much for you. As you said, it releases interface pointers when they fall out of scope. Also, Visual Basic handles BSTRs correctly.
The rules for using BSTRs go like this: when a caller allocates a BSTR and passes it into a COM object, the caller owns that memory. However, when a COM object passes a BSTR back to the caller as an out parameter, it becomes the caller's responsibility to free the BSTR. Callers that forget to release BSTRs properly generate memory leaks.
In general, for [in] parameters, memory is allocated and freed by the caller. For [in/out] parameters memory is allocated and freed by the caller, but the callee may reallocate the memory buffer prior to returning. Therefore, callers cannot rely on the location being the same. For [out] parameters, memory is allocated by the callee and freed by the caller. You're on your own when managing BSTRs using C++. If you are worried about memory leaks, there is a way to spy on the task allocator.
The Task Allocator
You're probably used to managing your own memory with various runtime memory management routines such as the ones provided by the C Runtime Library (that is, malloc, free, and the C++ operators new and delete). Remember that, above all, COM exists as a software integration and distribution technology. Many software projects are built using various components produced by disparate languages and toolsincluding C++, Visual Basic, and the Java language.
Rather than depending upon all the tool vendors to implement memory management the same way, COM developers delegate their memory management calls to the COM task allocator. In addition, most COM routines and interface methods follow the convention that memory chunks of unknown size that are passed as out parameters should be allocated by the object and freed by the caller. This reduces the number of round-trips involved in making method calls (and I'm sure you know that unnecessary interapartment round-trips are evil). BSTRs (the kind of strings used by Visual Basic and the Java language) are allocated by the task allocator and must adhere to the aforementioned rule.
So if you're developing a large project that uses a whole bunch of different COM objects produced by various tools (for which you do not have the source code), you need to depend on the other clients and objects to handle memory allocations properly.
IMalloc and Company
The task allocator is accessed through an interface named IMalloc. In addition to the IMalloc interface, COM defines an interface named IMallocSpy that can be plugged into the system to watch allocations as they happen. Figure 1 shows the IMalloc and IMallocSpy interfaces.
Note that for each function in IMalloc, IMallocSpy has a pair of pre and post functions. To spy on memory allocations as they happen, simply implement this interface and plug it into the system using the API function CoRegisterMallocSpy, whose signature looks like this:
HRESULT CoRegisterMallocSpy(IMallocSpy* pMallocSpy);
|Once you've plugged your task allocator spy into the system, the task allocator will call your implementation of each pre and post function. For example, the task allocator will call your version of PreAlloc before calling IMalloc::Alloc. Then the task allocator will call your version of PostAlloc right after executing IMalloc::Alloc. As you can see, this is a pretty handy way to track memory allocations as they happen. You can even attach header and tail information to each block to keep even closer tabs on each memory chunk. Note, though, that IMallocSpy will not always reflect the true state of memory allocations. For instance, the following scenario will show up as a false leak:
This difference is a result of COM lazily freeing the meta data. For this reason you should use IMallocSpy to track trends in memory growth. BSTR's are also lazily freed (unless you disable caching as described below). Memory allocated with either CoTaskMemAlloc or SafeArrayCreate, marshaled as a parameter (for non-interface types), is freed when expected.
- Obtain the total number of memory blocks used to date.
- Create 10 object instances and immediately release them.
- Again obtain the total number of memory blocks used to date.
Unfortunately, if you're programing with Visual Basic, it's very hard to implement this interface. Notice that IMallocSpy uses a data type (void*) that Visual Basic doesn't understand. Fortunately, you're working in COM where C++, Visual Basic, and Java-language developers can meet in the middle. You can create an implementation of IMallocSpy using C++ and have the object notify the Visual Basic-based app about the state of a memory allocation through a Visual Basic-compliant interface.
In this example, I used the ActiveX® Template Library (ATL) to implement a version of IMallocSpy. I created a simple DLL server and added a simple COM object named MallocSpyObj. Once I pumped out the object using the Visual Studio® wizards, I defined a Visual Basic-compliant interface that mirrored the real IMallocSpy interface. Figure 2 shows the IMallocSpyObjEvents interface.
The basic differences between IMallocSpyObjEvents and IMallocSpy are that IMallocSpyObjEvents returns HRESULTs and the void pointer types are defined as longs. Visual Basic will understand how to implement IMallocSpyObjEvents. You can have MallocSpyObj call back to the Visual Basic-based application using this interface whenever a task allocator event occurs.
Next, I added the IMallocSpy interface to the ATL-based COM class, as shown in Figure 3. Notice that IMallocSpy occurs in the inheritance list as well as in the interface map (the COM MAP). Also, notice the pointer to IMallocSpyObjEvents (the interface used to call back to the Visual Basic-based application). This pointer will be set by the Visual Basic-based application explicitly. Figure 4 shows an implementation of IMallocSpy that uses IMallocSpyObjEvents to call back to the Visual Basic-based application on every allocation and release. I left out the calls in the other functions to save some space.
Finally, the Visual Basic-based application needs a way to start and stop the spying process. The ATL task allocator spy implements two functions named StartSpying and StopSpying to give that control to the Visual Basic-based application (see Figure 5). In addition, the task allocator spy implements a function named SetMallocSpyEventSink to set up the callback to the Visual Basic-based program.
Notice how StartSpying peels off a copy of the object's IMallocSpy interface and hands it over to COM by calling CoRegisterMallocSpy. StopSpying revokes the task allocator spy by calling CoRevokeMallocSpy. Finally, SetMallocSpyEventSink initializes m_pMallocSpyObjEvents so that the task allocator spy has a way to call back to the host application.
Spying Memory Using a Visual Basic Add-in
There are several ways you can use this task allocator spy. You can simply create an instance of it in your Visual Basic-based program and implement IMallocSpyObjEvents. A more interesting way (and the path I took here) is to
write a Visual Basic add-in to track memory.
Visual Basic add-ins are simple COM classes that are useful for extending the Visual Basic IDE. To create an add-in, just start up Visual Basic and have the New Project Wizard create an add-in for you. Then create an instance of a class that implements IDTExtensibility2. (Make sure you use the "Implements IDTExtensibility2" statement in your class module code.) Visual Basic expects to see this interface as part of your object.
Next, make sure your new object's typelib is checked in the Visual Basic Projects | Components dialog box. In your main add-in form, declare an instance of MallocSpyObj. The example I'm giving lets users turn the spy allocator on and offthe pushbuttons turn the spying on and off by calling StartSpying and StopSpying. Finally, the add-in responds to the object's callbacks by implementing IMallocSpyObjEvents. This add-in simply responds to the callbacks by putting a message in a listbox. Figure 6 shows the add-in's code; Figure 7 shows the add-in dialog.
Figure 7 Add-in Dialog
A Visual Basic add-in is simply a COM class, and like all other COM classes it requires some registry settings. Your add-in will register itself automatically when you build it. Fortunately, Visual Basic 6.0 also includes a new designer for managing configuration information, such as deciding when your add-in is turned on.
In the project browser, notice there's a Designers node. When you double-click on the Designers node, you can choose the Connection designer. Then you get a dialog box for setting up these options. You can use the designer to set up the target application (Visual Basic versus Visual Studio) and when the add-in loads (manually, upon startup of Visual Basic).
Turning on the Add-in
Once the add-in has registered itself, it's ready to be used. To turn on the add-in, select the Add-In Manager from the Visual Basic Add-in menu. Figure 8 shows the Add-In Manager. Check the Loaded/Unloaded checkbox and the add-in will load up. Then whenever you want to spy on task allocations, push the Start Spying button and task allocator spy will call back to the add-in, which will note the event in a listbox. So use it if you suspect a client of abusing the task allocator (that is, not releasing a pointer).
Figure 8 Add-in Manager
This is just one of many uses for the Visual Basic extensibility model. You can add a menu item that engages or disengages the task allocator spy. But there is one caveat: as you use the task allocator spy, be aware that the release versions of the COM libraries cache task allocator memory for BSTRs and you may get inaccurate results from time to time. You can get around this by using the debug version of OLEAUT32, setting the system environment variable OANOCACHE=1.
Have a question about programming in
Visual Basic, Visual FoxPro, Microsoft
Access, Office, or stuff like that? Send
your questions via email to George Shepherd at email@example.com.
© 1999 Microsoft Corporation. All rights reserved. Legal Notices.