0
votes

Consider the following example of the simplest COM object we can define in C# (built using Visual Studio 2010 SP1 with .NET framework 4.0):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

namespace CcwTestLib
{
   [ComVisible(true)]   
   [Guid("8ABD40E2-05E2-4436-9EAD-073911357155")]  
   public class CcwTestObject
   {

   }
}

We compile this assembly and register it for COM interop using regasm (or the built in option in Visual Studio).

Now we simply write an unmanaged Win32 console application in C++ that does nothing other than to create an instance of this object and release it 100,000 times. For example using the following program:

#include "stdafx.h"

// {8ABD40E2-05E2-4436-9EAD-073911357155}
static const GUID CLSID_CcwTestObject = 
   { 0x8abd40e2, 0x5e2, 0x4436, { 0x9e, 0xad, 0x7, 0x39, 0x11, 0x35, 0x71, 0x55 } };

int _tmain(int argc, _TCHAR* argv[])
{
   CoInitializeEx(NULL,  COINIT_MULTITHREADED);

   IUnknown *pTestObject = NULL;

   const int iCount = 100000;

   wprintf(L"Allocating COM instance %i times...\n", iCount);

   for (int i = 0; i < iCount; i++)
   {
      HRESULT hr = CoCreateInstance(CLSID_CcwTestObject, 
                                    NULL, 
                                    CLSCTX_INPROC_SERVER, 
                                    IID_IUnknown, 
                                    (LPVOID*)&pTestObject);
      if (FAILED(hr))
      {
         wprintf(L"Error: %i", hr);
         return -1;
      }

      pTestObject->Release();
   }

   CoUninitialize();

   return 0;
}

When running this application on our local system it completed in about 820ms and consumes about 32MB memory. Increasing iCount to 10,000,000 makes the program take a lot longer to complete (of course) but looking at the memory consumption it increases to about 92MB and stays there for the remainder of the execution of the program. Nothing too strange so far.

Now for the interesting part, leading up to my question. Let's remove the Guid attribute from the .NET class (and disable automatic COM registration if enabled so that the previous registration is still left intact in the registry) and rebuild the assembly.

We run the test program again with iCount set to 100,000. This time the program completes in about 90,000ms! That is about 100 times slower than before!

Even more interesting and troublesome is when we increase iCount to 10,000,000 and start the program. If we monitor its memory consumption using Process Explorer or VMMap or a similar program we can see it slowly increase, but it doesn’t stop at 92MB as we might expect. Instead it seems to continue on forever. Presumably the application will crash when running out of virtual memory space at around 2GB (since it is an x86 process), but since it’s moving so slow we didn’t wait for that to happen in this test but quit around 1,200MB.

It should be noted that using the COM object, calling its methods and so on (if we had defined any) works just fine, as it should since all the necessary information for creating the object is stored in the registry. A part of it on our system looks like the following:

[HKEY_CLASSES_ROOT\CLSID\{8ABD40E2-05E2-4436-9EAD-073911357155}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="CcwTestLib.CcwTestObject"
"Assembly"="CcwTestLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"=file:///D:/Coding/Projects/CcwTest/CcwTestLib/bin/Debug/CcwTestLib.dll

Where the CLSID correctly points to the assembly and its codebase, and the explicit type within the assembly.

We also discovered that altering the Guid in the attribute to anything other than the one it is registered with creates the very same problem.

So why is this happening? Is this a bug in .NET? And is there any workaround to this problem?

I would be very happy for some insight into this problem, which took us about a week to narrow down to this much simplified scenario from a discovered memory leak in our product.

1
Any sane code stops at 1 when the COM call returns an error.Hans Passant
But no call here does return an error?DeCaf
Well, that's bad. Creating an object of an unregistered class should always produce an error. You'd better find out why.Hans Passant
But it's not an unregistered class, is it? The information in the registry clearly associates a CLSID with the exact type in the assembly, complete with codebase and everything. Why would the actual System.GuidAttribute be necessary when instantiating the object? And if for some reason it is necessary, why does CoCreateInstance produce a memory leak instead of an error code?DeCaf
The [Guid] generates the CLSID, chicken and egg. It is core to the way COM finds DLLs to create objects. If you change the [Guid] but don't get an error then you are creating some random old type. Whatever shrapnel was left in the registry by forgetting to unregister old assemblies.Hans Passant

1 Answers

0
votes

It seems the memory leak was a bug that has been fixed in .NET 4.5.

For more details see the issue at Microsoft Connect.

To quote from their answer:

The memory leak is already fixed (.NET Framework 4.5 Developer Preview was the first publicly available build with the fix). As for the slowness, the Guid mismatch effectively deactivates our internal cache so we perform a bunch of registry lookups and by-name type lookups on each activation. Since this is not a mainline scenario, and should be limited to cases where the class has been re-Guid'ed without updating the registration, we are deprioritizing this issue for now. It's still tracked for future releases.