Export (0) Print
Expand All

COM Interop Part 1: C# Client Tutorial

Visual Studio .NET 2003

COM Interop provides access to existing COM components without requiring that the original component be modified. When you want to incorporate COM code into a managed application, import the relevant COM types by using a COM Interop utility (TlbImp.exe) for that purpose. Once imported, the COM types are ready to use.

In addition, COM Interop allows COM developers to access managed objects as easily as they access other COM objects. Again, COM Interop provides a specialized utility (RegAsm.exe) that exports the managed types into a type library and registers the managed component as a traditional COM component.

At run time, the common language runtime marshals data between COM objects and managed objects as needed.

This tutorial shows how to use C# to interoperate with COM objects.

COM Interop Part 2: C# Server Tutorial covers using a C# server with a C++ COM client. For an overview of both tutorials, see COM Interop Tutorials.

Sample Files

See COM Interop Part 1 Sample to download and build the sample files discussed in this tutorial.

Further Reading

Tutorial

C# uses .NET Framework facilities to perform COM Interop. C# has support for:

  • Creating COM objects.
  • Determining if a COM interface is implemented by an object.
  • Calling methods on COM interfaces.
  • Implementing objects and interfaces that can be called by COM clients.

The .NET Framework handles reference-counting issues with COM Interop so there is no need to call or implement AddRef and Release.

This tutorial covers the following topics:

Creating a COM Class Wrapper

For C# code to reference COM objects and interfaces, you need to include a .NET Framework definition for the COM interfaces in your C# build. The easiest way to do this is to use TlbImp.exe (Type Library Importer), a command-line tool included in the .NET Framework SDK. TlbImp converts a COM type library into .NET Framework metadata — effectively creating a managed wrapper that can be called from any managed language. .NET Framework metadata created with TlbImp can be included in a C# build via the /R compiler option. If you are using the Visual Studio development environment, you only need to add a reference to the COM type library and the conversion is done for you automatically.

TlbImp performs the following conversions:

  • COM coclasses are converted to C# classes with a parameterless constructor.
  • COM structs are converted to C# structs with public fields.

A great way to check the output of TlbImp is to run the .NET Framework SDK command-line tool Ildasm.exe (Microsoft Intermediate Language Disassembler) to view the result of the conversion.

Although TlbImp is the preferred method for converting COM definitions to C#, it is not always possible to use it (for example, if there is no typelib for the COM definitions, or if TlbImp cannot handle the definitions in the typelib). In these cases, the alternative is to manually define the COM definitions in C# source code using C# attributes. Once you have created the C# source mapping, you simply compile the C# source code to produce the managed wrapper.

The main attributes you need to understand to perform COM mapping are:

  • ComImport - Marks a class as an externally implemented COM class.
  • Guid – Used to specify a universally unique identifier (UUID) for a class or an interface.
  • InterfaceType – specifies whether an interface derives from IUnknown or IDispatch.
  • PreserveSig – specifies whether the native return value should be converted from an HRESULT to a .NET Framework exception.

Each of these attributes is shown in the context of an actual example in this tutorial.

Declaring a COM coclass

COM coclasses are represented in C# as classes. These classes must have the ComImport attribute associated with them. The following restrictions apply to these classes:

  • The class must not inherit from any other class.
  • The class must implement no interfaces.
  • The class must also have a Guid attribute that sets the globally unique identifier (GUID) for the class.

The following example declares a coclass in C#:

// 
// declare FilgraphManager as a COM coclass 
// 
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] 
class FilgraphManager
{ 
}

The C# compiler will add a parameterless constructor that you can call to create an instance of the COM coclass.

Creating a COM Object

COM coclasses are represented in C# as classes with a parameterless constructor. Creating an instance of this class using the new operator is the C# equivalent of calling CoCreateInstance. Using the class defined above, it is simple to instantiate the class:

class MainClass 
{
    public static void Main() 
    {
        // 
        // Create an instance of a COM coclass - calls
        //
        // CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770, 
        //                  NULL, CLSCTX_ALL, 
        //                  IID_IUnknown, &f) 
        //
        // returns null on failure. 
        // 
        FilgraphManager f = new FilgraphManager(); 
    }
}

Declaring a COM Interface

COM interfaces are represented in C# as interfaces with ComImport and Guid attributes. They cannot include any interfaces in their base interface list, and they must declare the interface member functions in the order that the methods appear in the COM interface.

COM interfaces declared in C# must include declarations for all members of their base interfaces with the exception of members of IUnknown and IDispatch — the .NET Framework automatically adds these. COM interfaces which derive from IDispatch must be marked with the InterfaceType attribute.

When calling a COM interface method from C# code, the common language runtime must marshal the parameters and return values to/from the COM object. For every .NET Framework type, there is a default type that the common language runtime will use to marshal when marshaling across a COM call. For example, the default marshaling for C# string values is to the native type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling using the MarshalAs attribute in the C# declaration of the COM interface.

In COM, a common way to return success or failure is to return an HRESULT and have an out parameter marked as "retval" in MIDL for the real return value of the method. In C# (and the .NET Framework), the standard way to indicate an error has occurred is to throw an exception.

By default, the .NET Framework provides an automatic mapping between the two styles of exception handling for COM interface methods called by the .NET Framework.

  • The return value changes to the signature of the parameter marked retval (void if the method has no parameter marked as retval).
  • The parameter marked as retval is left off of the argument list of the method.

Any non-success return value will cause a System.COMException exception to be thrown.

This example shows a COM interface declared in MIDL and the same interface declared in C# (note that the methods use the COM error-handling approach).

Here is the original MIDL version of the interface:

[ 
  odl, 
  uuid(56A868B1-0AD4-11CE-B03A-0020AF0BA770), 
  helpstring("IMediaControl interface"), 
  dual, 
  oleautomation 
] 
interface IMediaControl : IDispatch 
{ 
  [id(0x60020000)] 
  HRESULT Run(); 

  [id(0x60020001)] 
  HRESULT Pause(); 

  [id(0x60020002)] 
  HRESULT Stop(); 

  [id(0x60020003)] 
  HRESULT GetState( [in] long msTimeout, [out] long* pfs); 

  [id(0x60020004)] 
  HRESULT RenderFile([in] BSTR strFilename); 

  [id(0x60020005)] 
  HRESULT AddSourceFilter( [in] BSTR strFilename, [out] IDispatch** ppUnk);

  [id(0x60020006), propget] 
  HRESULT FilterCollection([out, retval] IDispatch** ppUnk); 

  [id(0x60020007), propget] 
  HRESULT RegFilterCollection([out, retval] IDispatch** ppUnk); 

  [id(0x60020008)] 
  HRESULT StopWhenReady(); 
};

Here is the C# equivalent of this interface:

using System.Runtime.InteropServices;

// Declare IMediaControl as a COM interface which 
// derives from the IDispatch interface. 
[Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
    InterfaceType(ComInterfaceType.InterfaceIsDual)] 
interface IMediaControl // cannot list any base interfaces here 
{ 
    // Note that the members of IUnknown and Interface are NOT
    // listed here 
    //
    void Run();

    void Pause();
    
    void Stop();

    void GetState( [In] int msTimeout, [Out] out int pfs);

    void RenderFile(
    [In, MarshalAs(UnmanagedType.BStr)] string strFilename);

    void AddSourceFilter(
    [In, MarshalAs(UnmanagedType.BStr)] string strFilename, 
    [Out, MarshalAs(UnmanagedType.Interface)] out object ppUnk);

    [return : MarshalAs(UnmanagedType.Interface)]
    object FilterCollection();

    [return : MarshalAs(UnmanagedType.Interface)]
    object RegFilterCollection();
    
    void StopWhenReady(); 
}

Note how the C# interface has mapped the error-handling cases. If the COM method returns an error, an exception will be raised on the C# side.

To prevent the translation of HRESULTs to COMExceptions, attach the PreserveSig(true) attribute to the method in the C# declaration. For details, see PreserveSigAttribute Class.

Using Casts Instead of QueryInterface

A C# coclass is not very useful until you can access an interface that it implements. In C++ you would navigate an object's interfaces using the QueryInterface method on the IUnknown interface. In C# you can do the same thing by explicitly casting the COM object to the desired COM interface. If the cast fails, then an invalid cast exception is thrown:

// Create an instance of a COM coclass:
FilgraphManager graphManager = new FilgraphManager();

// See if it supports the IMediaControl COM interface. 
// Note that this will throw a System.InvalidCastException if 
// the cast fails. This is equivalent to QueryInterface for 
// COM objects:
IMediaControl mc = (IMediaControl) graphManager;

// Now you call a method on a COM interface: 
mc.Run();

Putting It All Together

Here is a complete example that creates an AVI file viewer using C#. The program creates an instance of a COM coclass, casts it to a COM interface, and then calls methods on the COM interface.

The examples in this section represent two approaches:

  • Example 1   Using TlbImp to create the .NET Framework class.
  • Example 2   Writing C# code that manually does the COM mapping.

Example 1: Using TlbImp

This example shows you how to create an AVI viewer using TlbImp. The program reads an AVI filename from the command line, creates an instance of the Quartz COM object, then uses the methods RenderFile and Run to display the AVI file.

These are the steps to build the program:

  • Run TlbImp over the TLB. The Media Player used in this example is contained in Quartz.dll, which should be in your Windows system directory. Use the following command to create the .NET Framework DLL:
    tlbimp c:\winnt\system32\quartz.dll /out:QuartzTypeLib.dll 
    

    Note that the resulting DLL needs to be named QuartzTypeLib, so the .NET Framework can load the containing types correctly when running.

  • You can use the Ildasm tool to examine the resulting DLL. For example, to display the contents of the file QuartzTypeLib.dll, use the following command:
    Ildasm QuartzTypeLib.dll
    
  • Build the program using the C# compiler option /R to include the QuartzTypeLib.dll file.

You can then use the program to display a movie (an example movie to try is Clock.avi, which resides in your Windows directory).

// interop1.cs
// compile with: /R:QuartzTypeLib.dll
using System;
class MainClass 
{ 
      /************************************************************ 
      Abstract: This method collects the file name of an AVI to 
      show then creates an instance of the Quartz COM object.
      To show the AVI, the program calls RenderFile and Run on 
      IMediaControl. Quartz uses its own thread and window to 
      display the AVI.The main thread blocks on a ReadLine until 
      the user presses ENTER.
            Input Parameters: the location of the AVI file it is 
            going to display
            Returns: void
      **************************************************************/ 
      public static void Main(string[] args) 
      { 
            // Check to see if the user passed in a filename 
            if (args.Length != 1)
            { 
                  DisplayUsage();
                  return;
            } 
 
            if (args[0] == "/?")
            { 
                  DisplayUsage();
                  return;
            } 
 
            string filename = args[0]; 
 
            // Check to see if the file exists
            if (!System.IO.File.Exists(filename))
            {
                  Console.WriteLine("File " + filename + " not found.");
                  DisplayUsage();
                  return;
            }
    
            // Create instance of Quartz
            // (Calls CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770,
            // NULL, CLSCTX_ALL, IID_IUnknown, &graphManager).): 
 
            try
            {
                  QuartzTypeLib.FilgraphManager graphManager = 
                        new QuartzTypeLib.FilgraphManager();
 
                  // QueryInterface for the IMediaControl interface:
                  QuartzTypeLib.IMediaControl mc =
                        (QuartzTypeLib.IMediaControl)graphManager;
 
                  // Call some methods on a COM interface 
                  // Pass in file to RenderFile method on COM object. 
                  mc.RenderFile(filename);
 
                  // Show file. 
                  mc.Run();
            }
            catch(Exception ex)
            {
                  Console.WriteLine("Unexpected COM exception: " + ex.Message);
            }
 
            // Wait for completion.
            Console.WriteLine("Press Enter to continue."); 
            Console.ReadLine();
      }
    
      private static void DisplayUsage() 
      { 
            // User did not provide enough parameters. 
            // Display usage: 
            Console.WriteLine("VideoPlayer: Plays AVI files."); 
            Console.WriteLine("Usage: VIDEOPLAYER.EXE filename"); 
            Console.WriteLine("where filename is the full path and"); 
            Console.WriteLine("file name of the AVI to display."); 
      } 
}

Sample Run

To display the example movie, Clock.avi, use the following command:

interop1 %windir%\clock.avi

This will display the movie on your screen after you press ENTER.

Example 2: The C# Code Approach

This example uses the same Main method as Example 1, but instead of running TlbImp, it simply maps the Media Player COM object using C#.

// interop2.cs
using System;
using System.Runtime.InteropServices;
 
namespace QuartzTypeLib 
{
      // Declare IMediaControl as a COM interface which 
      // derives from IDispatch interface:
      [Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), 
      InterfaceType(ComInterfaceType.InterfaceIsDual)] 
      interface IMediaControl   // Cannot list any base interfaces here 
      { 
            // Note that IUnknown Interface members are NOT listed here:
 

            void Run(); 
            void Pause();
 
            void Stop();
 
            void GetState( [In] int msTimeout, [Out] out int pfs);
 
            void RenderFile(
                  [In, MarshalAs(UnmanagedType.BStr)] string strFilename);
 
            void AddSourceFilter( 
                  [In, MarshalAs(UnmanagedType.BStr)] string strFilename, 
                  [Out, MarshalAs(UnmanagedType.Interface)]
                  out object ppUnk);
 
            [return: MarshalAs(UnmanagedType.Interface)] 
            object FilterCollection();
 
            [return: MarshalAs(UnmanagedType.Interface)] 
            object RegFilterCollection();
            
            void StopWhenReady(); 
      }
      // Declare FilgraphManager as a COM coclass:
      [ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] 
      class FilgraphManager   // Cannot have a base class or
            // interface list here.
      { 
            // Cannot have any members here 
            // NOTE that the C# compiler will add a default constructor
            // for you (no parameters).
      }
}
 
class MainClass 
{ 
      /********************************************************** 
      Abstract: This method collects the file name of an AVI to 
      show then creates an instance of the Quartz COM object.
      To show the AVI, the program calls RenderFile and Run on 
      IMediaControl. Quartz uses its own thread and window to 
      display the AVI.The main thread blocks on a ReadLine until 
      the user presses ENTER.
            Input Parameters: the location of the AVI file it is 
            going to display
            Returns: void
      *************************************************************/ 
 
      public static void Main(string[] args) 
      { 
            // Check to see if the user passed in a filename:
            if (args.Length != 1) 
            { 
                  DisplayUsage();
                  return;
            } 
 
            if (args[0] == "/?") 
            { 
                  DisplayUsage(); 
                  return;
            }
 
            String filename = args[0]; 
 
            // Check to see if the file exists
            if (!System.IO.File.Exists(filename))
            {
                  Console.WriteLine("File " + filename + " not found.");
                  DisplayUsage();
                  return;
            }
 
            // Create instance of Quartz 
            // (Calls CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770, 
            //  NULL, CLSCTX_ALL, IID_IUnknown, 
            //  &graphManager).):
            try
            {
                  QuartzTypeLib.FilgraphManager graphManager =
                        new QuartzTypeLib.FilgraphManager();
 
                  // QueryInterface for the IMediaControl interface:
                  QuartzTypeLib.IMediaControl mc = 
                        (QuartzTypeLib.IMediaControl)graphManager;
 
                  // Call some methods on a COM interface.
                  // Pass in file to RenderFile method on COM object.
                  mc.RenderFile(filename);
        
                  // Show file. 
                  mc.Run();
            }
            catch(Exception ex)
            {
                  Console.WriteLine("Unexpected COM exception: " + ex.Message);
            }
            // Wait for completion. 
            Console.WriteLine("Press Enter to continue."); 
            Console.ReadLine();
      }
 
      private static void DisplayUsage() 
      { 
            // User did not provide enough parameters. 
            // Display usage. 
            Console.WriteLine("VideoPlayer: Plays AVI files."); 
            Console.WriteLine("Usage: VIDEOPLAYER.EXE filename"); 
            Console.WriteLine("where filename is the full path and");
            Console.WriteLine("file name of the AVI to display."); 
      } 
}

Sample Run

To display the example movie, Clock.avi, use the following command:

interop2 %windir%\clock.avi

This will display the movie on your screen after you press ENTER.

See Also

C# Tutorials | COM Interop Tutorials

Show:
© 2014 Microsoft