.NET

.NET Interoperability: COM Interop

Brian Long (www.blong.com)

Table of Contents

Click here to download the files associated with this article.

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

.NET is a new programming platform representing the future of Windows programming. Developers are moving across to it and learning the new .NET oriented languages and frameworks, but new systems do not appear overnight. It is a lengthy process moving entire applications across to a new platform and Microsoft is very much aware of this.

To this end, .NET supports a number of interoperability mechanisms that allow applications to be moved across from the Win32 platform to .NET piece by piece, allowing developers to still build complete applications, but which comprise of Win32 portions and some .NET portions, of varying amounts.

When building new .NET applications, there are provisions for using existing Win32 DLL exports (both custom DLL routines and standard Win32 API routines) as well as COM objects (which then act like any other .NET object).

When building Win32 applications there is a process that allows you to access individual routines in .NET assemblies. When building Win32 COM client applications, there is a mechanism that lets you use .NET objects as if they were normal COM objects.

This paper investigates the interoperability options that involve COM (generally described as COM Interop)

The accompanying paper, .NET Interoperability: .NET <-> Win32 (see Reference 1), looks at interoperability between .NET and Win32 that does not involve COM.

The coverage will have a specific bias towards a developer moving code from Borland Delphi to Borland Delphi for .NET, however the principles apply to any other development tools. Clearly the Delphi-specific details will not apply to other languages but the high-level information will still be relevant. You can find the full story of COM Interop in the book .NET and COM (see Reference 2).

Because of the different data types available on the two platforms (such as PChar in Win32 and the new Unicode String type on .NET), inter-platform calls will inevitably require some form of marshaling process to transform parameters and return values between the data types at either end of the call. Fortunately, as we shall see, the marshaling is done for us after an initial process to set up the inter-platform calls.

In a COM Interop system, there must be some form of reconciliation between the COM reference counting model and the .NET garbage collection model. Again, after the initial setup step, this is all taken care of for the developer by wrapper objects manufactured by the .NET support tools.

Note: the information in the paper is based around the Delphi for .NET Preview compiler as shipped with Delphi 7 and Updates to it made available after Delphi 7's release. At the time of writing the Delphi for .NET Preview compiler represents the only .NET-enabled version of Delphi available. When Delphi 8 for .NET (codenamed Octane) becomes available, various steps in this paper that use the Delphi for .NET Preview compiler, as well as some of the code snippets, may require changes.

Note: an updated version of this article specific to Delphi 8 for .NET is now available online. Click here to read it.

.NET Clients Using Win32 COM Server Objects (RCW)

You may start writing new .NET applications (or porting Win32 COM applications over to .NET) and need to access existing COM objects from within them. In order for .NET to use a COM object, wrapper objects called Runtime-Callable Wrappers (RCW objects) need to be generated. These wrapper objects cater for the difference in lifetime management between .NET and COM. RCW objects are .NET objects that manage the reference count of a COM object as well as dealing with the marshaling of parameters and return types for the COM object methods.

Interop Assemblies

RCW objects are manufactured at runtime by the CLR using information found in an Interop Assembly (an assembly containing definitions of COM types that can be used from managed code). You use a type library importer to scan the COM server type library and generate appropriate .NET-compatible information in an Interop Assembly for your COM server.

The type library importer can be invoked from a utility, Tlbimp.exe, that is supplied with the .NET Framework SDK. You can also do it under program control using the TypeLibConverter class in the System.Runtime.InteropServices namespace, although use of the utility program is much more common.

Creating An Interop Assembly

To see the process, let's consider an in-proc COM server DLL, called COMServer.dll, which contains a coclass COMObject (optionally accessible through the ProgID COMServer.COMObject) that implements an interface, ICOMInterface:


ICOMInterface = interface(IUnknown)
  ['{8E9B4AAE-5290-4360-AA38-97EA7E43375E}']
  procedure One(const Msg: WideString); safecall;
  function Two(Input: Integer; out Output: Integer): WordBool; safecall;
end;

To generate an Interop Assembly use the TlbImp.exe utility like this:


C:\Temp>TlbImp comserver.dll /verbose /out:Interop.ComServer.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001.  All rights reserved.

Type ICOMInterface imported.
Type COMObject imported.
Type library imported to C:\Temp\Interop.ComServer.dll

Notice the name of the Interop Assembly follows a convention. Visual Studio.NET uses the Interop prefix convention, which seems descriptive enough for Interop Assemblies that describe custom COM servers. The other half of the assembly name is typically set to the library name, as found in the type library. If you open the type library in Delphi the top level node in the Type Library Editor tells you the library name. By default, Delphi-written COM servers use the project name, but this is not always the case.

Despite this convention you can actually call the Interop Assembly what you like. So long as the reference in the Delphi for .NET uses clause matches the Interop Assembly name, things will work out (however the assembly references are case sensitive and cause an internal error of you get the case wrong in your uses clause).

Note that the type library importer makes certain assumptions about the use of parameters and sometimes will use parameter types that are not the most appropriate. You can modify the results of the import process using creative round tripping against the Interop Assembly. The technique of creative round tripping is discussed in Reference 1 and Reference 3. You can also find mention of it, along with advice on how to resolve common Interop Assembly errors in Reference 4.

Referencing An Assembly From Delphi for .NET

Now that we have an assembly we need to access it from Delphi for .NET. The command-line switch for referencing other assemblies is -LU (the same switch used to reference packages in Delphi for Win32).

With the original Delphi for .NET Preview, you must copy the assembly into either the .NET Framework directory (C:\WINNT\Microsoft.NET\Framework\v1.0.3705), or install it into the system GAC in order to successfully reference it. However the Delphi for .NET Preview Update removes this requirement; it now searches for referenced assemblies on the unit search path. The Update also accepts a full path to an assembly (including file extension).

When referencing an assembly you normally omit the file extension (unless supplying a full path to it, in which case you must pass the file extension):


dccil -LUInterop.ComServer dotNetApp.dpr

The intent of the -LU switch is to reference an assembly that does not have pre-generated compiler files available in the $(DFDN)\units directory (where $(DFDN) represents the Delphi for .NET Preview installation directory). The $(DFDN)\units directory contains files (compiled IL units, with a .dcuil extension, and assembly catalogue files, with a .dcua extension) for most of the standard RTL units and .NET assemblies. Where a referenced assembly has these files in $(DFDN)\units, the -LU switch is not required.

With the original Delphi for .NET Preview you may well find that this compilation process generates a fatal internal error (due to issues resolved in later builds). Another option that may resolve the problem, if it occurs, is to rebuild the whole RTL (runtime library), including a reference to your target assembly, forcing DCCIL to generate .dcua and .dcuil files for your assembly. These files should facilitate a successful compilation. Note that this operation is not required with the Delphi for .NET Preview Update.

Rebuilding The Delphi for .NET RTL

The intermediate compiled files for all the standard units and assemblies are located in $(DFDN)\units and the RTL source is in $(DFDN)\source\rtl. In this latter directory you can find a batch file called rebuild.bat, supplied to enable you to recompile the RTL units and assembly reference files for .NET Service Pack 2 or later (the supplied files are compiled for SP1). This batch file is currently the key to successfully linking to any arbitrary assembly of your choice with the original Delphi for .NET Preview, since the normal approach frequently fails with internal errors.

The batch file contains an important line that recompiles the core RTL Borland.Delphi.System unit, as well as generates reference files for a whole bunch of assemblies (and the assemblies they depend on):


dccil -m -Y -n..\..\units -lumscorlib -luSystem -luSystem.Windows.Forms -luSystem.XML -luSystem.Web -luSystem.Web.Services Borland.Delphi.System.pas

To generate appropriate intermediate files required to link to an assembly of your choice requires adding an extra option to this line, and then running the batch file. In our example we need to link to the Interop.ComServer.dll assembly, so change the batch file line to this:


dccil -m -Y -n..\..\units -lumscorlib -luSystem -luSystem.Windows.Forms -luSystem.XML -luSystem.Web -luSystem.Web.Services -luInterop.ComServer Borland.Delphi.System.pas

Now we are ready to rebuild, so run the batch file. Whilst rebuilding there will be a number of Identifier Redeclared errors when Borland.Delphi.System.pas compiles. This is a known issue and should be ignored; the output of the compile will be correct.

After the build process $(DFDN)\units will contain Interop.ComServer.dcua and Interop.ComServer.dcuil, files that allow you to successfully link to the assembly.

Early Binding

The most common requirement will be to use early binding to get compile-time type checking and direct (well, as direct as it gets) vtable calls to the COM object:


uses
  System.Windows.Forms,
  Interop.ComServer;
...
procedure TfrmRCW.btnEarlyBoundClick(Sender: TObject;
  Args: EventArgs);
var
  ComObj: IComInterface;
  Answer: Integer;
const
  Val = 45;
begin
  ComObj := ComObjectClass.Create as IComInterface;
  ComObj.One('Hello world');
  if ComObj.Two(Val, Answer) then
    MessageBox.Show(Format('Twice %d is %d', [Val, Answer]));
end;

Notice that the interface type name comes through unchanged, but to create an instance of an exposed coclass Foo you must use the RCW object FooClass.

Note that with the original Delphi for .NET Preview it is still necessary for the Interop Assembly to be in the .NET system directory or GAC during the application compile/link cycle, even with the DCUA and DCUIL files being present in the $(DFDN)\units directory. Because of this you should leave the file in the .NET Framework directory. Once the application is compiled you can delete it from this directory. The Delphi for .NET Preview Update fixes this problem.

Note also that the Interop Assembly must be accessible to the .NET application when running. This means you can place a copy of it in the application directory or install it in the GAC, however it must be strong named to do the latter. TlbImp.exe has a /keyfile option to allow a strong name key file to be specified for this purpose.

Late Binding

You can also perform late binding using the .NET reflection APIs, which do not require the Interop Assembly to be present. Late binding is supported on COM objects implementing IDispatch (i.e. Automation objects) and operates by calling IDispatch.Invoke. For simple pass-by-value parameters, things are quite straightforward: you set up a System.Type reflection object to represent a class that maps onto the ProgID, ask the Activator object to instantiate the referenced class, then access the members through the Type object's InvokeMember method. Arguments are passed as objects in arrays and the result (if any) is returned in an object.


uses
  System.Windows.Forms, System.Reflection;
...
procedure TfrmRCW.btnLateBoundClick(Sender: TObject;
  Args: EventArgs);
var
  T: System.Type;
  ComObj: TObject;
  OneArg: array[0..0] of TObject;
const
  Val = 45;
begin
  T := System.Type.GetTypeFromProgID('ComServer.ComObject');
  ComObj := Activator.CreateInstance(T);
  //Set up a call to a method with a value parameter
  OneArg[0] := 'Hello world';
  T.InvokeMember('One', BindingFlags.InvokeMethod, nil, ComObj, OneArg);
end;

Passing a parameter by reference is more tedious. You must call an overloaded version of InvokeMember, passing an array containing a single ParameterModifier object. This object's constructor takes a parameter specifying how many arguments the appropriate Automation member takes. This causes it to allocate an internal Boolean array with that many elements, which is exposed by the default array property, Item (meaning you can omit it, if you desire). Before invoking the member you must loop across each argument, specifying whether it is to be passed by reference or value by assigning True or False, respectively, to the corresponding Item array element.


uses
  System.Windows.Forms, System.Reflection;
...
procedure TfrmRCW.btnLateBoundClick(Sender: TObject;
  Args: EventArgs);
var
  T: System.Type;
  ComObj, Result: TObject;
  TwoArgs: array[0..1] of TObject;
  ParamModifiers: array[0..0] of ParameterModifier;
  Answer: Int32;
const
  Val = 45;
begin
  T := System.Type.GetTypeFromProgID('ComServer.ComObject');
  ComObj := Activator.CreateInstance(T);
  //Set up a call to a method with a value parameter and a reference parameter
  TwoArgs[0] := Int32(Val);
  Answer := Int32(0);
  TwoArgs[1] := Answer; //pass by reference
  ParamModifiers[0] := ParameterModifier.Create(2);
  ParamModifiers[0][0] := False; //1st arg is by value
  ParamModifiers[0][1] := True;  //2nd arg is by reference
  Result := T.InvokeMember('Two', BindingFlags.InvokeMethod, nil, ComObj,
    TwoArgs, ParamModifiers, nil, nil);
  Answer := Int32(TwoArgs[1]);
  if Result.ToString = System.Boolean.TrueString then
    MessageBox.Show(Format('Twice %d is %d', [Val, Answer]));
end;

Example: Using SAPI Automation Objects

To show a more realistic example we'll now generate an Interop Assembly for the Microsoft Speech API (SAPI). This requires us to run TlbImp.exe across the SAPI server, sapi.dll, located by default in C:\Program Files\Common Files\Microsoft Shared\Speech.

Note that whilst the DLL is called SAPI.dll, the library name in the type library is called SpeechLib. If we follow the naming convention, this means the Interop Assembly is called Interop.SpeechLib.dll.


TlbImp "C:\Program Files\Common Files\Microsoft Shared\Speech\sapi.dll" /out:Interop.SpeechLib.dll

Note that this particular import process generates a lot of warnings due to issues the importer doesn't know how to resolve on its own. Fortunately for a simple example, these issues will not affect us. However, for more complex use of COM objects that generate these warnings, some manual intervention may be required to teach the importer how to marshal the offending parameters.


TlbImp warning: At least one of the arguments for 'SpNotifyTranslator.InitCallback' can not be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp warning: The type library importer could not convert the signature for the member 'SPPHRASE.pProperties'.

With the Interop Assembly generated, the -LU compiler switch should let us compile managed SAPI applications without further trouble, such as:


uses
  Interop.SpeechLib;
...
procedure TfrmSAPIRCW.btnSpeakClick(Sender: TObject;
  Args: EventArgs);
var
  Voice: SpVoice;
begin
  Voice := SpVoiceClass.Create;
  Voice.Speak('Hello world', SpeechVoiceSpeakFlags.SVSFDefault);
end;

This can be compiled with:


dccil -LUInterop.SpeechLib dotNetSpeechApp1.dpr

Hooking COM Server Events

Setting up event handlers for COM object events is much the same as for any other object. You just need to know the types involved. Let's look briefly at a button Click event handler first. The delegate type of this event is EventHandler, defined as:


type
  EventHandler = procedure (Sender: TObject; Args: EventArgs) of object;

So you set up an event handler by declaring a method:


procedure Button1Click(Sender: TObject; Args: EventArgs);

and then assign it to the event like this:


Button1.Add_Click(Button1Click);

Sometimes in the Delphi for .NET Preview of the compiler this abbreviated form of event handler assignment fails with an internal error and so you have to resort to doing it longhand:


Button1.Add_Click(EventHandler.Create(Self, NativeInt(@TForm1.Button1Click)));

However, in future builds, you will be able to write:


Button1.Click := Button1Click;

To set up event handlers for COM objects, you must know the event interface and the expected parameter list (and how that translates into .NET). In the case of the SpVoice Automation object, according to the type library, the events interface is called _ISpeechVoiceEvents and it contains definitions of ten different events.

We will write event handlers for three of these, defined in the type library editor in Pascal syntax like this:


procedure EndStream(StreamNumber: Integer; StreamPosition: OleVariant);
procedure AudioLevel(StreamNumber: Integer; StreamPosition: OleVariant; AudioLevel: Integer);
procedure Word(StreamNumber: Integer; StreamPosition: OleVariant; CharacterPosition: Integer; Length: Integer);

The methods that we need to create in .NET are translations of these and look like this (clearly the names are arbitrary; it's the parameter list that is important):


procedure VoiceEndStream(StreamNumber: Integer; StreamPosition: TObject);
procedure VoiceAudioLevel(StreamNumber: Integer; StreamPosition: TObject; AudioLevel: Integer);
procedure VoiceWord(StreamNumber: Integer; StreamPosition: TObject; CharacterPosition, Length: Integer);

When the type library importer manufactures the Interop Assembly it defines event delegate types with names in the form: SourceInterfaceName_MethodNameEventHandler so the delegate types we need to work with are:

_ISpeechVoiceEvents_EndStreamEventHandler

_ISpeechVoiceEvents_AudioLevelEventHandler

_ISpeechVoiceEvents_WordEventHandler

If all internal compiler issues were resolved we could ignore these delegate type names and simply write:


constructor TfrmSAPIRCW.Create;
begin
  ...
  Voice := SpVoiceClass.Create;
  Voice.EventInterests := SpeechVoiceEvents.AllEvents;
  Voice.Add_Word(VoiceWord);
  Voice.Add_EndStream(VoiceEndStream);
  Voice.Add_AudioLevel(VoiceAudioLevel);
end;

However, at this stage of development this sort of code causes an internal error when used against Automation object events:


dotNetSpeechApp2.dpr(88) Fatal: Internal error: ILCG1474

This means we are forced to use the longhand form:


constructor TfrmSAPIRCW.Create;
begin
  ...
  Voice := SpVoiceClass.Create;
  Voice.EventInterests := SpeechVoiceEvents.AllEvents;
  Voice.Add_Word(_ISpeechVoiceEvents_WordEventHandler.Create(Self, NativeInt(@TfrmPInvoke.VoiceWord)));
  Voice.Add_EndStream(_ISpeechVoiceEvents_EndStreamEventHandler.Create(Self, NativeInt(@TfrmPInvoke.VoiceEndStream)));
  Voice.Add_AudioLevel(_ISpeechVoiceEvents_AudioLevelEventHandler.Create(Self, NativeInt(@TfrmPInvoke.VoiceAudioLevel)));
end;

We can now use these event handlers to write a simplistic animated text reader. The text to read can come from a text box and the Word event handler can highlight each word in the text box (memWords) as it is spoken. The EndStream event handler can remove any remaining highlight and the AudioLevel event handler can control a progress bar (pbVUMeter) being used as a VU meter for the speech:


procedure TfrmSAPIRCW.VoiceWord(StreamNumber: Integer;
  StreamPosition: TObject; CharacterPosition, Length: Integer);
begin
  memWords.SelectionStart := CharacterPosition;
  memWords.SelectionLength := Length; //highlight word
end;

procedure TfrmSAPIRCW.VoiceEndStream(StreamNumber: Integer; StreamPosition: TObject);
begin
  //Reset VU meter
  pbVUMeter.Value := 0;
  //Highlight word being spoken in the text box
  memWords.SelectionLength := 0;
  memWords.SelectionStart := Length(memWords.Text);
end;

procedure TfrmSAPIRCW.VoiceAudioLevel(StreamNumber: Integer; StreamPosition: TObject; AudioLevel: Integer);
begin
  pbVUMeter.Value := AudioLevel;
end;

This works well other than the AudioLevel event which doesn't fire, despite the EventInterests property being set correctly. I haven't identified the problem here; the event fires fine in a Win32 application.

Win32 COM Clients Using .NET Objects (CCW)

You may start writing new .NET objects (or porting Win32 COM objects over to .NET) and need to access these new objects from your existing Win32 COM client applications. In order for COM client applications to use a .NET object, wrapper objects called COM-Callable Wrappers (CCW objects) need to be generated. These wrapper objects cater for the difference in lifetime management between COM and .NET. CCW objects are COM objects that reconcile the reference counting of COM against the garbage collection of .NET as well as dealing with the marshaling of parameters and return types for the .NET object methods.

Registering A .NET Assembly for COM

CCW objects are manufactured at runtime by the CLR via class factories that are created when the .NET assembly is accessed by a COM client. This requires the assembly to be registered as a normal COM server would be.

The assembly registration can be performed from a utility, Regasm.exe, that is supplied with the .NET Framework (and the SDK). You can also do it under program control using the RegistrationServices class in the System.Runtime.InteropServices namespace, although use of the utility program is much more common.

To see the process, let's consider a .NET assembly, called dotNetAssembly.dll, that contains a class DotNetObject:


type
  DotNetObject = class(TObject)
  public
    constructor Create;
    procedure One(const Msg: String);
    function Two(Input: Integer; var Output: Integer): Boolean;
  end;

To register the assembly, the RegAsm.exe command can be used:

C:\Temp>Regasm dotNetAssembly.dll
Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001.  All rights reserved.

Types registered successfully

and for each class found in the assembly adds these entries to the registry:


HKCR\ProgID\(Default)="NamespaceQualifiedClassName"
HKCR\ProgID\CLSID\(Default)="{CLSID}"
HKCR\CLSID\{CLSID}\(Default)="NamespaceQualifiedClassName"
HKCR\CLSID\{CLSID}\InprocServer32\(Default)="WindowsSystemDirectory\mscoree.dll"
HKCR\CLSID\{CLSID}\InprocServer32\ThreadingModel="Both"
HKCR\CLSID\{CLSID}\InprocServer32\Class="NamespaceQualifiedClassName"
HKCR\CLSID\{CLSID}\InprocServer32\Assembly="FullAssemblyName"
HKCR\CLSID\{CLSID}\InprocServer32\RuntimeVersion="Version"
HKCR\CLSID\{CLSID}\ProgId\(Default)="ProgID"

Where:

Note that the assembly must be placed appropriately for the CLR to find it. This means you should install it in the GAC or place it in the application directory. If you wish to leave the assembly elsewhere (during development) you can do this as long as you specify the /codebase option when invoking regasm.exe. This option causes an additional entry to be added to the registry for each registered coclass specifying the assembly location using a fully qualified file name.

We can now proceed to use late binding against the .NET object (we'll see an example later), but clearly it is more typical for COM applications to use early binding. Early binding to a .NET object can be achieved by generating a type library for the .NET object, which is registered along with the assembly.

Interop Type Libraries

CCW objects (or exported classes) can be described in an Interop Type Library (a type library manufactured to contain COM type definitions that match the .NET metadata type definitions). You use a type library exporter to scan the .NET assembly and generate an Interop Type Library (also referred to as an exported type library).

The type library exporter can be invoked from a utility, Tlbexp.exe, that is supplied with the .NET Framework SDK, however this utility simply generates a type library and does nothing with it. It can also be invoked through the Regasm.exe utility, already described. Finally you can also do it under program control using the TypeLibConverter class in the System.Runtime.InteropServices namespace, although use of the Regasm.exe utility program is much more common.

Creating An Interop Type Library

To generate an Interop Assembly use RegAsm.exe like this:


C:\Temp>regasm dotNetAssembly.dll /tlb
Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001.  All rights reserved.

Types registered successfully
Assembly exported to 'C:\Temp\dotNetAssembly.tlb', and the type library was registered successfully

The /verbose command-line switch tells you a little more about the classes that have been described in the Interop Type Library (it offers no additional information when simply registering an assembly on its own) and any referenced assemblies that require a type library to be generated, so:


C:\Temp>regasm dotNetAssembly.dll /tlb /verbose
Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001.  All rights reserved.

Types registered successfully
Type Text exported.
Type @TClass exported.
Type ITextDeviceFactory exported.
Type DotNetObject exported.
Type @MetaText exported.
Type @MetaDotNetObject exported.
Type @FinalizeHandler exported.
Type TTextLineBreakStyle exported.
Type Unit exported.
Type Unit exported.
Assembly exported to 'C:\Temp\dotNetAssembly.tlb', and the type library was registered successfully

Notice that in the case of a Delphi for .NET Preview assembly, there are a whole variety of types found that are generated by the compiler that are not useful to the COM client. You can see many of these in the regasm.exe output above.

For each type found in the assembly, the following entries are added to the registry:


HKCR\ProgID\(Default)="NamespaceQualifiedClassName"
HKCR\ProgID\CLSID\(Default)="{CLSID}"
HKCR\CLSID\{CLSID}\(Default)="NamespaceQualifiedClassName"
HKCR\CLSID\{CLSID}\InprocServer32\(Default)="WindowsSystemDirectory\mscoree.dll"
HKCR\CLSID\{CLSID}\InprocServer32\ThreadingModel="Both"
HKCR\CLSID\{CLSID}\InprocServer32\Class="NamespaceQualifiedClassName"
HKCR\CLSID\{CLSID}\InprocServer32\Assembly="FullAssemblyName"
HKCR\CLSID\{CLSID}\InprocServer32\RuntimeVersion="Version"
HKCR\CLSID\{CLSID}\ProgId\(Default)="ProgID"
HKCR\CLSID\{CLSID}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}

It also registers the type library under HKCR\Typelib\{LIBID} and each exposed interface under HKCR\Interface\{IID}.

What's In An Interop Type Library?

The type library exporter exposes items that are deemed appropriate to be made available to COM. Note that the ComVisibleAttribute can be used to hide things from the COM world, such as record types and interfaces:


[ComVisible(False)]
Definition of something not to be exposed through the Interop Type Library

This is used for various reasons. For example, if you are creating a .NET object that implements an existing COM interface, you would define the interface in the assembly to mirror the existing COM definition. However there would be no point in making this new definition of the interface available so it would be marked as hidden.

In addition, ComImportAttribute can be used specifically to hide interfaces, instead of ComVisibleAttribute, e.g.


[ComImport]
Definition of interface not to be exposed through the Interop Type Library

These attributes are used on many of the standard .NET Fx assemblies meaning that, for example, you cannot create a .NET form from a Win32 COM application - the Form class in the System.Windows.Forms namespace is not exposed. Indeed this assembly only exposes a small portion of its wares, and other assemblies hide everything from direct use through COM.

.NET Interfaces

When a .NET class is exposed to a COM client the ClassInterfaceAttribute (from the System.Runtime.InteropServices namespace) controls what interface (if any) will automatically be created by the type library export process. This attribute has three values and the default value is not necessarily the best choice (certainly not if you want to do early binding). The sample class above implicitly uses the default value and so we should understand the attribute values to see what choices we have.

Because of the default behaviour of the attribute, if your class does not implement an interface, you might wish to specify the ClassInterfaceType.AutoDual attribute value to ensure you can use early binding against it, however Microsoft advise against this option as it may cause versioning problems for the COM clients if the .NET classes get modified:


type
  //Generate a class interface that support early and late binding
  [ClassInterface(ClassInterfaceType.AutoDual)]
  DotNetObject = class(TObject)
  public
    constructor Create;
    procedure One(const Msg: String);
    function Two(Input: Integer; var Output: Integer): Boolean;
  end;

Before we move on it should be made clear that defining an interface in .NET is an option you should consider carefully. If you want to define an interface for your class to implement you are at liberty to do so (and indeed there are benefits from doing so, not least of which is the control you get over multiple versions of the class). For example we could have an interface, IDotNetInterface, which defines the behaviour that will be made available through the DotNetObject class:


type
  IDotNetInterface = interface(IInterface)
  ['{3C32D881-43DA-40D2-A7F6-0AE830C2920F}']
    procedure One(const Msg: String);
    function Two(Input: Integer; var Output: Integer): Boolean;
  end;

  //Don't generate a class interface, since we already have a real interface
  [ClassInterface(ClassInterfaceType.None)]
  DotNetObject = class(TObject, IDotNetInterface)
  public
    constructor Create;
    procedure One(const Msg: String);
    function Two(Input: Integer; var Output: Integer): Boolean;
  end;

Notice that in this case the ClassInterfaceAttribute (from the System.Runtime.InteropServices namespace) has been used to tell the type library exporter not to create an interface that represents the class functionality; this is no longer necessary since we have a real interface designed to do just that.

Another noteworthy point is that you can specify your own IID for the interface, to be used when exposed to COM clients. The IID can be specified either with traditional Delphi syntax:


IDotNetInterface = interface(IInterface)
['{3C32D881-43DA-40D2-A7F6-0AE830C2920F}']
...
end;

or with the GuidAttribute class:


[Guid('3C32D881-43DA-40D2-A7F6-0AE830C2920F')]
IDotNetInterface = interface(IInterface)
...
end;

However in the Delphi for .NET Preview Update, the traditional Delphi syntax is ignored, so currently you should use the GUIDAttribute class.

Using An Interop Type Library

Now we have a type library describing the .NET objects (or the CCWs for them) it can be imported into Delphi 7 as normal (Project | Import Type Library...).

Assuming the Generate Component Wrappers checkbox is checked then this dialog will import the type library, generate definitions for the pertinent interfaces and coclasses and also create wrapper components for the coclasses.

The natural process is to press the Install... button, choose a design-time package to install the unit into the IDE through, then let the package be compiled and installed. Unfortunately this doesn't work with Interop Type Libraries generated for Delphi for .NET Preview assemblies. The IDE invokes the type library importer, which then gets stuck on the first type found containing a @ character.

Fortunately we can easily work around this issue by using the Create Unit button instead. This runs the type library importer in a manner that allows it to deal with these types whose names that are invalid identifiers (the @ characters are changed to underscores, as explained in comments in the generated type library import unit). However it does mean that we have to add the unit to a package, and compile and install the package manually, if we want the component wrappers to be installed on the Component Palette. This is not a requirement, as the type library import unit will be added to the project anyway, so you can programmatically connect to the .NET objects without any further steps.

Create Unit generates a type library import unit for the selected type library as well as for any type libraries referenced by it. For an Interop Type Library this will include the Common Language Runtime Library (mscorlib.tlb), the Interop Type Library for the Microsoft Common Language Runtime Class Library (mscorlib.dll).

Early Binding

To use early binding against the CCW you treat it like a normal COM object. This means you can use the CreateComObject routine from the ComObj unit or use the creator object defined in the type library import unit (CoXXXX where XXXX is the coclass name):


procedure TfrmCCW.btnEarlyBoundClick(Sender: TObject);
var
  DotNetObject: IDotNetInterface;
  Output: Integer;
begin
  DotNetObject := CoDotNetObject.Create;
  DotNetObject.One('Hello World!!!');
  if DotNetObject.Two(Input, Output) then
    ShowMessageFmt('The answer to %d is %d', [Input, Output])
end;

Remember that if the .NET class has no real interface defined, then you will only be able to do early binding if the ClassInterfaceAttribute is set to ClassInterface.AutoDual. In this case the code would need to be written slightly differently to cater for the name of the class interface:


procedure TfrmCCW.btnEarlyBoundClick(Sender: TObject);
var
  DotNetObject: _DotNetObject;
  Output: Integer;
begin
  DotNetObject := CoDotNetObject.Create;
  DotNetObject.One('Hello World!!!');
  if DotNetObject.Two(Input, Output) then
    ShowMessageFmt('The answer to %d is %d', [Input, Output])
end;

Late Binding

You can perform Variant-based late bound Automation against a .NET class so long as you register the assembly with regasm.exe. Whether you generate an Interop Type Library, and whether the class has a specific interface is irrelevant.


procedure TfrmCCW.btnLateBoundClick(Sender: TObject);
var
  DotNetObject: Variant;
  Output: Integer;
begin
  DotNetObject := CreateOleObject('dotNetAssembly.DotNetObject');
  DotNetObject.One('Hello world!!!');
  if DotNetObject.Two(Input, Output) then
    ShowMessageFmt('The answer to %d is %d', [Input, Output])
end;

If you have an Interop Type Library you can also use late bound Automation against the dispinterface in the type library for slightly better performance (but not as good as the performance obtained through early binding). If you implement a specific interface the code looks like this:


procedure TfrmCCW.btnMediumBoundClick(Sender: TObject);
var
  DotNetObject: IDotNetInterfaceDisp;
  Output: Integer;
begin
  DotNetObject := CreateOleObject('dotNetAssembly.DotNetObject') as IDotNetInterfaceDisp;
  DotNetObject.One('Hello World!!!');
  if DotNetObject.Two(Input, Output) then
    ShowMessageFmt('The answer to %d is %d', [Input, Output])
end;

If not, and the ClassInterface.AutoDual attribute value was specified, the code looks like this:


procedure TfrmCCW.btnMediumBoundClick(Sender: TObject);
var
  DotNetObject: _DotNetObjectDisp;
  Output: Integer;
begin
  DotNetObject := CreateOleObject('dotNetAssembly.DotNetObject') as _DotNetObjectDisp;
  DotNetObject.One('Hello World!!!');
  if DotNetObject.Two(Input, Output) then
    ShowMessageFmt('The answer to %d is %d', [Input, Output])
end;

Summary

This paper has looked at the COM Interop mechanism that facilitates building Windows systems out of COM and .NET components. This will continue to be a useful technique whilst .NET is still at an early stage of its life and Win32 dominates in terms of existing systems and developer skills. Indeed, due to the nature of legacy code this may continue long after .NET programming dominates the Windows arena.

The coverage of COM Interop has been intended to be complete enough to get you started without having too many unanswered questions. However it is inevitable in a paper of this size that much information has been omitted. The references below should provide much of the information that could not be fitted into this paper.

References

  1. .NET Interoperability: .NET ↔ Win32 by Brian Long.
    This paper looks at the issues involved in .NET code using routines in Win32 DLLs (including Win32 API routines) using the PInvoke mechanism, and also Win32 applications accessing routines in .NET assemblies using the Inverse PInvoke mechanism.
  2. .NET and COM, The Complete Interoperability Guide by Adam Nathan (of Microsoft), SAMS.
    This covers everything you will need to know about interoperability between .NET and COM, plus lots more you won't ever need.
  3. Inside Microsoft .NET IL Assembler by Serge Lidin (of Microsoft), Microsoft Press.
    This book describes the CIL (Common Intermediate Language) in detail and is the only text I've seen that shows how to export .NET assembly methods for Win32 clients. The author was responsible for developing the IL Disassembler and Assembler and various other aspects of the .NET Framework.
  4. Troubleshooting .NET Interoperability by Bob Burns (of Microsoft), Microsoft Developer Network.
    This article looks at correcting common problem with all forms of .NET Interoperability, including correcting common problems in Interop Assemblies. This section is mainly borrowed from information in a similar page in the .NET Framework SDK.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.