Writing Your Own COM Interop in C#

Introduction

Although the automatic creation of COM interop DLLs is very useful, it is not perfect.

In the case of interfacing with OLE conforming COM objects (for example, interfaces derived from IDispatch and method parameters that are part of the VARIANT structure), the interop DLL automatically created by VisualStudio (or rather by TlbImp.exe, the application that generates the interop DLL) is more often than not sufficient.

However, this method does have its drawbacks. First, it requires the creation of an additional DLL for each COM DLL.

Second, if you need to use IUnknown-based interfaces that may not even be defined in the type library, no automatic interop DLL can be created.

Third, the marshalling of method parameters in such cases is often incorrect, rendering the automatic interop DLL useless.

And finally, there's no way to group the objects into seperate namespaces—meaning the interop DLL for a COM DLL containing many objects tends to create a huge jumble of classes with no clear ordering. One way around this problem is to write a wrapper in C++.NET, but this still requires an additional interfacing DLL to exist in between the C# client and the COM DLL.

In this article, I will explore how to use P/Invoke, and the COM interop classes to gain access to COM objects and present a class structure to ease the process. I will concentrate on COM inproc-servers in this article, but the methods described here can be applied to any type of server—even DCOM-based servers.

Instansiating a COM Object

How do you instansiate a COM object? In actual fact, the same way that you do in C++—by using the CoCreateInstance method. This is defined in ole32.dll.

For information about the CoCreateInstance method, see the following Web site:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/cmf_a2c_1nad.asp.

The interop declaration is as follows:

public class Ole32Methods
{
   [DllImport("ole32.Dll")]
   static public extern uint CoCreateInstance(ref Guid clsid,
      [MarshalAs(UnmanagedType.IUnknown)] object inner,
      uint context,
      ref Guid uuid,
      [MarshalAs(UnmanagedType.IUnknown)] out object rReturnedComObject);
}

A point to note is the [MarshalAs(UnmanagedType.IUnknown)] attributes. These inform the P/Invoke mechanism that the objects are in fact IUnknown interfaces and should be treated as such. The result of these attributes is to wrap up the 'object' parameters in an internal .NET class that handles the IUnknown methods of AddRef, QueryInterface, and Release (__ComObject). Therefore, because P/Invoke knows that the return object is in fact an IUnknown, its Release method will be called when the object is finalized.

If the COM object doesn't exist, or there was an error in the creation of the COM object, the rReturnedComObject parameter will be set to null and an error code (for example, a value not equal to zero) will be returned from the function. The Guid struct is already defined in .NET, and is synonymous with the GUID struct in C++.

Finally, I've labelled rReturnedComObject as 'out' and not 'ref'. This indicates that the object returned is created inside of the method. An example of the code to instansiate a COM object using this method is shown below.

static bool CreateObjectExample()
{
   const uint CLSCTX_INPROC_SERVER = 1;

   // CLSID of the COM object
   Guid clsid = new Guid("F333F56A-B59D-41FE-822D-27989266A535");

   // GUID of the required interface
   Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");

   object instance = null;

   uint hResult = Ole32Methods.CoCreateInstance(ref clsid, null,
                  CLSCTX_INPROC_SERVER, ref IID_IUnknown, out instance);

   return hResult == 0;
}

Unfortunately, the object that is returned isn't much use at present. It effectively represents an instance of the COM object without any interfaces, or at least with just an IUnknown interface to which you don't have access.

Defining Interfaces

You need to manually define each interface on the COM object. These can be derived from the .idl of the COM DLL in question. In defining the methods of the interfaces, standard P/Invoke marshalling applies. Consider the following method definition as it would appear in a .idl file.

[
   object,
   uuid(F333F56A-B59D-41FE-822D-27989266A535),
        pointer_default(unique)
]
interface ITestInterface1 : IUnknown
{
   HRESULT GetString(LPSTR szString, int * pnLength);
};

Below is shown the C# definition of this interface using P/Invoke.

[Guid("F333F56A-B59D-41FE-822D-27989266A535"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestInterface1
{
   int GetString([MarshalAs(UnmanagedType.LPStr)] StringBuilder builder,
                 ref int pnLength);
}

Now, examine the attributes attached to the interface itself. The Guid attribute is the ID of the interface. The InterfaceType attribute indicates the type of the interface—in this case, that it is an IUnknown-derived interface.

Note: P/Invoke has no way of determining the order of the methods in the interface, and so they must therefore be in the same order as in the .idl file.

Writing Your Own COM Interop in C#

Obtaining Interfaces to COM Instances

In C++, you would call QueryInterface on the IUnknown pointer to obtain interfaces to the COM object's instance so that you can use the methods on them. However, at present all you have is an object instance with no methods you can call, and an interface definition. In fact, the answer is simple: Just use the as operator to change the type of the object:

ITestInterface1 iTestInterface1 = instance as ITestInterface1;

This automatically carries out a QueryInterface, and also AddRefs the instance of the COM object. Again, when the instance of the interface is finalized, its Release method is automatically called. If the COM object doesn't support that particular interface, the interface variable will be set to null.

Releasing COM objects

The garbage collector will automatically call release on each COM interface for you when the object is finalised, but you also can explicitly call release on an interface by using this method in the Marshal class:

System.Runtime.InteropServices.Marshal.ReleaseComObject(iTestInterface1);
System.Runtime.InteropServices.Marshal.ReleaseComObject(instance);

The CodeGuru.Darwen.Com Class Library

To simplify the above mechanisms, this article presents a class library that contains, amongst other things, a class that can be used to create COM objects easily. The definition of this class is shown below.

public class ComObject : IDisposable
{
   private object m_object = null;
   private uint m_hResult = 0xFFFFFFFF;
   private const uint CLSCTX_INPROC_SERVER = 1;
   private static Guid IID_IUnknown = new
           Guid("00000000-0000-0000-C000-000000000046");

   public ComObject()
   {
      object [] aAttributes = GetType().GetCustomAttributes
                              (typeof(ClsidAttribute), true);

      // checking code has been removed here for readability

      ClsidAttribute attribute = aAttributes[0] as ClsidAttribute;
      Guid clsid = new Guid(attribute.Clsid);

      m_hResult = Ole32Methods.CoCreateInstance(ref clsid,
                                                null,
                                                CLSCTX_INPROC_SERVER,
                                                ref IID_IUnknown,
                                                out m_object);
   }

   public object Unknown
   {
      get
      {
         return m_object;
      }
   }

   public virtual void Dispose()
   {
      if (m_object != null)
      {
         System.Runtime.InteropServices.Marshal.ReleaseComObject(m_object);
         m_object = null;
      }
      
      GC.SuppressFinalize(this);
   }

   // other properties removed for readability
}

This class searches its inheritance tree looking for a ClsidAttribute, which is simply a custom attribute taking a GUID in string form. From this attribute, it takes the CLSID needed to instansiate the COM object, and calls CoCreateInstance with the GUID of IUnknown. To use this class, derive from it and attach a ClsidAttribute with the CLSID of the COM object to be created.

[Clsid("3C526F5F-9AAC-4F70-9A99-C2D455A7AF87")]
public class TestComObject : ComObject
{
   public TestComObject()
   {
   }
}

Therefore, to use the following, all that needs to be done is this:

using (TestComObject testComObject = new TestComObject())
{
   ITestInterface1 iTestInterface1 = testComObject.Unknown as ITestInterface1;
   System.Runtime.InteropServices.Marshal.ReleaseComObject(iTestInterface1);
} // testComObject.Dispose is called on exit

Conclusion

You have seen how to instansiate a COM object using a P/Invoke call to CoCreateInstance. You have further seen how to define interfaces and how to obtain pointers to these interfaces from a COM instance. I have taken inproc-servers as my example, but this method can equally be applied to any type of COM server—even a DCOM-based server.

The samples supplied include a test COM object written in C++, the CodeGuru.Darwen.Com class library, and a test C# console application demonstrating the mechanism. The solution file for the C# is in the same directory as the application, and the solution file for the C++ DLL is in the same folder as the DLL.

You will, of course, need to build and register the COM DLL before running the C# test application.



About the Author

David McClarnon

He first encountered Windows programming using Visual C++/MFC version 1.5 on Windows 3.11 a very long time ago. He is now a contract developer specialising in .NET/native interop with p/invoke.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • Due to internal controls and regulations, the amount of long term archival data is increasing every year. Since magnetic tape does not need to be periodically operated or connected to a power source, there will be no data loss because of performance degradation due to the drive actuator. Read this white paper to learn about a series of tests that determined magnetic tape is a reliable long-term storage solution for up to 30 years.

Most Popular Programming Stories

More for Developers

RSS Feeds