COM and Visual Basic .NET

I l @ ve RuBoard

Working with COM components is almost a given for any major application built today. Admittedly, most, if not all, of the Win32 API and COM objects will eventually be published with managed interfaces. But even then, there will always be some need to work directly with COM components. Legacy COM components can be expected to live for many years before they're upgraded or replaced .

What Is COM Interop?

Some people get confused when they discuss COM interop. We're sorry to say that some of the more reputable technical publications don't always do much better. The confusion often seems to surround the difference between calling native methods from Visual Basic .NET and calling COM objects. Part of the problem might be that an implicit assumption that any calls that cross the native/managed barrier must be COM interop. This is simply not the case. COM interop implies a whole nasty set of requirements and possibly thread marshaling overhead that is simply not necessary when you call a native Win32 method. To keep it simple, think of it this way. If you need to import a type library or create a COM reference in your project, it's COM interop. If you use the Declare statement or the DllImport attribute, on the other hand, you're just calling native methods ”no COM interop involved.

COM vs. .NET

COM differs from the .NET Framework object model in several fundamental ways. First and foremost, COM is based on binary standard. The internal binary layout of classes must comply with COM rules. In contrast, .NET is based on a type standard. Its common type system establishes a framework that enables cross-language integration, type safety, and high-performance code execution. We'll look at each of the other major differences in turn .

Type Libraries vs. Assemblies

COM uses type libraries to store type information. A type library contains only the public types the designers wanted to be made available. Moreover, in COM a type library is optional. In the managed world, type information is stored as metadata and is mandatory for all types public, private or otherwise . This metadata is embedded inside assemblies to ensure that the type information is always present.

Interfaces and Inheritance

Unmanaged (COM) objects always communicate through interfaces. Period. COM requires the implementation of the IUnknown interface for all objects. This interface supports the QueryInterface method, which retrieves pointers to interfaces implemented by an object. Managed objects and classes can pass classes and interfaces around indiscriminately. You can get an interface supported by a managed object simply by casting to the desired type.

.NET also allows both implementation and interface inheritance. It also supports cross-language inheritance ”classes can inherit from other classes regardless of what .NET language the base class was written in. COM allows interface inheritance only and does not support implementation inheritance.

New vs. New

The Visual Basic .NET New operator is used to create instances of new classes. You can, however, use the CreateObject method to specifically create new instances of COM objects. COM relies on the CoCreateInstance API or IClassFactory interface to create new instances of COM objects. (That's what Visual Basic 6.0's New operator did.)

Object Identity

Uniquely identifying COM classes requires the definition and use of globally unique identifier (GUID) values (really big numbers ). Each public interface is assigned a GUID, including the COM class itself. Each GUID must be stored in the registry to allow other clients to access the classes and interfaces. Whenever an interface changes, it must be given a new GUID. The CLR, on the other hand, uses fully qualified names or strong names ( names with a digital signature) to uniquely identify types.

Error Handling

All COM methods that can generate an error usually return an HRESULT . This value indicates whether that method call succeeded or failed. It can also indicate various failures or error information. All managed objects implement exceptions using structured exception handling for the vast majority or errors. There are certainly no HRESULT values.

Object Lifetimes

COM objects manage their own object lifetimes via a mechanism called reference counting. This is supported through the IUnknown interface methods AddRef and Release . Whenever a copy of an object's interface is made, a call must be made to AddRef , which increments a simple counter. When a client is done with an interface, it must call Release on the underlying object. When the reference count eventually reaches zero, the object is destroyed. In this way, we describe COM objects as having a deterministic lifetime. As soon as the objects are not in use, they are destroyed .

In .NET, things work a little differently. The CLR manages the lifetime of objects through garbage collection, which is a nondeterministic memory model. Essentially what this means is that when an object is no longer in use, it will be destroyed ”but not necessarily immediately. In fact, when garbage collection occurs is completely up to the garbage collector. This can lead to some interesting and problematic cleanup issues, but more on that later.

IDispatch vs. Reflection

COM Automation is implemented through the IDispatch interface. This is intended to provide a flexible way to access COM objects without your having to know the interface layout in advance. All Visual Basic 6.0 COM components implement the IDispatch interface by default. This mechanism also allows scripting languages such as VBScript to talk to COM classes. Late binding in Visual Basic 6.0 is also implemented using the IDispatch interface.

In contrast, Visual Basic .NET supports late binding through reflection. Reflection is a mechanism by which a client can discover virtually anything about a type. Through reflection, you can inspect fields, properties, methods, interfaces, and inheritance hierarchies of any type. In addition, you can use reflection to inject MSIL code at run time and alter method contents or add new methods. (Emitting MSIL code at run time isn't a common practice, nor is it for the faint of heart, but it sure is powerful.)

What COM Interop Does

Given these huge differences between COM and .NET, you can only imagine the complexities involved in having them coexist in same application. Worry not ”Microsoft made huge investments in COM interop to make this as seamless as possible. So what exactly does COM interop do for you? In a nutshell , it provides the following basic services:

  • Marshaling and type translation

    Handles data type conversion between managed and unmanaged data types.

  • Lifetime management for COM objects

    Managing object references to ensure that objects are either released or eventually marked for garbage collection. You can do your own object cleanup to guarantee a more deterministic release of underlying COM objects.

  • Object identity

    Enforces and maintains COM identity rules.

  • Type binding

    COM interop supports both early bound interface and late-bound (IDispatch) interfaces.

  • Error handling

    The COM interop provides translation for COM HRESULT return values to .NET exceptions. It also supports translating .NET exceptions to HRESULT values for COM to .NET communication.

Let's see how all of this comes together to produce a usable application.

Using COM from Visual Basic .NET

To call a COM component from Visual Basic .NET, you must do a couple of things. First, you must obtain a managed wrapper for the underlying COM type library. In .NET parlance, this wrapper is called a runtime callable wrapper (RCW), or interop assembly . Note that the interop assembly is merely a wrapper for the COM component and does not contain any of the component's functionality. The interop assembly's purpose in life is to make accessing the underlying COM component as seamless as possible. In other words, it does all of the standard marshaling stuff for you. This means that all of the machines your application runs on will still need the COM component to be properly registered before it can be used.

Creating an RCW

As we've stated, to access a COM component through Visual Basic .NET you must have an interop assembly. There are two ways to get this assembly. First, and preferably, the original developer of the component should publish what we call a primary interop assembly (PIA). You can think of this as the definitive, or vendor-approved, interop assembly. If this is not available (which is highly likely at this point), you can create your own interop assembly in one of the following ways:

  • By adding a COM reference to your Visual Basic .NET project.

  • Using the Type Library Importer (Tlbimp.exe) utility (which ships with the .NET Framework SDK).

  • Programmatically generate your own wrapper using the System.Runtime.InteropServices.TypeLibConverter class, which also supports generating primary interop assemblies.

  • Defining types manually in your own assembly.

The most common approaches are the first two. The last option, baking your own assembly, is way beyond the scope of this book; I recommend that you think carefully before going down that path .

Primary Interop Assemblies

You can obtain interop assembly in two ways. The first choice is to use primary interop assembly from the vendor, if available. Primary interop assemblies (PIAs) do not currently exist for most COM components, but expect more vendors to provide them in the future. Visual Studio .NET provides primary interop assemblies for some of the standard COM components and are installed by default in <Drive> :\Program Files\Microsoft.NET\Primary Interop Assemblies. Of course, a PIA need not be placed in a specific location, and you certainly do not need to store it along with all the other included PIAs.

To create a primary interop assembly, you must take these steps:

  1. Create a public/private key pair with using sn.exe tool (for example, sn.exe “k keys.snk )

  2. Run Tlbimp.exe against the COM DLL or type library, specifying the /primary and / keyfile : [filename] options.

We start by creating a public/private key pair because this is a requirement of any primary interop assembly. The key pair is used to generate a signed library for registering in the GAC. This provides a unique identity for the assembly that is absolute. In other words, the runtime cannot possibly confuse your assembly for another assembly that might contain identical namespaces and classes.

At this point, you have a primary interop assembly, and all you need to do is register it. All primary interop assemblies are registered using the Regasm.exe utility. Note that it is never a good idea to generate a primary interop assembly for a component you don't own. Keep an eye on the MSDN Web site ”more and more primary interop assemblies for existing COM components should be published as time goes on.

Tlbimp and RCWs

Tlbimp.exe is a command-line utility for importing COM type libraries. It generates a runtime callable wrapper for your COM type library. This RCW can then be referenced in your Visual Basic .NET project, and the objects contained in the type library can be used as if they were any other .NET objects. You can also use Tlbimp to generate PIAs. (See the earlier sidebar on PIAs.)

Tlbimp is fairly simple to use and works well for the vast majority of cases. It is easily accessible through the Visual Studio .NET command prompt shortcut. From the Start menu, choose All Programs, Microsoft Visual Studio .NET, Visual Studio .NET Tools. All you need to do, in the simplest case, is to point the utility at a COM type library ”either a DLL, if the type library is included, or a TLB file ”and it will generate an RCW.

More Info

For more information on how to use Tlbimp, see the .NET Framework SDK documentation.


For all of Tlbimp's ease of use, it should come as no surprise that it doesn't necessarily do everything that you need. In these cases, you can crack the generated interop assembly and customize the MSIL code. I won't cover that subject here, but I'll mention some limitations so you can at least understand what you'll need to work around. Tlbimp needs a little help in the following situations:

  • Success HRESULT values

    All failure HRESULT values cause exceptions in managed code. However, success HRESULT values other than S_OK (such as S_FALSE ) that are returned from unmanaged code are not reported to managed callers of that code. If you don't need to differentiate between various success HRESULT values, this limitation does not present a problem. If you do need to differentiate between two success HRESULT values (such as S_OK and S_FALSE ), use PreserveSigAttribute .

  • C-style arrays

    Tlbimp cannot generate the proper marshaling attributes because there is no size_of information present in the input. To marshal C-style arrays, you need to do custom marshaling and specify the additional parameter SizeParamIndex to indicate the length of the array.

  • Passing null to ByRef or out parameters

    This subject can get quite involved. Suffice it to say that currently there is no way to pass null to ByRef parameters.

  • Multidimensional arrays

    Type libraries can contain definitions of methods that have variable-length array arguments. In such cases, the size of the array is typically passed as a separate method argument (for example, HRESULT SomeMethod(int size, byte* buffer); ). Because the type library has no information to tie the two arguments together, the runtime cannot marshal the array correctly. To correct the problem, you must do custom marshaling and provide the array size using the MarshalAs attribute.

  • Unions with reference types

    In .NET, structures marked with the explicit layout attribute can't contain reference types. If the type library contains a union with a reference type (such as CHAR* or VARIANT* ), it will be converted to a structure with size and packing information but without any members .

    Caution

    The type library might itself also contain errors or incorrect information, which might become apparent only when it is converted to .NET metadata. In this case, you must correct the original COM type library by modifying the Microsoft Interface Description Language (MIDL) or Object Definition Language (ODL) used to produce it and recompiling it with the MIDL compiler. For more information, search on MIDL and ODL in the Platform SDK.


In any of the above situations, it is a good idea to generate the defaults using TLBIMP, open the MSIL using ILDASM.exe, edit the MSIL to get the desired output, and then use ILASM.exe to regenerate the assembly. This is also an opportune time to remove unused types, set PreserveSig for a success HRESULT , change ref to arrays, and change parameters to IntPtr . But this is really for advanced developers ”you shouldn't attempt this without a decent understanding of MSIL and the ILASM and ILDASM tools.

Using Your RCW

Using an interop assembly in your application is just about as simple as working with COM objects from Visual Basic 6.0. For the most part, the objects are virtually indistinguishable from other .NET classes. Once you have an interop assembly (primary or otherwise), invoking the COM object is pretty simple. You reference the assembly or type library in your project and use the contained types as you would any other managed type.

Garbage collection and reference counting

In most cases, you don't need to worry about when the garbage collector gets around to cleaning up your COM object, but in some situations it is of critical importance. If it is vital that your application release a particular object at a specific time, you can take advantage of a method of the Marshal class (remember it?): Marshal.ReleaseComObject . Marshal.ReleaseComObject essentially forces the release of a COM object's interface. This can bring about the object's destruction. This method, by definition, decrements the RCW's reference count. Much like in COM, when the RCW's reference count hits zero, the object is toast .

That's about all I'll cover about using COM from Visual Basic .NET. It's mostly so simple that there really isn't much to talk about. Things get a little more interesting, however, in the next section, which covers using Visual Basic .NET from COM.

Using Visual Basic .NET from COM

The key to creating a Visual Basic .NET component that is visible to COM clients is to create a COM callable wrapper (CCW). At a very basic level, creating classes in Visual Basic .NET that are visible to COM clients is simple. Every .NET object is potentially a COM-accessible object, provided you take the step of registering the .NET assembly containing your object. If you're using Visual Basic .NET, all you need to do is select the Register For COM Interop option on the project's property page (as shown in Figure 4-2). Alternatively, you can manually run the Regasm.exe utility on your managed assembly.

Figure 4-2. The project settings and registering an application for COM interop.

graphics/f04pn02.jpg

.NET is a paradigm shift from COM, but building .NET objects is probably the easiest way to build COM objects. Every .NET object is potentially a COM object ”all you need to do is create a CCW. The CCW provides a whole host of features. It has a class factory, it has a type library, it implements IUnknown and IDispatch , and it is CoCreatable . However, not all features of the CLR types are exposable to COM through the wrapper. Shared methods and parameterized constructors are not exposed. Overloaded methods are renamed , and the inheritance hierarchy is flattened.

Creating a More Sophisticated COM Class

Visual Basic makes it easy to create more sophisticated classes. The default mechanism described above works, but it isn't easy to control. You have no way to define the GUID associated with your types, and everything defaults to supporting only the IDispatch interface. That might work, but it doesn't address every possible scenario in which you'd want to expose a class or set of classes to COM. At times, you'll need to emulate an existing COM component or interface for the sake of backward compatibility. In this case, you probably want to start with a Visual Basic .NET COM Class file, which is available through the Add New Item dialog box (shown in Figure 4-3).

Figure 4-3. The COM class file is available through the Add New Item dialog box.

graphics/f04pn03.jpg

This COM class file does several things. First, it creates an empty class in a new file. Second, it applies the Visual Basic .NET specific ComClass attribute to that class and provides a set of autogenerated GUIDs. All you need to do is add public methods to the new class. (You can add private members, but they just won't be visible to the COM world). Your class file might look something like this:

 <ComClass(ComClass1.ClassId,ComClass1.InterfaceId,ComClass1.EventsId)>_ PublicClassComClass1 #Region "COMGUIDs" 'TheseGUIDsprovidetheCOMidentityforthisclass 'anditsCOMinterfaces.Ifyouchangethem,existing 'clientswillnolongerbeabletoaccesstheclass. PublicConstClassIdAsString= "3A6B5582-9A72-438F-8B66-C0832FEE6AA3" PublicConstInterfaceIdAsString=_  "42D21B6F-D962-4552-8104-C4291B07E3ED" PublicConstEventsIdAsString= "74DE4086-EB18-4243-9AFD-061CD36A127C" #EndRegion 'AcreatableCOMclassmusthaveaPublicSubNew() 'withnoparameters,otherwise,theclasswillnotbe 'registeredintheCOMregistryandcannotbecreated 'viaCreateObject. PublicSubNew() MyBase.New() EndSub EndClass 

This is a pretty simple way to create your own COM components. But it is also very powerful. You should look into the documentation for the ComClass attribute to find out what else you can do to customize your components.

There are many ways to create COM components from Visual Basic .NET. We've only touched on some of the Visual Basic .NET “specific mechanisms. A plethora of attributes are available in the .NET Framework that you can use to customize marshaling, interfaces, GUIDs, and everything in between.

COM Threading Models

COM supports two threading models: multithreaded apartment (MTA) and single-threaded apartment (STA). Of course, CLR-based languages are free threaded by default ”which creates a potential problem. STA components don't play nicely (or sometimes at all) in a free-threaded environment. Thankfully, a simple solution is available: the MTAThread and STAThread attributes.

The simple rule for COM threading models is that STA components should be used by STA threads but MTA components can be used universally . There are a great many STA threaded components (mostly built by Visual Basic 5.0 and 6.0).

Threading Concerns

By default, .NET components are free-threaded. The Visual Basic .NET compiler, on the other hand, will mark your main program threads as STA threads. The documentation for the STAThread attribute can be misleading. It states the following: "Threading models only pertain to applications that use COM interop. Applying this attribute to an application that does not use COM interop has no effect." This might lead you to ask why the compiler defaults to STA threads for the main application method. It turns out that in the .NET Framework, COM interop is used in many instances. The Windows.Forms namespace is full of examples of this. This should not create a major problem for your applications ”you can always create additional free threads. All you need to be concerned about is whether any spawned threads need to talk to STA components. If so, they must be marked with the STAThread attribute.

I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net