//
you're reading...
.NET/COM/ActiveX Interop, CLR

Invoking Finalizers of .NET-based COM Objects in an Unmanaged App Part 1

1. Introduction.

1.1 .NET objects, when exposed as COM objects in an unmanaged application, face a tricky problem : their finalizers are often not invoked throughout the application’s lifetime.

1.2 For more information on finalizers in general and the limitation that we have just mentioned above, please refer to : Object.Finalize Method on MSDN.

1.3 This series of blogs will examine the various ways that this issue can be worked around. We will build up a sample COM-visible C# class which is instantiated inside unmanaged code. We will then look at 3 types of unmanaged applications, namely : Visual C++ 6.0, Visual Basic 6.0 and VBScript and discuss what are the right techniques that each of these application types should use.

1.4 In this part 1, we will concentrate on application types created using Visual C++ 6.0.

2. Some Background to the Problem.

2.1 The component that bridges an unmanaged application with .NET objects which are exposed as COM objects is mscoree.dll. This DLL is part of the .NET CLR and it is itself an unmanaged DLL.

2.2 The interesting thing is that it exports the 4 APIs that all COM in-proc servers must export : DllCanUnloadNow(), DllGetClassObject(), DllRegisterServer() and DllUnregisterServer() which suggests that it is a COM in-proc server itself. In fact, it really is one. Besides exposing several COM classes, it serves as a bridge, as mentioned above and as demonstrated below.

2.3 Whenever a .NET object is created and exposed as a COM object (via a COM-Callable Wrapper, CCW), it is mscoree.dll that gets loaded. It is mscoree.dll that loads the relevant .NET assembly that houses the .NET class that is to be created. It is mscoree.dll that exposes the class factory that is used by the unmanaged application to instantiate the required .NET class. It is thus lilely that it is mscoree.dll that creates the CCW that serves as the proxy for the .NET object in the unmanaged world.

2.4 Each CCW internally holds just one reference to the underlying .NET object no matter how many references the application holds on the CCW itself. Now, when an unmanaged application has done using a CCW, it must call the CCW’s Release() method as usual. When the reference count of the CCW reaches zero, it will release its hold on the .NET object and the .NET object then becomes eligible for collection by the Garbage Collector (GC).

2.5 As we know, the exact time that the GC performs the actual collection is undefined. Hence the exact time that the finalizer of a .NET object gets executed is non-deterministic.

2.6 Now, for a managed application, all created objects will get garbage collected eventually before the end of the application. Hence the finalizers will get executed at some point during the lifetime of the application. This is so except in some rare unexpected circumstances, e.g. where a finalizer hangs (in this situation, the CLR will abandon the hung finalizer and move on with other finalizers if there be any).

2.7 Now, for unmanaged applications that use .NET-based COM objects, garbage collection almost always never get executed unless the unmanaged application is .NET-aware. Applications built from pre-.NET era compilers (e.g. Visual Basic 6 or Visual C++ 6.0) or run from .NET-unaware interpreters (e.g. VBScript) suffer this problem. Newer .NET-aware C++ Compilers (e.g. Visual Studio 2008) supply runtimes that will cause graceful shutdown of .NET objects.

2.8 For such applications, the only time that the CLR is notified of process termination is via the calling of the mscoree.dll’s DllMain() function with the 2nd (“reason”) parameter set to DLL_PROCESS_DETACH. For some reason, this notification comes too late and the execution of finalizers cannot be safely run.

2.9 To overcome this difficulty, unmanaged applications must call the CorExitProcess() API (which is exported from mscoree.dll) at the end of the application. This API will cause the CLR to perform garbage collection and launch the finalizers of all collected objects.

2.10 In the sections that follow we will discuss how this can best be accomplished for the 3 types of unmanaged applications : Visual C++ 6.0-based, Visual Basic 6.0-based and VBScript-based. However, before that, we will first build up a test C# class that is COM-visible and is instantiated inside unmanaged code. We will additionally discuss a simple but effective alternative technique : by deliberate invokation of the garbage collector for these applications on demand.

3. Sample COM-Visible C# Class.

3.1 Listed below is a full listing of the source codes for the C# class that I prepared for use throughout this blog :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace TestCSCOMServer01
{
    public class SubObject
    {
        public SubObject()
        {
            MessageBox.Show("SubObject() constructor.");
        }

        ~SubObject()
        {
            MessageBox.Show("~SubObject() destructor.");
        }
    }

    [ComVisible(true)]
    [Guid("E38B2273-1DD7-448b-9F68-1B856EA79986")]
    public interface ITestInterface01
    {
        void Method01(string str);
    }

    [ComVisible(true)]
    [Guid("D4F2936C-890C-4f02-83DD-97A63B99BEBD")]
    [ProgId("TestCSCOMServer01.TestImpl01")]
    [ClassInterface(ClassInterfaceType.AutoDual)]  // set to AutoDual or AutoDispatch
    public class TestImpl01 : ITestInterface01
    {
        public TestImpl01()
        {
            m_SubObject = new SubObject();
            MessageBox.Show("TestImpl01() constructor.");
        }

        ~TestImpl01()
        {
            MessageBox.Show("~TestImpl01() destructor.");
        }

        public void Method01(string str)
        {
            MessageBox.Show(str);
        }

        private SubObject m_SubObject;
    }
}

The class name is TestImpl01. It implements an interface named ITestInterface01 which contains only one method Method01().

For simplicity, TestImpl01.Method01() simply displays the input string parameter in a message box. TestImpl01 also displays message boxes in its constructor and destructor (C#’s equivalent of a finalizer) for indication purposes.

As an added test requirement, we make TestImpl01 contain a member object “m_SubObject” of type SubObject. It is a very simple class the purpose of which is to show that when we are able to invoke the finalizer of TestImpl01, the finalizer if its member objects will also be invoked.

The above source codes, when compiled, will produce TestCSCOMServer01.dll. I have selected the “Register For COM Interop” property setting so that the DLL is COM-registered by the IDE upon successful compilation.

4. Visual C++ 6.0-Based Client Application.

Listed below is a listing for a Visual C++ 6.0 application that instantiates TestImpl01 as a COM object :

#include "stdafx.h"
#import "C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.tlb"
#import "..\TestCSCOMServer01\bin\Debug\TestCSCOMServer01.tlb"
using namespace TestCSCOMServer01;

int main(int argc, char* argv[])
{
  ::CoInitialize(NULL);

  if (1)
  {
    BSTR bstr = ::SysAllocString(L"Hello World");

    ITestInterface01Ptr spITestInterface01 = NULL;

    spITestInterface01.CreateInstance(__uuidof(TestImpl01));

    spITestInterface01 -> Method01(bstr);

    ::SysFreeString(bstr);
    bstr = NULL;
  }

  ::CoUninitialize();

  return 0;
}

We will now analyze this client code.

4.1 When this program is compiled and run, at the point where an instance of TestImpl01 is created :

spITestInterface01.CreateInstance(__uuidof(TestImpl01));

The following message boxes will be displayed one after the other :

These indicate that SubObject() constructor is invoked as a result of TestImpl01 being constructed.

4.2 Then when the following code is run :

spITestInterface01 -> Method01(bstr);

The following message box will be displayed :

This message box is of course displayed as a result of the implementation code for TestImpl01.Method01().

4.3 Then, when the rest of the code is run to completion, no further message boxes were displayed despite the fact that the destructors for TestImpl01 and SubObject clearly will display indication message boxes. This clearly shows that the finalizers for TestImpl01 and SubObject are not invoked.

5. Ensuring Finalizer Invokation.

5.1 To correct the situation and ensure that the finalizers are invoked, we need to make a call to CorExitProcess() which is exported from mscoree.dll.

5.2 The simplest way to do this (thus avoiding compilation and linking problems for Visual C++ 6.0 trying to #include mscoree.h and link to mscoree.lib) would be to declare a pointer type to the CorExitProcess() API and then load the mscoree.dll manually using LoadLibrary() and then grabbing a function pointer to CorExitProcess() by GetProcAddress().

5.3 The full augmented version of the code in section 4 is listed below :

#include "stdafx.h"
#import "C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.tlb"
#import "..\TestCSCOMServer01\bin\Debug\TestCSCOMServer01.tlb"
using namespace TestCSCOMServer01;
// Declare a function pointer to the CorExitProcess() API.
typedef void (__stdcall *PCorExitProcess)(int exitcode);

int main(int argc, char* argv[])
{
  // First make sure that we are able to get
  // a pointer to the CorExitProcess() API
  // exported from mscoree.dll.
  PCorExitProcess pCorExitProcess = NULL;
  HMODULE hMod = LoadLibrary("mscoree.dll");

  if (hMod == NULL)
  {
    // If we are not able to load the DLL,
    // exit.
    return 0;
  }

  pCorExitProcess = (PCorExitProcess)::GetProcAddress(hMod, "CorExitProcess");
  if (pCorExitProcess == NULL)
  {
    // If we are not able to get a pointer to CorExitProcess()
    // from the DLL, exit.
    return 0;
  }

  ::CoInitialize(NULL);

  if (1)
  {
    BSTR bstr = ::SysAllocString(L"Hello World");

    ITestInterface01Ptr spITestInterface01 = NULL;

    spITestInterface01.CreateInstance(__uuidof(TestImpl01));

    spITestInterface01 -> Method01(bstr);

    ::SysFreeString(bstr);
    bstr = NULL;
  }

  ::CoUninitialize();
  // Invoke CorExitProcess(). The finalizers will be
  // invoked but program will stop right here.
  pCorExitProcess(0);
  // The remaining code will not be called.
  FreeLibrary(hMod);
  hMod = NULL;

  return 0;
}

Please refer to the self-explanatory comments included in the code above.

5.4 The crucial part of the code above is the call to CorExitProcess() (via the pCorExitProcess pointer) which will invoke the finalizers for SubObject and TestImpl01. Message boxes displaying “~SubObject() destructor.” and “~TestImpl01() destructor.” will be launched one after the other. This clearly showed that the finalizers are invoked finally.

5.5 Now, take note that right after CorExitProcess() is called, the program terminates immediately. The remaining code to free the handle to mscoree.dll is not called.

6. Interesting Side Note

6.1 Now, as a demonstration of what the CLR will do when a finalizer takes too long to complete (as per point 2.6 above), when the first message boxes showing the finalizer message for SubObject appears, do not click on its “OK” button.

6.2 What will happen is that the message box will be dismissed automatically. The CLR will think that the current finalizer has hung. It will abandon the call to the finalizer and move on.

7. In Conclusion.

7.1 Note that the above problem will not happen for a program written using Visual Studio 2008. A call to CorExitProcess() will be automatically executed by the runtime at the end of the program. Thus ensuring graceful shutdown of all COM-imported .NET objects upon program termination.

7.2 This enhancement has been in place since Visual C++ .NET 2002. Hence for all Visual C++ programs written since  Visual C++ .NET 2002, an explicit call to CorExitProcess() is not required.

7.3 In the next part of this series, we will explore how to enure that the finalizers for the TestImpl01 and the SubObject objects get invoked from a Visual Basic 6.0 program.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

One thought on “Invoking Finalizers of .NET-based COM Objects in an Unmanaged App Part 1

  1. My impression is that the managed object can also expose an IDisposable interface, function like Close() directly, just no available “using” statement like c#.

    Besides, the finalizer generally is kind of dangerous, meaning some object is not disposed correctly.

    Posted by sali98 | August 24, 2011, 7:58 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: