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).
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 );
}
}
}
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);
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.
The Runtime Callable Wrapper performs many roles:
// 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 ).
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.
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.
A COM Server (written in VB or C++) can be used from a .NET client as follows:
If you load the assembly generated by tlbimp.exe into ILDasm.exe and examine the generated manifest, you will note the following
IDL Parameter Attribute | C# Parameter Keyword | Meaning | Example |
---|---|---|---|
[in] | No keyword. This is the assumed direction | Called function receives copy of data. | OnlyInParam( int ) |
[out] | out | Value assigned in callee and returned to caller. | OnlyOutParam( out int ) |
[in,out] | ref | Value is assigned by caller but may be reallocated/changed by callee. | InAndOutParam( ref int ) |
[out, retval] | n/a | The type becomes the physically returned value of the function. | RetVal() |
/* IDL */
// The coclass supports two incoming interfaces and one
outgoing interface
coclass Car
{
[default] interface ICar;
// Exposes Drive method
interface IDriverInfo;
// Exposes Name method
[default, source] dispinterface _ICarEvents;
};
/* C# client */
// Method 1: Access methods using the coclass
Car c = new Car();
c.Drive(); //
ICar interface method
c.Name();
// IDriverInfo interface method
// Method 2: Explicitly access interfaces
IDriverInfo iDI = (IDriverInfo)c;
iDI.Name();
Car c = new Car();
string[] names = (string[]) c.GetNames();
// Assume GetNames() returns a SAFEARRAY of strings
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:
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 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)
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.
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) { ... }
}
}
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:
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
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")]
An assembly is configured into COM+ using regsvcs.exe tool. This tool performs the following actions: