//
you're reading...
CLR

RCW Internal Reference Count

1. Introduction.

1.1 The Runtime Callable Wrapper (RCW) is used to represent a COM object in the managed world. Internally a RCW contains a reference count on the COM object that it wraps.

1.2 Note that this reference count is not equivalent to the well-known COM reference count. The COM reference count is held within the COM object itself.

1.3 An RCW internally maintains a separate reference count. This blog aims to provide as much detail as possible on this internal reference count. As time goes on, this blog will be updated with more information that hopefully serves useful purposes for developers and researchers.

2. The Various COM Interface Pointers Cached Within a RCW.

2.1 As we know, an RCW is a .NET managed object created to represent a COM object (which is an unmanaged object). It is thus subject to garbage collection.

2.2 Each COM coclass instance is wrapped by one single RCW. Note that a RCW internally contains various interface pointers that are associated with that specific COM coclass instance.

2.3 This means that a RCW may internally hold IUnknown or IDispatch or any other interfaces derived from that specific COM coclass instance via QueryInterface().

2.4 As an example, let’s say we have a COM coclass (implemented in a DLL) “TestCOMObject01″ which implements an ITestCOMObject01 interface as its default interface. Once the type library of this COM server has been imported into a C# project, the “TestCOMObject01″ coclass will be represented by the RCW class “TestCOMObject01Class”.

Let’s further suppose that the COM coclass also implements another COM interface “ISomeInterface01″. Observe the following code below :

static void DemonstrateSingleRCWMultipleCOMInterfaces()
{
  TestCOMObject01Class com_object_01 = new TestCOMObject01Class();
  ISomeInterface01 pISomeInterface01 = (ISomeInterface01)com_object_01;

  if (com_object_01 == pISomeInterface01)
  {
    Console.WriteLine("com_object_01 == pISomeInterface01");
  }
}

In the above code, when “com_object_01″ is first created by the “new” operator, an initial IUnknown interface pointer will be obtained from the coclass instance and it will be stored inside the “com_object_01″ RCW. The RCW will not immediately perform a COM QueryInteface() call for the ITestCOMObject01 interface until a method or a property of the ITestCOMObject01 interface is first accessed.

Similarly when “com_object_01″ is cast into a “ISomeInterface01″ type :

ISomeInterface01 pISomeInterface01 = (ISomeInterface01)com_object_01;

the RCW will not immediately perform a COM QueryInteface() call for the COM “ISomeInterface01″ interface. It will do so when a method or property of the “ISomeInterface01″ is first accessed.

The fact that “com_object_01″ is equivalent to “pISomeInterface01″ may be deduced from metadata obtained through the imported interop assembly of the COM server :

if (com_object_01 == pISomeInterface01)
{
  Console.WriteLine("com_object_01 == pISomeInterface01");
}

The fact that com_object_01 is equivalent to pISomeInterface01 is significant. That is, the RCW that wraps “pISomeInterface01″ is the same RCW as the “com_object_01″.

2.5 Let’s say that the default COM interface behind “TestCOMObject01Class” implements IDispatch and it exposes a method named “TestMethod01()”, then the IDispatch interface is first queried for when the Type.InvokeMember() method is used to invoke the method. I have extended DemonstrateSingleRCWMultipleCOMInterfaces() to demonstrate this :

static void DemonstrateSingleRCWMultipleCOMInterfaces()
{
  TestCOMObject01Class com_object_01 = new TestCOMObject01Class();
  ISomeInterface01 pISomeInterface01 = (ISomeInterface01)com_object_01;

  if (com_object_01 == pISomeInterface01)
  {
    Console.WriteLine("com_object_01 == pISomeInterface01");
  }

  com_object_01.TestMethod01();

  Type type = com_object_01.GetType();

  type.InvokeMember
  (
    "TestMethod01",
    BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod,
    null,
    com_object_01,
    null
  );
}

In the above code, when type.InvokeMember() is called, the IDispatch interface is queried for from the COM object behind “com_object_01″ and then used to call the “TestMethod01()” method.

Hence, eventually, the RCW internally holds a COM interface pointer to “ITestCOMObject01″ and “IDispatch” but not “ISomeInterface01″ (because no access to the methods or properties of “ISomeInterface01″ was ever made).

3. What Happens if the COM Object is a Proxy ?

3.1 An interesting situation occurs if a RCW holds a pointer to a proxy instead of the actual COM object. This can happen if the code that created the COM object is declared to run in an apartment that is not compatible with the apartment of the COM object itself.

3.2 For example, if the COM coclass represented by “TestCOMObject01Class” is implemented as a STA COM class, the following code will demonstrate how an instance will be created in an incompatible apartment :

[MTAThread()]
static void Main(string[] args)
{
  DemonstrateSingleRCWMultipleCOMInterfaces();
}

The main thread of the application (a console app) is run in a MTA thread which means that when “com_object_01″ is created (inside DemonstrateSingleRCWMultipleCOMInterfaces()), a proxy to the IUnknown interface of the actual COM object (an STA object) is returned to the RCW.

3.3 This has no effect on the RCW itself and the observations described in section 2 concerning the various interface pointers cached remains the same.

3.4 This, however, affects the reference count of the COM object itself as well as the return value of methods like Marshal.AddRef() and Marshal.Release() as we shall see below.

3.5 The thing is, when a RCW contains a proxy, it returns that proxy when methods like Marshal.GetIUnknownForObject() and Marshal.GetIDispatchForObject() are called.

3.6 Now, a COM proxy to a COM object holds a strong reference on the object of course. However, the proxy itself holds its own reference count and increments this count when Marshal.AddRef() is called on it. It decrements this count when Marshal.Release() is used on it. The reference count of the actual COM object itself will not be returned.

4. The RCW Internal Reference Count.

4.1 We have been talking alot about COM reference counts so far. We will now talk about the RCW’s internal reference count. When a RCW is first created, this ref count is initially set to 1. It will usually remain at value 1 unless the same COM object enters managed code a second (or more) time.

4.2 This RCW ref count affects the wrapped COM object in that once it reaches zero, the RCW will release its references to all cached interface pointers.

4.3 The Marshal.ReleaseComObject() method can be used to perform decrementation of a RCW’s internal reference count.

4.4 However, at the time of this writing (circa August 2011), it is not clear to me under what circumstances will the RCW’s internal ref count be decremented naturally by the CLR without deliberate programmer call to Marshal.ReleaseComObject().

4.5 This RCW ref count, however, is not affected by calls to Marshal.GetIUnknownForObject(), Marshal.GetIDispatchForObject(), Marshal.AddRef() and Marshal.Release().

4.6 For example, when Marshal.GetIUnknownForObject() is called, the direct IUnknown pointer of the wrapped object is returned (as an IntPtr). This will cause the reference count of the COM object to be incremented. However, this will not in any way affect the RCW’s ref count.

4.7 The code below demonstrates the use of Marshal.ReleaseComObject() and its implications :

static void DemonstrateMultipleRCWButSingleRCWInternalRefCount()
{
  // com_object_01 is an RCW.
  TestCOMObject01Class com_object_01 = new TestCOMObject01Class();
  // com_object_02 is a reference to com_object_01.
  // But there is only one RCW and the RCW ref count remains at 1.
  TestCOMObject01Class com_object_02 = com_object_01;

  // Obtain the COM IUnknown interface pointer behind the
  // COM object referenced by "com_object_01".
  // This call will increment the ref count of the COM
  // object held by the RCW "com_object_01". But it will
  // not increase the RCW's own ref count.
  IntPtr pUnknown = Marshal.GetIUnknownForObject(com_object_01);

  // Call to COM method will be successful
  // for both RCWs.
  com_object_01.TestMethod01();
  com_object_02.TestMethod01();

  // When Marshal.ReleaseComObject() is called on an RCW,
  // the ref count of the RCW is decremented. The ref count
  // typically becomes zero as a result (this is because the
  // RCW's ref count on the COM object remains the same no
  // matter how many references to the RCW exists elsewhere
  // in the managed code).
  //
  // Once the RCW ref count reaches zero, the RCW will
  // release all internally cached interface pointers
  // (as in IUnknown::Release()).
  //
  // The RCW will then be in a state where it has separated
  // from its internal COM object.
  int iRefCount = Marshal.ReleaseComObject((object)com_object_01);

  // Calling a method of the COM object from com_object_01
  // or com_object_02 will fail.
  try
  {
    com_object_01.TestMethod01();
  }
  catch(Exception ex)
  {
    Console.WriteLine("Exception : {0:S}.", ex.Message);
  }

  Marshal.Release(pUnknown);
}

The following are the pertinent points of the above function :

  • An instance of the TestCOMObject01Class class is created and referenced by com_object_01. Hence com_object_01 represents an RCW for the COM coclass TestCOMObject01.
  • Another instance of TestCOMObject01Class (com_object_02) is declared and is assigned com_object_01.
  • However, there is only one RCW. Furthermore, the RCW ref count will remain at 1.
  • Then, using Marshal.GetIUnknownForObject(), the code obtains the COM IUnknown interface pointer wrapped by com_object_01.
  • This will increment the ref count of the COM object held by the com_object_01 RCW. But it will not increase the RCW’s ref count.
  • The code then calls Marshal.ReleaseComObject() on com_object_01.
  • When Marshal.ReleaseComObject() is called on an RCW, the ref count of the RCW is decremented. Note that the ref count is merely decremented and will not be forced decremented to 0 immediately.
  • However, the ref count typically results in zero as a result (check out the return value in “iRefCount”).
  • This is because the RCW’s ref count on the COM object remains the same no matter how many references to the RCW exists elsewhere in the managed code.
  • Now once the RCW ref count reaches zero, the RCW will release all internally cached interface pointers (release as in IUnknown::Release()).
  • The RCW will then be in a state where it has separated from its internal COM object. Further use of all references to the RCW (i.e. via com_object_01 or com_object_02) will result in an exception with the following description :

“COM object that has been separated from its underlying RCW cannot be used.”

  • This will be so despite the fact that there is still another reference to the COM object (recall that we had called Marshal.GetIUnknownForObject() on com_object_01 earlier).
  • This is because having additional references on a COM object will not increase the reference count of an RCW that references the same COM object.

4.8 Hence I repeat and emphasize that Marshal.ReleaseComObject() will only decrement the internal reference count of a RCW and will not force the ref count to zero.

4.9 The next 2 sections will demonstrate how the internal reference count of an RCW can be incremented through the re-entry of a COM object into managed code.

5. Re-Entry of COM Object into Managed Code Demonstration 1.

5.1 In this section I will demonstrate one scenario in which a COM object re-enters managed code, causing it to be wrapped in an existing RCW and causing the RCW to increment its internal reference count.

5.2 In order to facilitate the re-entry of a COM object into managed code, I have prepared the following 2 APIs to be DLLImported into C# code :

ITestCOMObject01* g_ITestCOMObject01 = NULL;

void __stdcall SetMyObject(/*[in]*/ ITestCOMObject01* pITestCOMObject01)
{
  if (g_ITestCOMObject01)
  {
    g_ITestCOMObject01 -> Release();
    g_ITestCOMObject01 = NULL;
  }

  if (pITestCOMObject01)
  {
    pITestCOMObject01 -> QueryInterface(IID_ITestCOMObject01, (void**)&g_ITestCOMObject01);
  }
}

void __stdcall GetMyObject(/*[out]*/ ITestCOMObject01** ppITestCOMObject01Receiver)
{
  // Initialize receiver.
  if (ppITestCOMObject01Receiver)
  {
    *ppITestCOMObject01Receiver = NULL;

    if (g_ITestCOMObject01)
    {
      g_ITestCOMObject01 -> QueryInterface(IID_ITestCOMObject01, (void**)ppITestCOMObject01Receiver);
    }
  }
}

The first API is SetMyObject() which is meant to store an input pointer to an ITestCOMObject01 interface in a global variable g_ITestCOMObject01. g_ITestCOMObject01 is then retrievable via the second API named GetMyObject().

The basic idea is that an ITestCOMObject01 COM object (represented by an RCW) be easily stored in unmanaged code via SetMyObject() and then retrieved into managed code via GetMyObject().

Now, when the COM object re-enters managed code, the CLR will be able to determine that an existing RCW already wraps it. This determination process likely uses the COM Object Identifier (OID) field of a Standard Marshaled Object Reference.

The Standard Marshaled Object Reference (SMOR) is a data structure which is used as part of COM’s Standard Marshaling Architecture. That is, when a COM object is exported (marshaled) from one COM apartment into another. In the world of COM and DCOM, the OID uniquely identifies a COM identity in the network. It is used by APIs like CoUnmarshalInterface() to maintain the COM identity laws for proxies.

The SMOR and its associated concepts are likely re-used in COM interop marshaling. To learn more about the SMOR, refer to the book “Essential COM” by Don Box pages 212 and 219.

5.3 The SetMyObject() and GetMyObject() APIs are declared in the C# client as follows :

[DllImport("TestCOMServer01.dll", CallingConvention = CallingConvention.StdCall)]
private extern static void SetMyObject([In] TestCOMObject01Class pITestCOMObject01);

[DllImport("TestCOMServer01.dll", CallingConvention = CallingConvention.StdCall)]
private extern static void GetMyObject([Out] out TestCOMObject01Class ppITestCOMObject01Receiver);

5.4 The following is a sample code demonstrating the use of SetMyObject() and GetMyObject() for object re-entry :

static void DemonstrateCOMObjectReEntryIntoManagedCode()
{
  // com_object_01 is an RCW.
  TestCOMObject01Class com_object_01 = new TestCOMObject01Class();
  // Set com_object_01 as the COM object to
  // re-enter managed code via GetMyObject().
  SetMyObject(com_object_01);

  // Prepare a new COM object reference.
  TestCOMObject01Class com_object_02 = null;
  // Return com_object_01 back to managed code.
  // However, the same RCW will be used to
  // refer to the underlying COM object.
  // The RCW's internal ref count will be
  // incremented to 2.
  GetMyObject(out com_object_02);

  // Test equality between com_object_01 and com_object_02.
  if (com_object_01 == com_object_02)
  {
    Console.WriteLine("com_object_01 == com_object_02");
  }

  // When Marshal.ReleaseComObject() is called on an RCW,
  // the ref count of the RCW is decremented. This time,
  // the ref count will not become zero because it was
  // decremented from a value of 2. The ref count will
  // become 1 instead.
  int iRefCount = Marshal.ReleaseComObject((object)com_object_01);

  // Calling a method of the COM object from com_object_01
  // or com_object_02 will succeed.
  try
  {
    com_object_01.TestMethod01();
  }
  catch (Exception ex)
  {
    Console.WriteLine("Exception : {0:S}.", ex.Message);
  }

  SetMyObject(null);
}

Here are some of the pertinent points about the code above :

  • A TestCOMObject01Class object is instantiated. Referened by com_object_01.
  • At this point, the com_object_01 RCW will only contain an IUnknown interface pointer of the underlying COM object.
  • The SetMyObject() API is then called.
  • A pointer to the ITestCOMObject01 interface of the COM object referenced by com_object_01 is then queried before being passed to SetMyObject().
  • Hence when SetMyObject() is called, com_object_01 would internally contain an ITestCOMObject01 pointer. This is different from the previous examples where a pointer to an interface is only queried for when a method or property of that interface is accessed.
  • Next, GetMyObject() is called and another instance of TestCOMObject01Class (i.e. com_object_02) is used to wrap the returned object.
  • The interop marshaler is able to detect that the returned ITestCOMObject01 interface pointer originates from an existing COM object which is already wrapped by com_object_01.
  • The RCW for com_object_01 is then re-used. Essentially com_object_02 will be equivalent to com_object_01. That is, they both refer to the same RCW.
  • The internal reference count of the RCW is incremented (to 2).
  • Then when Marshal.ReleaseComObject() is called next, the internal reference count of the RCW is decremented but remains non-zero (i.e. 1).
  • A subsequent call to a method of the COM object (i.e. TestMethod01()) will succeed.

6. Re-Entry of COM Object into Managed Code Demonstration 2.

6.1 In this section, I will demonstrate yet another scenario in which a COM object re-enters managed code, causing it to be wrapped in an existing RCW and causing the RCW to increment its internal reference count.

6.2 This time, the COM object is implemented as a singleton. The code for the COM coclass (implemented in ATL) is listed below :

class ATL_NO_VTABLE CTestCOMObject02 :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CTestCOMObject02, &CLSID_TestCOMObject02>,
	public IDispatchImpl<ITestCOMObject02, &IID_ITestCOMObject02, &LIBID_TestCOMServer01Lib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
	CTestCOMObject02()
	{
	}

	~CTestCOMObject02()
	{
	}	

DECLARE_REGISTRY_RESOURCEID(IDR_TESTCOMOBJECT02)

BEGIN_COM_MAP(CTestCOMObject02)
	COM_INTERFACE_ENTRY(ITestCOMObject02)
	COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

	DECLARE_CLASSFACTORY_SINGLETON(CTestCOMObject02)

	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:

	STDMETHOD(TestMethod01)(void);
};

The most important part of the code above is highlighted in bold :

DECLARE_CLASSFACTORY_SINGLETON(CTestCOMObject02)

6.3 The above macro indicates that the class CTestCOMObject02 uses the class factory for producing singletons. A singleton class is such that all instances created from a class factory of that class always produces the same instance.

6.4 The following is a C# function that demonstrates the use of the CTestCOMObject02 class. When imported into C#, CTestCOMObject02 is declared as TestCOMObject02Class :

static void DemonstrateMultipleRCWToACOMSingleton()
{
  TestCOMObject02Class com_object_01 = new TestCOMObject02Class();
  TestCOMObject02Class com_object_02 = new TestCOMObject02Class();
  TestCOMObject02Class com_object_03 = new TestCOMObject02Class();

  // Call to COM method will be successful
  // for all RCWs.
  com_object_01.TestMethod01();
  com_object_02.TestMethod01();
  com_object_03.TestMethod01();

  if (com_object_01 == com_object_02)
  {
    Console.WriteLine("com_object_01 == com_object_02");
  }

  if (com_object_01 == com_object_03)
  {
    Console.WriteLine("com_object_01 == com_object_03");
  }

  if (com_object_02 == com_object_03)
  {
    Console.WriteLine("com_object_03 == com_object_03");
  }

  int iRecCount = Marshal.ReleaseComObject((object)com_object_01);
  iRecCount = Marshal.ReleaseComObject((object)com_object_02);
  iRecCount = Marshal.ReleaseComObject((object)com_object_03);

  // Calling a method of the COM object from
  // com_object_01, com_object_02 or com_object_03
  // will fail.
  try
  {
    com_object_01.TestMethod01();
  }
  catch (Exception ex)
  {
    Console.WriteLine("Exception : {0:S}.", ex.Message);
  }
}

Here, what appears to be 3 instances of TestCOMObject02Class are instantiated. However, in actual fact, only one instance of the ITestCOMObject02 COM object is ever created. Hence all 3 instances of TestCOMObject02Class are equal. Only one RCW is ever created and com_object_01, com_object_02 and com_object_03 all refer to the same RCW.

It also takes 3 calls to Marshal.ReleaseComObject() to fully decrement the internal reference count of the RCW to zero.

7. In Conclusion.

7.1 I hope the reader has benefitted from this rather long exposition into the RCW’s internal reference count.

7.2 I shall certainly do further research and seek to find situations in which the internal ref count of an RCW is decremented naturally by the CLR without the use of Marshal.ReleaseComObject().

About these ads

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

2 thoughts on “RCW Internal Reference Count

  1. Love the post!
    I think the only ‘natural’ situation where the CLR decrements the ref count of RCW instance is in the finalizer, of course it decrements it to zero.

    Posted by Arthur | September 22, 2012, 3:54 pm
  2. This article is so, so good. Thanks

    Posted by Simon | May 21, 2013, 10:20 am

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

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: