4
votes

I have a C# client that consumes interfaces from a native C++ COM server dll. The DLL implements 4 interfaces. These 4 interfaces are implemented by 4 coclasses in the DLL. But only 1 coclass is exposed to the client. Interfaces 2,3,4 are returned to the client by one of the methods in interfaces 1.

C++ COM server:

interface IFace1: IUnknown{
HRESULT CreateOtherInterface([in] REFIID iidFace, [out, iid_is(iidFace)] void** ppOut);
};

coclass ClassIFace1
{
    [default] interface IFace1;
};

C# Client:

ClassIFace1 Face1Obj = new ClassIFace1();

IFace1 Face1Ctrl = (IFace1)Face1Obj; 

IFace2 Face2Ctrl = null;
IntPtr Face2IntPtr = new IntPtr();

Face1Ctrl.CreateOtherInterface(Face2Guid, out Face2IntPtr);
Face2Ctrl = (IFace2)Mashal.PtrToStructure(Face2IntPtr);

//Consume Face2Ctrl

if(Face1Obj != null)
{
    Marshal.ReleaseComObject(Face1Obj);
}

As IFace2, IFace3 and IFace4 does not share the same coclass as IFace1, I suspect the Marshal.ReleaseComObject(Face1Obj) line will only destruct ClassIFace1 object but not ClassIFace2, ClassIFace3, ClassIFace4 objects and results in memory leaks. Is there any way to solve this? Or the Marshal.ReleaseComObject(Face1Obj) actually destroy other COM objects as well?

3
Perhaps I should be more specific here. The COM server will talk to devices. And different devices expose a different set of features. Each feature is implemented as a feature interface such as IFace2, IFace3 etc. Clients just need to give the iid of the feature that they are interested in, the COM server will return the feature interface if the device supports it, otherwise the call will fail.Angela Yan
Client has no knowledge whether IFace2, IFace3 etc are implemented with the same coclass as IFace1 or not (actually they are different coclasses), and neither do they care. They just get all the feature interfaces from IFace1, and release the feature interfaces individually after finish with them. This method works great for C++ clients for all these years, until we need to implement a C# client to talk to the COM server.Angela Yan

3 Answers

1
votes

It is the normal behaviour with COM objects, that you access them exclusively by their interfaces. The coclass is necessary only to create new instances of a COM object, you do not directly access the coclass. So i suspect your example should look like that:

IFace1 face1Ctrl = new ClassIFace1();

The method CreateOtherInterface() looks a bit strange to me, it has the same signature as a QueryInterface(), so i assume it should do the same thing (i'm not familiar with C++):

IFace2 face2Ctrl;
face1Ctrl.CreateOtherInterface(IFace2, out face2Ctrl);

I think this should do, just try it out. If it where the normal QueryInterface method, you should be able to get the interface like that:

IFace2 face2Ctrl = face1Ctrl as IFace2;

COM objects are reference counted, they are released as soon as you destroy the last reference to the interface. As soon as the garbage collector destroys your variables with references to the COM object, the COM object will release itself. That can be a problem in C#, because you have to wait on the garbage collector and cannot determine the order of releasing. If you need to release a COM object at a given moment, you can use Marshal.ReleaseComObject(), but normally you just wait on the garbage collector to decrement the reference counter.

As long as you do not know the implementation of the COM object, you do not know, if each interface has it's own coclass, or if one coclass implements several interfaces. You should not need this knowledge, when you use the COM object. The query interface could create a new coclass and return it's interface, or it could return itself and increase the reference counter.

1
votes

Like what Hans said, CreateOtherInterface looks very odd. Normally, you don't need to create it by yourself. All you need to do is to make sure client can access all four coclasses. Then, the Activator.CreateInstance or the native CoCreateInstance will do the right thing for you. Another choice is to expose one single coclass and have that single coclass supporting all four interfaces.

However, since you mentioned that only 1 coclass is exposed to client, I am imagining there are some strange reasons that the TLB file consumed by the client doesn't see the other 3 coclasses or the other 3 coclasses were not registered properly but discovered by the first coclass in some proprietary ways. I am also assuming that you cannot modify server side implementation.

Given all these assumption, here is my answer. The reference counts are maintained within the 4 coclasses independently. So, releasing the reference on the first coclass won't decrement the reference counts in the other three coclasses.

There are some more things you need to pay attention. You are using Marshal.ReleaseComObject(Face1Obj) to release the first coclass. You can do that because the first coclass was wrapped by a Runtime Callable Wrapper (RCW). Like what Martin said, even if you don't call the Marshal.ReleaseComObject(), .NET runtime will do it for you when garbabge collection occurs.

However, Face2Ctrl is obtained differently. It's not wrapped by a RCW. You are treating the returned pointer as a structure directly. This doesn't sound right to me because you may have issues on memory alignment and data marshalling. What you want to do might be calling Marshal.GetObjectForIUnknown which will return a RCW for you. Once you get your RCW, you can call Marshal.ReleaseComObject() to release your RCW in timely manner.

If the implementation of CreateOtherInterface is like QueryInterface, which always AddRef on the returned interface, you should call Marshal.Release on the returned interface once you are done with Face2Obj. Marshal.ReleaseComObject() is not enough because it just releasees the reference count added by the RCW but in this case, you need more one call on IUnknown.Release

0
votes

In addition, there is some mistakes on how to get the interface. Here is the complete solution for the C# client:

//======Create IFace1 and IFace2 interface===============
Type consoleType = Type.GetTypeFromCLSID(Face1CoCLSID);
Object Face1Obj = Activator.CreateInstance(consoleType);
IFace1 Face1Ctrl = (IFace1)Face1Obj;

Guid IFace2Guid = typeof(IFace2).GUID;
IntPtr Face2IntPtr = IntPtr.Zero;

//Face2 object's ref count will go up 1
Face1Ctrl.CreateOtherInterface(Face2Guid, out Face2IntPtr); 

//Face2 object's ref count will go up 2. One by "GetObjectForIUnknown()" 
//and one by "as", since the "as" will trigger .Net to call QueryInterface()
IFace2 Face2Ctrl = Marshal.GetObjectForIUnknown(Face2IntPtr) as IFace2; 

//=============Consume Face2Ctrl=========================

//======Destroy IFace1 and IFace2 interface===============

if (Face2Ctrl != null)
{
//Release 3 times as there were 3 RefCount obtained.

    Marshal.Release(Face2IntPtr);
    Marshal.Release(Face2IntPtr);
    Marshal.Release(Face2IntPtr);
    Face2Ctrl = null;     
}

if(Face1Obj != null)
{
//both Face1 object and Face2 object will get FinalRelease() after
//this line.
    Marshal.ReleaseComObject(Face1Obj); 
    Face1Obj = null;
}