Interacting with Unmanaged Code

Summary

Understanding Interoperability Issues

Key point: When you build assemblies using .NET-aware compilers, you are creating managed code that can be hosted by the Common Language Runtime. Managed code offers a number of benefits such as automatic memory management, a unified type system (CTS), self-describing assemblies and so-forth.

On the other side, COM servers bear no relationship to .NET assembles (beyond a shared file extension ,DLL). COM servers contains platform-specific code, not platform-agnostic IL instructions. Also COM servers work only with a unique set of data types (BSTR, VARIANT, etc.). COM servers require code support  in the form of class factories, IDL, and registry entries. Finally, COM types must be properly reference-counted.

.NET types and COM types have so little in common, however these two architectures can coexist, i.e., interoperate. In general, the .NET framework supports the following types of interoperability:

When .NET interoperability services are used, you directly or indirectly interact with the types defined by System.Runtime.InteropServices namespace. For example, InterfaceTypeAttribute class controls how a managed interface is exposed to COM clients (IDispatch- or IUnknown-derived).

Interacting with C DLLs

Platform Invocation Services (PInvoke) provide a way for managed code to call unmanaged functions implemented in a traditional C (non-COM) DLL. PInvoke shields the developer from the task of location and invoking the function export. It also takes care of marshalling managed data to and from their unmanaged counterparts.

The most typical use of PInvoke is to allow .NET components to call raw Win32 API. It is also typically used to access function exports defined in custom DLLs.

using System;
using System.Runtime.InteropServices;     // Must reference to gain access to PInvoke services

namespace PInvokeExample
{
    class PInvokeClient
    {
        /* The process of calling a Win32 API begins by declaring the function to call 
        * with the static and extern C# keywords. Note that the return type and arguments
        * are specified in terms of managed data types. Once it has been declared it must
        * be adorned with the DllImport attribute; at a minimum you must specify the name
        * of the DLL where the function lives
        *
        * Recall: DllImport attribute is really an instance of class DllImportAttribute */
        [DllImport("user32")]
        public static extern int MessageBox( int hWnd, string strMsg, string strTitle, int nType);

        static void Main(string[] args)
        {
            // Call a Win32 API
            MessageBox(0, "Hello World", "PInvoke test", 0 );
        }
    }
}

DLLImportAttribute Type

This type is part of the System.Runtime.InteropServices namespace and is used to indicate that the attributed method is implemented as an export from an unmanaged DLL. DLLImportAttribute defines a set of public fields which can be specified to further configure the process of binding to the export function.

// Fully Setting DllImport attribute. EntryPoint filed is used to establish an alias.
[DllImport("user32", ExactSpelling=true, CharSet=CharSet.Unicode, EntryPoint="MessageBoxW")]
public static extern int DisplayMessage( int nHwnd, string s1, string s2, int nType);

 

Understanding .NET to COM Interoperability

How can managed code use unmanaged COM types? The Runtime Callable Wrapper (RCW) is an intervening layer that correctly exposes COM types as .NET equivalents. The RCW can be understood to be a proxy to the real COM class. Every coclass accessed by .NET requires an RCW:

Also be aware that there is a single RCW per COM object, regardless how many interfaces the .NET client has obtained from a given COM class. RCW is generated automatically using tlbimp.exe tool (type library importer). Also note that legacy COM servers do not require modifications to be consumed by a .NET aware language.

RCW

The Runtime Callable Wrapper performs many roles:

Expose COM data types as their .NET equivalents

// COM IDL method definition
HRESULT DisplayName( [in] BSTR strName);

// C# mapping of COM IDL method
void DisplayName( String strName);

The following table documents some of the mapping between COM IDL data types and .NET data types (and their corresponding C# alias)

COM IDL Data Type .NET Data Type C# Alias
char, boolean, small System.SByte sbyte
wchar_t, short System.Int16 short
long, int System.Int32 int
unsigned char, byte System.Byte byte
unsigned short System.UInt16 ushort
unsigned long, unsigned int System.UInt32 uint
double System.Double double
HRESULT System.Int32 int
BSTR, char*, LPSTR, wchar_t*, LPWSTR System.String string
VARIANT, IUnknown*, IDispatch* System.Object object
Decimal System.Decimal n/a
Date System.DateTime n/a
GUID System.Guid n/a
Currency System.Decimal n/a

Note that an IDL pointer definition will point to the same base class and C# alias (int maps to System.Int32 ).

Managing a Coclass's Reference Count

The RCW manages the reference count of the underlying coclass because .NET types do not use the COM reference-counting scheme. The RCW caches all interface references internally and triggers the final release when the type is no longer used by the .NET client.

Hiding Low-Level COM Interfaces

The RCW also hides low-level COM interfaces from .NET clients (same approach as VB6). For example, with COM connection points, a C++ COM server must implement IConnectionPointContainer and friends, while VB6 hides all this using the WithEvents keyword. In the same vein, RCW also hides all these low level details, and therefore, the .NET client only sees and interacts with the custom interfaces implemented by the COM coclass

The RCW hides the following interfaces: IClassFactory, IConnectionPointContainer, IConnectionPoint, IDispatch, IDispatchEx, IProvideClassInfo, IEnumVariant, IErrorInto, ISupportErrorInfo, IUnknown and others.

Using a COM Server with .NET

A COM Server (written in VB or C++) can be used from a .NET client as follows:

Examining the Generated Assembly

If you load the assembly generated by tlbimp.exe into ILDasm.exe and examine the generated manifest, you will note the following

Understanding COM to .NET interoperability

For a COM class to use a .NET type, you need to fool the coclass into believing that the managed type is in fact unmanaged. For example, the COM type must be able to obtain new interfaces using QueryInterface(), simulate unmanaged memory management with AddRef() and Release(), and so on. To make .NET assemblies available to a class in a COM server, you must take the following steps:

The Role of the CCW

When a COM client accesses a .NET type, the CLR uses a proxy called the COM Callable Wrapper (CCW) to negotiate the COM to .NET navigation. See below

The CCW is a reference-counted entity. When the COM client has issued the final release, CCW releases its reference to the .NET type, at which point, it is ready to garbage-collected. The CCW implements a number of interfaces to fool the COM client into thinking that it is dealing with a real coclass. The CCW also provides support for the standard COM interfaces in the following table

COM Implemented Interface Meaning
IConnectionPointContainer
IConnectionPoint
The .NET supports events.
ISupportErrorInfoIErrorInfo
IErrorInfo
Allow COM coclasses to send COM error information
IEnumVariant The .NET type supports the IEnumerable interface.
ITypeInfo
IProvideClassInfo
Allow COM client to interact with the .NET metadata
IUnknown 
IDispatch
IDispatchEx
Provide support for early and late binding to the .NET type.

The Class Interface

The CCW takes the same approach as VB6. Consider the following VB code where a COM client pretends to work with an object reference, obRS, when they are really working with an interface reference:

Dim o as ADODB.Recordset    ' Query for: [default] _Recordset
o.Open( ... )               ' Really calls _Recordset->Open(...)

In contrast, .NET types do not need to support any interfaces. However, given that class COM clients cannot work with object references, another responsibility of the CCW is to support a class interface to represent each property, method, field, and event defined by the public sector of the .NET type.

By default, any method defined on a .NET class is exposed to COM as a raw dispinterface. This means that all COM clients that want to use class-level methods to manipulate the .NET type must use late binding. To alter this behavior use the ClassInterfaceAttribute type, which can be assigned any value of the ClassInterfaceType enumeration (AutoDispatch, AutoDual, and None).

using System.Runtime.InteropServices;
...

public interface IAdvancedMath { ... }

[ClassInterface( ClassInterfaceType.AutoDual )]        // Creates a dual interface
public class Math : IAdvancedMath
{
    ...
}

Once the .NET project is compiled, there are two approaches to registering the assembly and creating type library information

We get the following coclass if a type library was generated for the above .NET code

// Other IDL
...

coclass Math
{
    [default] interface _Math;
    interface IAdvancedMath;  
    interface IManagedObject;
    interface  _Object;        // unmanaged representation of .NET's System.Object
};

Finally, note that all registered .NET assemblies are members of the .NET category (use OLE/COM Object Viewer to view this category)

.NET to COM Mapping Issues

Key fact: The Common Type System (CTS) defines a number of constructs that simply cannot be represented by classic COM.  For example, classic COM classes cannot have non-default constructors, overloaded operators, overloaded methods, and cannot derive from each other using classical inheritance. Therefore, tlbexp.exe performs some magic when building the COM type library.

For example, public field data are represented as properties. Also, because COM does not support classical inheritance between types, tlbexp.exe models the is-a relationship between the base and derived types using interface implementation.

Controlling the generated IDL 

When you build .NET types that you expect to be used by classic COM clients, you can make use of attributes to override the default attribute mappings generated by tlbexp.exe.

namespace NS
{
    using System;
    using System.Runtime.InteropServices;

    // This interface has attributes that will be used by tlbexp
    [ GuidAttribute("12345678-1111-2222-3333-123456789ABCD"),          // Interface GUID
      InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)        // Interface is dual and derived from IDispatch
    ]
    public interface IMath
    {
        [DispId(123)] int Add(int x, int y);                            // Method id
    }
    

    [ GuidAttribute("12345678-1111-2222-3333-123456789ABCD") ]
    public class Math
    {
        ...

        // This attribute causes ExtraRegistrationLogic to be called during registration. Add any extra logic
        [ComRegisterFunctionAttribute]
        public static void ExtraRegistrationLogic( string strLoc) { ... }

        // This attribute causes ExtraUnregistrationLogic to be called during un-registration. Add any extra logic
        [ComUnregisterFunctionAttribute]
        public static void ExtraUnregistrationLogic( string strLoc) { ... }
    }
}

 

Interacting With COM+ Services

Key point: To build managed types that can be configured to run under the COM+ runtime, you need to equip your .NET entities with many attributes that are defined in System.Runtime.EnterpriseServices namespace.

The general process of building .NET assemblies for COM+ is as follows:

Building a COM+ Aware C# Type

Here is a simple COM+ class using C#

using System.EnterpriseServices;

// This object is poolable and supports constructor strings
[ ObjectPooling(true, 5, 100),
  ConstructionEnabledAttribute(true) ]
public class MyFirstComPlus : ServicedComponent, IObjectConstruct
{
    // Implementation of IObjectContruct
    public void Construct(object o)
    {
        IObjectConstructString ics = (IObjectConstructString)o;
    }
        
    // Implementation of inherited abstract members
    public override void Activate()     { ... }
    public override void Deactivate()   { ... }
    public override bool CanBePooled()  { return true; }

    ... 
}

Given that this assembly will end in the GAC, you will need to build a strong name for it using the sn.exe utility

Assembly-Level COM+ Attributes

Certain aspects of the containing COM+ application can be specified using Assembly-Level attributes (simply place them in the AssemblyInfo.cs file).

The following assembly-level attributes specify that this assembly should be deployed in a server application

[assembly: ApplicationActivation(ActivationOption.Server) ]
[assembly: ApplicationID("12345678-1111-2222-3333-123456789ABCD")]
[assembly: ApplicationName("MyFirstCOMPLUSTApplication")]

Configuring an Assembly in the COM+ Catalog

An assembly is configured into COM+ using regsvcs.exe tool. This tool performs the following actions: