.NET
Interoperability: COM Interop
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.
.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
(Win32) to Borland Delphi 8 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, using C# syntax mostly, 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 marshalling 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.
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.
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. However it
is most common to have your IDE do the work for you, saving you from having to
worry about the details.
To see the
process that’s involved, let's make a simple example that uses the Microsoft
Speech API 5.x (SAPI 5.x). Note that SAPI 4.0 is more widespread, but is
completely incompatible with 5.x. You can download the full SDK for SAPI 5.1
from http://www.microsoft.com/speech/download/sdk51
but Windows XP has version 5.0 installed by default anyway, which should
suffice. SAPI 5.x is implemented in an in-proc COM server called sapi.dll
located in C:\Program Files\Common Files\Microsoft Shared\Speech. It has a type
library bound into it as a custom resource, in the same way that Delphi COM
servers do. To learn about programming the Microsoft Speech API, see Reference 5 and Reference 6.
To generate an
Interop Assembly for the SAPI 5.x COM server you simply need to add a reference
to its type library into your project references, in much the same way as
adding a reference to a regular .NET assembly; by right-clicking on the References node in the Project
Manager and choosing Add Reference, or by selecting Project | Add
Reference...
In the Add Reference dialog the .NET
Assemblies tab is selected by default but the COM Imports tab allows you to locate type libraries.
All registered
type libraries will be displayed, listed by the help strings (the Delphi 7 Type
Library Editor allows you to set this for Delphi COM servers). If your COM
server has not been registered, thereby meaning its type library has not been
registered, you can still find it with the Browse... button.
Once a type
library has been added, the References node of the Project Manager reflects this by
listing the generated Interop Assembly.
Notice the
name of the Interop Assembly follows a convention used by the popular IDEs.
Whilst it is
very convenient to have the IDE create your Interop Assembly, you can opt to
create them yourself if you wish. This allows you the luxury of choosing your
own arbitrary name for it. To generate an Interop Assembly use the TlbImp.exe
utility like this:
|
Note: the type library importer makes
certain assumptions about the use of parameters and sometimes will use
parameter types that are not the most appropriate. The warnings in the output
above suggest that in this case it could not even decide how to deal with the
marshaling requirements in cases. 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.
Note: in order to call
utilities supplied with the .NET Framework SDK you must have the appropriate
paths set up. This is not done on a global scale by the installation process,
but a handy batch file is supplied that sets it up for you. The batch file is
called sdkvars.bat and by default is located in C:\Program
Files\Microsoft.NET\SDK\v1.1\Bin. To make it easy to invoke this, you can add a
new shortcut to a Start Menu group, or to your desktop and set the command to
be:
cmd /K
"C:\Program Files\Microsoft.NET\SDK\v1.1\bin\SDKVars.bat"
This will
launch a command prompt, run the batch file, and leave the command prompt open
(as opposed to closing it, which it would unhelpfully do by default).
Now that we
have an Interop Assembly we can use it. Here is the most trivial SAPI
application we can test out, which makes use of the COM object that implements
the SpVoice interface.
|
This is enough
to have the computer speak to you in its default voice. You can take it
slightly further by adding a text box onto the form and passing its Text property to the Speak method instead. The
screenshot below shows some text that contains XML tags, which are used to
alter attributes of the spoken text.
Note: 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. When generating an Interop Assembly
using TlbImp.exe, you can use the /keyfile option to specify a strong name
key file for this purpose.
When
If you select
a referenced assembly in the Project Manager, various properties will be
displayed in the Object Inspector. One option is Copy Local. For an assembly
registered in the GAC this property will be False as the assembly will be found
at runtime. However for Interop Assemblies created by the IDE this property is
set to True. When you run the application any referenced assemblies with Copy
Local set to True will be copied into the application’s output directory (wherever
the .EXE is set to be created). If the assembly already exists in that
directory it will be overwritten to ensure you have the most up-to-date
version.
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 objects. The
example above took this approach. As long as you know the namespace in the
Interop Assembly and the types you need to define, you should find the Code
Completion will answer questions about which methods can be called and which
parameters need to be passed.
The basic rule
is that for an exposed coclass Foo you must create an instance of the RCW class FooClass. Interfaces carry
through into the Interop Assembly with their names unchanged. However you
should know that an Interop Assembly will automatically create another
interface for each original coclass Foo called Foo. This interface will support the coclass’s default
interface as well as a wrapper for the default source interface (events
interface). In the case of the original SAPI coclass SpVoice, its default interface
is ISpeechVoice and its events
interface is _ISpeechVoiceEvents. In the Interop
Assembly the coclass is represented by the RCW SpVoiceClass but it will return a SpVoice interface, where SpVoice is a combination of ISpeechVoice and _ISpeechVoiceEvents_Event. The latter interface
is a helper interface to make it easy to set up event handlers with .NET code
as we shall see later.
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 (or &Type) reflection object to
represent a class that maps onto the ProgID, ask the Activator object to instantiate
the referenced class, then access the methods through the Type object's InvokeMember method. Arguments are
passed as objects in arrays and the result (if any) is returned in an object.
|
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 method 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.
The Speech API
doesn’t make much use of reference parameters, but I found an example method
that does. SAPI can display certain dialog boxes (or occasionally windows) if
the underlying voice or recognition engines support them. These dialogs are
identified by well known strings that can be found in the SAPI 5.1 SDK
documentation. To invoke a dialog you should check whether it is supported
using the IsUISupported method. If this
reports back positively then you can display it with DisplayUI. This pair of methods
takes various value parameters but also take a reference parameter which in
Win32 is a Variant but becomes an Object in .NET.
The idea is
that some dialogs make require additional information to be passed in and can
also pass information back, so the parameter is a reference parameter. As it
happens, none of the SAPI dialogs make use of this information, but a value still
needs to be passed. Indeed this parameter is optional under Win32 Automation,
and so EmptyParam would do just fine in
To see how
reference parameters are dealt with, consider this early bound code that
invokes the volume control window (ultimately the same as manually running the
sndvol32.exe Windows accessory).
|
This uses both
methods, so uses two reference parameters in total. To rework this as late
bound code requires it to be rewritten like this:
|
Setting up
event handlers for COM object events is much the same as for any other object,
as the Interop Assembly defines delegate types for all event methods, and
implements helpful add and remove routines in the events interface wrapper. In
the Speech API case mentioned above the real
events interface is called _ISpeechVoiceEvents but the Interop Assembly defines
a wrapper interface called _ISpeechVoiceEvents_Event.
All you need
to know in order to write event handlers for COM events you need to know about
the delegate types present in the Interop Assembly. Once you have the
information it is simply a case of implementing a compatible routine and using Include to add it to the
appropriate event. Using Include adds your handler to the potential list of other
handlers; .NET events, as implemented by delegates, support event handler
multiplexing. The add/remove routines in the generated wrapper interface are
used to insert/remove your handler in/from the list when using Include/Exclude.
Let’s look at
an example where we need to implement event handlers for the following events: Word (triggered as each
separate word is about to be spoken), EndStream (triggered when the speech stream
has been fully processed) and AudioLevel (triggered as the voice amplitude changes). To
find the event handler signatures we need to examine the delegate types in the
Interop Assembly. Remember that what looks like a regular procedural type (or
event handler type) definition in
We can use the
.NET Framework SDK IL Disassembler utility (ildasm.exe) to examine the delegate
types (see note above regarding using the SDK
tools. In the original type library the events interface was called _ISpeechVoiceEvents and the pertinent methods are called Word,
EndStream and AudioLevel. When the type library
importer manufactures the Interop Assembly it creates delegate types with names
in the form: SourceInterfaceName_MethodNameEventHandler so the specific delegate types we need will therefore be called _ISpeechVoiceEvents_WordEventHandler, _ISpeechVoiceEvents_EndStreamEventHandler and _ISpeechVoiceEvents_AudioLevelEventHandler. If you locate one of these delegates in ILDasm you can see the
class inheritance and the presence of the custom Invoke method.
Double-clicking the method shows more detail on its makeup,
showing the parameter names (which admittedly are irrelevant; it’s the types
that are crucial).
From this we could reconstruct a suitable
type
_ISpeechVoiceEvents_AudioLevelEventHandler
= procedure(StreamNumber: Integer; StreamPosition: &Object; AudioLevel: Integer);
However there is a certain amount of manual translation required
here. It can be simpler to use the popular Reflector tool from Lutz Roeder,
available free from http://www.aisto.com/roeder/dotnet.
This tool can decompile IL code in assemblies to C#, Visual Basic.NET and also
The
[ComVisible(false), TypeLibType(16)] public _ISpeechRecoContextEvents_EndStreamEventHandler = procedure Invoke( [In] StreamNumber: Integer; [In, MarshalAs(UnmanagedType.Struct)] StreamPosition: TObject; [In] StreamReleased: boolean) |
The event handler
methods that we need will look like this:
|
In order to have the
events trigger we need to assign a value to the voice object’s EventInterests parameter. The catch-all
value of SpeechVoiceEvents.SVEAllEvents causes all events to
fire, but the SpeechVoiceEvents enumerated type acts
as a bitmask type, so you can add them together (or combine them with the or
operator), with appropriate casting:
|
We can use these event
handlers to write a simplistic animated text reader. The text to read comes
from a text box and the Word event handler highlights each word in the text box (speechTextBox) as it is spoken. The EndStream event handler can
remove any remaining highlight and the AudioLevel event handler can
control a progress bar (audioProgressBar) being used as a VU meter for the
speech:
|
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 a COM client application 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.
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:
|
To register the
assembly, the RegAsm.exe command can be used:
|
and for each class
found in the assembly adds these entries to the registry:
|
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.
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.
To generate an Interop
Assembly use RegAsm.exe like this:
|
Note: As you can see the Borland RTL code contains some items
that worry the type library exporter, but we’ll address that soon.
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:
|
Note: in the case of a
For each type found in
the assembly, the following entries are added to the registry:
|
It also registers the
type library under HKCR\Typelib\{LIBID} and each exposed interface under HKCR\Interface\{IID}.
The type library
exporter exposes items that are deemed appropriate to be made available to COM,
namely all visible classes, records and interfaces. You can use ComVisibleAttribute to hide things from
the COM world, such as record types and interfaces:
|
Quite importantly you can
employ this attribute on an assembly-wide basis to hide everything from COM by
default, then specify the items you wish to be made visible to COM clients.
This approach works well with Delphi-generated assemblies:
|
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.
Applying it to our sample assembly yields this more apt verbose output:
|
In addition, ComImportAttribute can be used
specifically to hide interfaces, instead of ComVisibleAttribute, e.g.
|
These attributes are
used on many of the standard .NET Framework 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.
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 advises against this option as
it may cause versioning problems for the COM clients if the .NET classes get
modified:
|
Before we move on it
should be made clear that defining an interface in .NET is an option you should
consider carefully, indeed it is positively encouraged. 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:
|
Notice that in this
case the ClassInterfaceAttribute has been used to tell the
type library exporter not to create an interface that represents the class
functionality; the auto-generated interface is no longer necessary since we now
have a real interface designed to do just that.
When regasm.exe is run
against the new assembly it will only expose IDotNetInterface:
|
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
|
or with the GuidAttribute class:
|
However in Delphi 8 for
.NET the traditional
Now we have a type
library describing the .NET objects (or the CCWs for them) it can be imported
into
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. 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. You can now either use the component
wrappers on the Component Palette, or programmatically refer to the items
exposed through the type library.
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):
|
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:
|
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.
|
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:
|
If not, and the ClassInterface.AutoDual attribute value was
specified, the code looks like this:
|
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.
Brian Long used
to work at Borland
Besides authoring a Borland Pascal problem-solving book published in 1994 and
contributing towards a Delphi
for .NET book in 2004, 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 2000
award and was voted Best Speaker at Borland's BorCon
2002 conference in
There are a growing number of conference papers and articles available on Brian's Web site, so feel free to have a browse.