Freigeben über


Interop 101 - Part 3

In the last part of this little series, we looked into how C# (and .NET languages in general) can call into native code as directly as possible through P/Invoke. While this is a viable technique in many cases, it doesn’t scale to complex interop nicely. Users of Java probably recognized it as something quite similar to JNI (Java Native Interface). Of course, as I mentioned in my first post on the subject, there are a number of ways to perform interop on Windows and in this part we are going to delve into COM interop.

 

The Component Object Model or COM does not need much introduction by now, as it’s been around since for over 10 years now. COM was actually designed to solve a more general interop problem than the native/managed issue we’re looking at. I have yet to come up with my own great one-liner to describe COM so I’ll quote Wikipedia to say that “The essence of COM is a language-neutral way of implementing objects such that they can be used in environments different from the one they were created in, even across machine boundaries.” Sounds like a perfect candidate for interop with managed code, after all, what’s the CLR if not a difference type of machine (the JVM makes it even clearer by keeping the work machine in its name).

 

There are two “starting points” in my COM scenario. The first (and easiest) is when you already have a COM component. I’ve seen many companies that already wrap their functionality in COM (heck, just look at the number of COM type libraries that are installed on your box right now) so it’s a fairly common starting point. On the other hand, we have our old Hello World class, which is a simple ANSI C++ piece of code (granted it calls into a Win32 API but let’s imagine it’s something cross-platform). In these cases, you need to wrap this object into COM before we can discuss the interop story.

 

In my very humble opinion, COM is powerful but complicated, and wrapping functionality into COM can be a pain. Enter ATL and Visual C++ wizards, which are the easiest way of writing a COM component. First thing is the New Project dialog, where you should choose the ATL Project wizard and then choose to make it an attributed DLL on the application settings page (I’ll spare the screenshots of the wizard). Two projects are created, one of which is something of an implicit project. Now we have a COM object, which will be our Hello World wrapper object, so we need to do two things: encapsulate the original class and add SayThis to the COM object’s interface. The first part is traditional C++, just #include the HelloWorld.h header and adding a class which will encapsulate the original one. The second comes by using the Add Method wizard on the wrapper class. Ok what does this look like? Behold the magic of ATL code (believe me these few lines hide a lot of underlying functionality).

 

The COM interface

// IHelloWorld

[

      object,

      uuid("784C374B-2DAF-4ECF-BDE6-90360E7630B6"),

      dual, helpstring("IHelloWorld Interface"),

      pointer_default(unique)

]

__interface IHelloWorld : IDispatch

{

      [id(1), helpstring("method SayThis")] HRESULT SayThis([in] BSTR phrase);

};

The COM object declaration

// CHelloWorld

[

      coclass,

      default(IHelloWorld),

      threading(apartment),

      vi_progid("comwrapper.HelloWorld"),

      progid("comwrapper.HelloWorld.1"),

      version(1.0),

      uuid("9CCC02BA-2A46-423E-8E3B-80FB1DD6A6B1"),

      helpstring("HelloWorld Class")

]

class ATL_NO_VTABLE CHelloWorld : public IHelloWorld

{

public:

      CHelloWorld() : hw(new HelloWorld())

      {

      }

      ~CHelloWorld()

      {

            delete hw;

      }

      DECLARE_PROTECT_FINAL_CONSTRUCT()

      HRESULT FinalConstruct()

      {

            return S_OK;

      }

      void FinalRelease()

      {

      }

private:

      HelloWorld *hw; // The original encapsulated native HelloWorld

public:

      STDMETHOD(SayThis)(BSTR phrase); // The exported method

};

 

The COM object implementation

// CHelloWorld

STDMETHODIMP CHelloWorld::SayThis(BSTR phrase)

{

      hw->SayThis(phrase);

      return S_OK;

}

 

Alright, so this all compiles nicely into a DLL, you can register it with regsvr32.exe, and now we want to do invoke this functionality from C#!

After seeing the magic of ATL, now we’ll see the magic of the CLR and its built-in COM interop functionality. First thing to do is add a reference to our COM component. Nothing could be easier: just right-click on the references folder in our C# project and select “Add Reference”. In the resulting dialog, we select the COM tab and in the list we should find the component we created above. Once we add it, Visual Studio will do all sorts of wondrous things to fool the project that it has something that looks like a managed assembly. If our wrapper is called comwrapper then here is our resultant C# caller code.

 

using System;

using System.Collections.Generic;

using System.Text;

using comwrapper; // The COM DLL we created

namespace CSharpComCaller

{

    class Program

    {

        static void Main(string[] args)

        {

            CHelloWorld chw = new CHelloWorld(); // CHelloWorld is the COM wrapper object

            chw.SayThis("I'm a C# application calling native code via COM interop!");

        }

    }

}

 

We’re done! What do you think? Pretty elegant solution, no? It’s pretty close to perfect when you already have COM components. Is there a flip-side though? There are two. One is the fact that you need to create COM wrappers, which can be a hassle. The second is that all the magic of COM and COM interop comes at a price. Performance. Obviously, there’s always a performance price when doing any kind of interop, but if you have a simple native type and you want to call it from managed code, using COM interop has by far the worst performance cost. This fact brings us to the lesser-known yet infinitely more powerful and elegant method that is native C++ interop, which we will lay bare in our next installment of this series. Eventually, we’ll do a performance comparison as well so that I may back up this claim (probably not an extremely scientific one though).

 

In the meantime, if you’d like to learn more (there’s a lot) about COM interop, look here.

Comments