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. .NETCOM 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. AssembliesCOM 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 InheritanceUnmanaged (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. NewThe 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 IdentityUniquely 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 HandlingAll 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 LifetimesCOM 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. ReflectionCOM 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 DoesGiven 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:
Let's see how all of this comes together to produce a usable application. Using COM from Visual Basic .NETTo 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 RCWAs 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:
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 .
To create a primary interop assembly, you must take these steps:
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 RCWsTlbimp.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:
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 RCWUsing 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 countingIn 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 COMThe 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.
.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 ClassVisual 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.
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 ModelsCOM 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 ConcernsBy 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 |