>Introduction
Microsoft realized that the very first version of .NET needed a way to work with the existing Windows technology used to develop applications over the past 8+ years: COM. With that in mind, Microsoft added support in the .NET runtime for interoperating with COM – simply called “COM Interop”. The support goes both ways: .NET code can call COM components and COM code can call .NET components. This article is geared towards VB6 programmers who are familiar with developing COM components and familiar with the concept of an interface. I’ll review some background on COM, explain how VB6 interacts with COM, and then show how to design .NET components to smoothly interact with COM. In a future article, I’ll discuss how to use an existing COM component in .NET applications.
For those die-hard COM experts, there will be some things in this article that are oversimplified, but the concepts, as presented, are the important points to know for those developers supplementing their COM code with .NET components.
A Brief History of COM and VB6
One of the key concepts of COM is that everything is called through an interface. There is no such thing as simply creating a COM object and calling a method. You must use an interface. You’re probably thinking “Wait a minute! I’ve coded hundreds of classes in VB6 and never needed an interface”. That’s right – you didn’t need one because VB6 provided it for you in the background. Whenever you defined a public method on a class, VB6 made that method part of a COM interface and made your class implement the interface.
For example, consider the following VB6 class from an ActiveX DLL project. The class is called “Robot”:
Option Strict
Public Sub MoveForward()
...
End Sub
Public Sub FindCar()
...
End Sub
When you compile this into a DLL, VB6 creates an interface called “_Robot” (note the leading underscore) and a “coclass” called “Robot”, which implements the interface “_Robot” (a “coclass” is the actual COM creatable object). The Microsoft utility OLE View is a tool used to examine the types defined in a COM component. OLE View should be installed under “Start, Programs, Microsoft Visual Studio 6.0, Microsoft Visual Studio 6.0 Tools, OLE View”. Using the ActiveX DLL compiled above, I started up OLE View, selected “View TypeLib…” from the “File” menu and selected the DLL. Here are the important parts of the output:
interface _Robot : IDispatch {
...
HRESULT MoveForward();
...
HRESULT FindCar();
};
coclass Robot {
[default] interface _Robot;
};
The first item to point out is that there is an interface called “_Robot”. This is the interface VB6 creates for us. It defines the two methods I added to the class (MoveForward and FindCar). Next, you’ll see a “coclass” called “Robot”. This represents the actual, creatable class. Since everything in COM is called through an interface, the “coclass” lists all interfaces supported by this object. Right now, there is only the “_Robot” interface – and it’s marked with the “default” modifier. There are a lot of COM details about the default interface, but for this article, the importance of a default interface in terms of VB6 components is that of all the interfaces a class may implement, only the default interface supports late binding. And since scripting clients like VBScript only do late binding, the methods on the default interface are the only ones they can see.
Multiple Interfaces
COM will allow you to implement more than one interface. Take the ActiveX DLL from the previous section and add a new class module called “IMaid” (VB6 doesn’t support directly creating a COM interface – more on that below). Here’s the IMaid code:
Option Explicit
Public Sub CleanKitchen()
...
End Sub
Public Sub WashCar()
...
End Sub
Now add “Implements IMaid” to the Robot class:
Option Explicit
Implements IMaid
Public Sub MoveForward()
End Sub
Public Sub FindCar()
End Sub
Private Sub IMaid_CleanKitchen()
End Sub
Private Sub IMaid_WashCar()
End Sub
If I compile this DLL now and examine it with OLE View, I see the following structures (again, only the relevant portions are shown below):
interface _Robot : IDispatch {
...
HRESULT MoveForward();
...
HRESULT FindCar();
};
coclass Robot {
[default] interface _Robot;
interface _IMaid;
};
interface _IMaid : IDispatch {
..
HRESULT CleanKitchen();
..
HRESULT WashCar();
};
coclass IMaid {
[default] interface _IMaid;
};
As noted earlier, VB6 does not support creating a true COM interface. However, since VB6 creates a COM interface with every class (prefixed with the underscore), the VB6 compiler will actually use the auto-generated interface when compiling the code when you use the “Implements” keyword on a class in VB6. So above, the Robot coclass implements the interface _Robot (automatically generated by VB6) and the _IMaid interface (the automatically generated one from the IMaid class). Also note that the _Robot interface created by VB6 is marked as the default interface. If you were going to use this object in a scripting environment (such as an ASP page), you could only access the MoveForward and FindCar methods. The methods implemented by the IMaid interface are not accessible since they are not on the default interface.
Almost to .NET!
If you’ve stuck around this long, the answer is yes, you’re getting close to some .NET code! But it’s important to know these concepts before you move into making .NET components that you expose to COM. Think of it as having a blueprint before starting to build a house. Sure, you can work without a blueprint, but the house will come out much nicer if you start off right. A quick recap of what happens in VB6 when it creates COM objects:
- COM is interface based. Everything in COM must be called through an interface.
- VB6 doesn’t require that you implement a specific interface when creating a COM class. Instead, it will create an interface for you. Its name will be defined as your class name prefixed with the underscore (_) character.
- The interface created by VB6 is always marked as the default COM interface and therefore only those methods are available to scripting clients (in the example above, VBScript can not access the IMaid methods of the Robot object).
Where Are My Methods?!
Many people who first expose a .NET object to COM notice that when they try and use the object in VB6, none of their methods are listed. Create a quick .NET class library that will be exposed to COM to see why that happens: [VB.NET]
Option Strict On
Option Explicit On
Namespace QuickNET
Public Class Bee
Public Sub FindFlowers()
End Sub
Public Sub MakeHoney()
End Sub
End Class
End Namespace
[C#]
using System;
namespace QuickNET
{
public class Bee
{
public void FindFlowers()
{
}
public void MakeHoney()
{
}
}
}
Now create a COM type library from this .NET component to see what it looks like (this doesn’t actually register it for COM – it just creates a COM type library). Go to a Visual Studio .NET Command Prompt (Start, Programs, Microsoft Visual Studio .NET, Visual Studio .NET Tools, Microsoft Visual Studio.NET Command Prompt), change to the directory of the above compiled .NET component and enter the following command:
TLBEXP.EXE QuickNET.dll /out:Com.QuickNET.tlb
The TLBEXP.EXE utility generates a COM type library from the .NET assembly. You can name the type library anything you want, but, by convention, it usually has a .tlb extension. I prefix my exported type libraries with “Com.”. Now load up OLE View and open the type library “Com.QuickNET.tlb”. Below are the relevant parts:
coclass Bee {
[default] interface _Bee;
interface _Object;
};
interface _Bee : IDispatch {
};
This looks very similar to VB6 COM type libraries. You can see that an interface called “_Bee” was created for the “Bee” class and it’s also the default interface. However, there are no methods on the interface. If you were to start up VB6 and add a reference to this tlb to your project, you’d notice by looking at the Object Browser that the Bee class has no methods (VB6 always looks at the “default” interface to see what methods are on the class).
NOTE: The _Object interface isn’t important for our discussion. Since everything inherits from the Object class in .NET and that class exposes a _Object interface, there’s a _Object interface added to all exported types.
So why didn’t TLBEXP.EXE put all of the methods on the _Bee interface? Since the layout of a COM interface is a binding contract, adding new methods to a .NET class and then regenerating the COM type library might change that layout – thus breaking existing COM clients compiled against the old layout. By defining an empty interface, all clients will do late-bound calls and new versions of the .NET component (and its COM wrapper) will work without recompiling the COM clients.
Taking Control
So how do you get those methods on your .NET component to show up in VB6? You can take what you know about VB6 and COM and apply it to .NET. Basically, you need to:
- Have an interface with your methods defined.
- Make that interface the default interface.
The first one is easy. Add a .NET interface that includes the methods you want to expose to COM:
[VB.NET]
Public Interface IBee
Sub FindFlower()
Sub MakeHoney()
End Interface
[C#]
public interface IBee
{
void FindFlower();
void MakeHoney();
}
Now you’ll simply implement that interface in your Bee class. [VB.NET]
Public Class Bee
Implements IBee
Public Sub FindFlower() Implements IBee.FindFlower
End Sub
Public Sub MakeHoney() Implements IBee.MakeHoney
End Sub
End Class
[C#]
public class Bee : IBee
{
public void FindFlower()
{
}
public void MakeHoney()
{
}
}
You’ll compile this component, run the TLBEXP.EXE utility and check it out with OLE View. What you see may surprise you:
interface IBee : IDispatch {
...
HRESULT FindFlower();
...
HRESULT MakeHoney();
};
coclass Bee {
[default] interface _Bee;
interface _Object;
interface IBee;
};
interface _Bee : IDispatch {
};
The IBee interface came straight through as a standard COM interface – it even includes the methods. The “Bee” coclass even implements this class (as expected), but there’s still that (empty) _Bee interface that is marked as the default interface. And that’s where VB6 will look for the methods belonging to the class. But the IBee interface is exposed as a standard COM interface so you could do everything through that interface and it would work fine. For example, using the type library created above, this VB6 code is perfectly legal:
Dim bee As IBee
Set bee = New Bee
bee.FindFlower
bee.MakeHoney
To make a cleaner integration, however, the IBee interface needs to be the default interface. That is accomplished through attributes.
Attributes
If you’re not familiar with attributes, here’s a quick summary. Attributes are a “descriptive declaration”. They’re used to “annotate” programming elements such as types, fields, methods, classes, etc… Attributes can have values associated with them and those values, along with the attribute information, are saved with all of the other .NET metadata. They can be used to describe code to the CLR (Common Language Runtime) or to affect application behavior at runtime. To control how TLBEXP.EXE creates the type library, you can use an attribute to prevent it from creating that “default” interface. The “ClassInterfaceAttribute” can be applied to a class with the “ClassInterfaceType.None” enumeration member. It can also be applied at the Assembly level where it would apply to every public class in the Assembly. Using the example above, you can make a small modification to the Bee class and apply the attribute:
[VB.NET]
Option Strict On
Option Explicit On
Imports System.Runtime.InteropServices
Namespace QuickNET
_
Public Class Bee
Implements IBee
Public Sub FindFlower() Implements IBee.FindFlower
End Sub
Public Sub MakeHoney() Implements IBee.MakeHoney
End Sub
End Class
End Namespace
[C#]
using System;
using System.Runtime.InteropServices;
namespace QuickNET
{
[ClassInterface(ClassInterfaceType.None)]
public class Bee : IBee
{
public void FindFlower()
{
}
public void MakeHoney()
{
}
}
}
After compiling this and re-creating the type library, OLE View shows a much cleaner structure:
interface IBee : IDispatch {
...
HRESULT FindFlower();
...
HRESULT FindHoney();
};
coclass Bee {
interface _Object;
[default] interface IBee;
};
Not only do you not have the “_Bee” interface, but your IBee interface is marked as the default interface. Why is that? If the ClassInterfaceAttribute specifies that no automatic interface is to be generated, TLBEXP.EXE will take the first interface implemented by the class and make it the default interface. So if you plan on exposing your .NET class to COM, take into account which interface is the first interface implemented in the source code (either through the VB.NET “Implements” keyword or the first interface listed after the “:” in your C# class definition).
Controlling Your GUIDs
The last thing you need to control is your GUIDs – Globally Unique Identifiers. You’ve probably seen a GUID before. Here’s a sample:
82CC3E6A-148E-4b77-866E-598DBEDC5C74
Every interface in COM and every coclass (creatable class object) is identified by a unique GUID. VB6 controls GUID creation for you. You can “coax” VB6 into using the same GUIDs for classes and interfaces by using the “Binary Compatibility” mode of your VB6 project when recompiling. The .NET TLBEXP.EXE utility will also auto-generate a GUID for every interface and class it exports to COM. But you can use attributes to define the GUID yourself. Why should you care? Whenever you register a .NET object as a COM component, registry entries are created. Some of those registry entries are the GUIDs used to identify your classes and interfaces to COM. If you don’t specify a specific GUID, TLBEXP.EXE will generate a new one every time you re-create your COM type library. COM clients already developed could have references to the old GUID and would no longer work since the GUIDs changed. Likewise, if you do control and define a specific GUID for your classes and interfaces, then the GUIDs aren’t changing and your COM clients won’t need to be recompiled and will continue to work even with new versions of your .NET component.
You can use attributes to define a GUID for your classes and interfaces. Instead of trying to “make up” your own unique GUID, VS .NET (along with previous versions of Visual Studio) comes with a tool for generating a GUID called (interestingly enough) guidgen.exe. This should be found in the “\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools” directory. Double-click on it and you should see a screen similar to the one below:
GUIDs are used in many different places, so guidgen supports creating a GUID in four different formats. For your purposes, you need the fourth format: Registry Format. You don’t need the opening and closing braces, but you can trim those off. Press “New GUID” to generate a new GUID and then “Copy” to copy it to the clipboard.
Now define a GUID for your IBee interface (make sure you’ve added the System.Runtime.InteropServices namespace to your code). Paste the clipboard into the “Guid” attribute and remove the leading and trailing braces:
[VB.NET]
_
Public Interface IBee
Sub FindFlower()
Sub MakeHoney()
End Interface
[C#]
[Guid("0490E147-F2D2-4909-A4B8-3533D2F264D0")]
public interface IBee
{
void FindFlower();
void MakeHoney();
}
Now the class also needs its own GUID. Go back to guidgen, click “New GUID” and then “Copy”. Now apply the Guid attribute to the class with the new GUID value (again, trim the braces): [VB.NET]
Guid("03AD5D2D-2AFD-439f-8713-A4EC0705B4D9")> _
Public Class Bee
Implements IBee
Public Sub FindFlower() Implements IBee.FindFlower
End Sub
Public Sub MakeHoney() Implements IBee.MakeHoney
End Sub
End Class
[C#]
[ClassInterface(ClassInterfaceType.None)]
[Guid("03AD5D2D-2AFD-439f-8713-A4EC0705B4D9")]
public class Bee : IBee
{
public void FindFlower()
{
}
public void MakeHoney()
{
}
}
Now tblexp will always use the same GUID.
Deployment
You’ve been using the TLBEXP.EXE utility throughout this article to generate a COM type library. That’s because, so far, you’ve only been interested in seeing how .NET exposes its structures to COM. To actually make the .NET component look like a COM component, you need to register it just like any other COM component. I’m sure you’re familiar with regsvr32: the utility for registering and unregistering COM components. This utility won’t work for a .NET component. Instead, you’ll need to use the .NET utility “regasm.exe” – short for “Register Assembly”. This utility adds registry entries to make your .NET component “look” like a regular COM component.
The REGASM.EXE utility also has the option to generate a COM type library which can be used by VB6. The “/tlb” option on REGASM performs the same thing that TLBEXP.EXE does. Therefore, you really don’t need to use TLBEXP when creating a .NET object for COM. Just use REGASM with the /tlb option. Here’s an example. Go to the Visual Studio .NET 2003 Command Prompt, change to the directory of your .NET assembly, and enter:
REGASM myassem.dll /tlb:com.myassem.tlb
Your object is now registered as a COM component, and you have a COM type library you can reference from VB6 to early-bind to the component. Now all you need to do is get COM to find the .NET assembly.
Local Deployment or the GAC?
Once your .NET assembly is registered as a COM component, any attempt to create an instance of one of the .NET components from COM will cause a copy of the .NET runtime to be loaded. The .NET runtime will then need to locate the assembly. Even though you’re running in a COM environment, the .NET rules for finding an assembly still apply:
- Tirst, the Global Assembly Cache (GAC) is checked.
- Then, the local directory is checked.
The second option is a bit easier but not as flexible. For the first option, simply copy the .NET assembly to the same directory as your COM exe client and the runtime will find it. However, if you’re building/debugging in the VB6 IDE, then the “local directory” is the location of VB6.EXE. In this situation, you need to copy your .NET assembly to different locations depending on whether you’re running inside the VB6 IDE or not. And copying files into the same directory as VB6.EXE isn’t a great idea. If you make any updates to the .NET assembly, you need to make sure you copy it to both locations. For these reasons, placing the assembly in the Global Assembly Cache (GAC) is usually the best option for COM Interop. Before placing an assembly in the GAC, you need to make sure you’ve got a fixed versioning scheme and a strong-name key pair. Getting a fixed versioning scheme is simple: In your AssemblyInfo file, change the value of the AssemblyVersion attribute from the default “1.0.*” to “1.0.0.0”. The version number is a key component of placing an assembly in the GAC. Leaving the “*” makes VS .NET generate a new value every time you build your project. Since other projects that reference your assembly look for a specific version number, having it change every time you build is a problem! Therefore, hard-code a specific value and as you develop new releases of your assembly, increment the version number as appropriate.
Next, you’ll need to generate a strong-name key pair. Use the SN.EXE tool by going to the Visual Studio .NET 2003 Command Prompt, changing to the directory of your .NET project and enter:
sn -k mykey.snk
Now update the AssemblyKeyFile attribute in your AssemblyInfo to point to the “mykey.snk” file. Finally, recompile your .NET component. It is now ready to be added to the GAC. Go to a Visual Studio .NET 2003 Command prompt, change to the directory of your .NET assembly and enter:
gacutil -I myassembly.dll
This installs your assembly into the GAC. Now, the runtime can locate the assembly no matter which COM client instantiates your .NET component.
Summary of Best Practices
This article has covered a lot of ground. Here’s a review of the best practices for hassle-free COM Interop:
- Define a .NET Interface for the methods you want to expose to COM.
- Assign a GUID to that interface with the “Guid” attribute.
- Have your class implement your interface as the first interface.
- Assign a GUID to that class with the “Guid” attribute.
- Add the “ClassInterface(ClassInterfaceType.None)” attribute to prevent regasm/tlbexp from creating an empty default interface.
- Hard-code a specific version number in your AssemblyVersion attribute.
- Create a strong-name key pair for your assembly and point to it via the AssemblyKeyFile attribute.
- Add your assembly to the GAC,
- Register your assembly for COM by using the REGASM command along with the “/tlb” option to generate a COM type library.
VB6 projects can add a reference to the generated type library to receive the benefits of early binding. Scripting clients like VBScript can also access the object, but they’ll only be able to access the methods on the default interface. When it comes time to enhance your .NET component, don’t touch your existing interface. If you need to add more methods, create a new interface (with a Guid) and implement that interface. Changing the existing interface could break clients already compiled against that interface.