Call VB6 From .NET
The .NET runtime's Interop services let VB6 apps use .NET DLLs—and vice versa.
by Rockford Lhotka

Technology Toolbox
VB.NET, VB6

Within the next few years, most shops will run a lot of VB6 code along with a growing amount of .NET code. You can make these two code bases interoperate as well as possible using the .NET runtime's interoperability services. These let you write Visual Basic .NET (VB.NET) applications that use existing VB6 ActiveX DLLs or other COM DLLs, and they let you write VB6 apps that use DLLs created with VB.NET. You can create .NET applications that leverage your existing investment in COM components, and you can migrate a VB6 ActiveX DLL to VB.NET—and still access its functionality from VB6 apps.

There is a learning curve, of course—starting with some key concepts about how the system marshals data between COM and .NET platforms, and how that can impact performance. When code on either platform calls a method on an object running on the other platform, the method call requires marshaling. In marshaling, the .NET Interop services package up the method call, transfer it to the other platform, invoke the method, package up the results, then transfer them back to your original platform (see Figure 1). This causes a performance hit as the marshaling process moves the parameters from one platform to the other.

This performance hit will vary depending on the data types of your parameters and result types. Performance is best when you're moving around simple numeric data, such as 32-bit integers. These are isomorphic or blittable data types, which are represented in memory the same way on both platforms. Consequently, Interop with them proceeds almost as quickly as regular method calls. The term "blittable" refers to the fact that you can simply copy, or blit, isomorphic data from COM memory to .NET memory (or vice versa).

Figure 1. Mix and Match Objects and Platforms

The blittable data types are the VB6 Byte, Integer, and Long, and the corresponding Byte, Integer, and Short data types in VB.NET. You can also blit the VB.NET 64-bit Long data type, which lacks a corresponding VB6 type. One-dimensional arrays of blittable types are blittable, as are VB6 user-defined types or VB.NET structures, as long as they comprise only blittable types.

So much for the good news. Other types of data require more work during the marshaling process because they aren't represented the same in .NET and COM. Data types such as Boolean are non-isomorphic or non-blittable and might require conversion from one format to another—and some extra cycles—as they're copied from one platform to another.

The .NET Interop services provide four types of marshaling (see Table 1). Of these, only Type I marshaling is supported automatically by Interop services and Visual Studio .NET (VS.NET). Type I marshaling handles nearly all the intrinsic data types in VB6 and VB.NET. The other three types can get complex and force you to edit the Microsoft Intermediate Language (MSIL) code in Notepad, among other things.

As long as you use data types supported by Type I marshaling, calling a VB6 DLL from a VB.NET application and vice versa is speedy and virtually transparent. So avoid using the Variant data type if you can, because that's typically where you run into issues. I'll show you how to use Type I marshaling because you'll most likely use this type in your applications.

Call VB6 DLLs From VB.NET
VS.NET lets you reference and use COM DLLs much as you reference and use .NET DLLs. Start by rummaging up a COM DLL. Open the VB6 IDE, create a new ActiveX DLL project, and name it COMdll, using the Project Properties window. Change the name of Class1 to COMclass and add this code:

Option Explicit
Public Function GetString() As String
   GetString = "A string from COM"
End Function
Public Function GetNumber() As Long
   GetNumber = 42
End Function

When you compile the project, you get a simple DLL that returns a blittable numeric value and a nonblittable text value. You can use this DLL transparently from .NET because Type I marshaling supports both data types.

Before you can use any DLL in VB.NET, you need to add a reference to it in your project. This holds true for both .NET DLLs and COM DLLs, and VS.NET allows you to add references to both types of DLLs from the same dialog.

In VS.NET, use the Project | Add Reference menu option to bring up the Add Reference dialog. Click on the COM tab, which gives you a list of all the COM components registered currently on your system. Scroll down and select the COMdll entry (see Figure 2).

Figure 2. Reference Those COM DLLs

VS.NET creates a runtime callable wrapper (RCW) assembly automatically when you click on OK. This important step occurs behind the scenes. The RCW is a .NET assembly that your .NET code sees much like a COM DLL. When you write code that calls the COMdll component, your code is really calling the RCW, which then calls the underlying COM component on your behalf. The RCW takes care of the marshaling process, making the actual call from the .NET platform over to the COM platform. It can also let you share COM components among many .NET apps (see the sidebar, "COM One, COM All").

Add a couple TextBox controls and a Button control to the form, then double-click on the Button control to bring up the code window. Add this code within the Click event:

Dim obj As New COMdll.comclass()
TextBox1.Text = obj.GetString
TextBox2.Text = obj.GetNumber
Marshal.ReleaseComObject(obj)

This code creates an instance of the COMdll component, then calls the methods on that object to populate the display. Notice that IntelliSense is available as you enter the code. VS.NET is fully aware of the details of the VB6 class and its methods.

The code then calls Marshal.ReleaseComObject to release the reference to the underlying COM object. You do the same thing when you set an object to Nothing in VB6 because COM objects are destroyed when they're released, and they expect to be released as soon as you're done using them. That's how VB6 works.

On the other hand, objects in VB.NET aren't destroyed until the .NET garbage collection algorithm gets around to it. By calling ReleaseComObject, you force .NET to more closely simulate the behavior of COM—always a good idea when working with COM objects.

Before this code will work, you need to add an Imports statement to the top of the file:

Imports System.Runtime.InteropServices

This provides the namespace where the Marshal class resides.

Now you can run the application and click on the button. The form should then display the data retrieved from the VB6 DLL.

Call VB.NET DLLs From VB6
You'll find it nearly as easy to reverse the process. Of course, without your intervention, a COM-based client—such as a VB6 app—won't be able to use a VB.NET DLL. This takes only a few steps to accomplish, though.

In VS.NET, create a new Class Library project named NETdll. Remove the template code from the Class1.vb file and write this code:

Public Class NETclass
   Public Function _
      GetString() As String
      Return "A string from .NET"
   End Function
   Public Function _
      GetNumber() As Integer
      Return 42
   End Function
End Class

This code provides the same functionality as the COM DLL you created earlier to make it available to VB6. Use the <ComClass()> attribute to mark NETclass so you can export it for use by COM. The <ComClass()> attribute makes .NET export the right COM interfaces to make your class available to VB6 clients.

Of course, COM relies on globally unique identifiers (GUIDs) to identify your class. If you don't specify them, .NET will create some for you automatically—but then you lose control of them. You didn't have control of them in VB6, and it caused no end of grief with incompatible CLSID values, binary compatibility, and DLL Hell.

The <ComClass()> attribute allows you to specify GUID values for your class, giving you control over when and if they change, and helping you prevent compatibility issues and the aforementioned DLL Hell. You should always provide unique GUID values for your class by using these parameters.

Figure 3. Have a GUID Time

You need to specify three values. The first is the class ID (CLSID) for the class itself. The second is the interface ID. In COM, you never really interact with a class; you always interact with an interface. So you need an ID—not only for your class, but also for its formal COM interface. The third and final parameter is an ID for your events. If your class raises any events, they will also have a formal interface that must have an associated GUID.

You can generate the GUID values by using the guidgen.exe utility (included with VS.NET) to generate Registry-style GUID values you can cut and paste into your code (see Figure 3). I prefer to declare three constant values in each class to store my GUIDs, named ClassId, InterfaceId, and EventsId (see Listing 1). Then you can use these constants in the <ComClass()> attribute:

<ComClass( _
   NETclass.ClassId), _
   NETclass.InterfaceId), _
   NETclass.EventsId)> _
   Public Class NETclass

The <ComClass()> attribute is specific to VB.NET. If you're programming in C# or don't want to rely on VB-specific features, you could do this by hand, using other .NET attributes. To do so, you must declare an interface that includes the methods you want to be available for use by COM (see Listing 2). Name your interface the same as the class, with an underscore in front. So for this example, you'd name the interface _NETclass. Then apply the <InterfaceType(ComInterfaceType.InterfaceIsDual)> attribute to tell .NET to export a dual interface to COM, so both early-bound clients (such as VB6) and late-bound clients (such as ASP) can use the DLL.

You also need to attribute the class itself with the <ClassInterface(ClassInterfaceType.None)> attribute to prevent any interfaces from being exported directly from the class itself. VB6 expects only the formal interface—prefixed with an underscore—to be made available.

Finally, use the Implements keyword to implement the interface within the class, and use the Implements clause to bind each method from the interface to a corresponding method within the class itself. If your class raises any events, you also need to create a formal interface that lists each event as a method. Attribute this interface with <InterfaceType()>. This will mark the interface as an IDispatch interface to COM. The class itself must also include the <ComSourceInterfaces()> attribute to link the event interface to the class.

You can download my code sample, which includes an example of this approach. I'll proceed now assuming the much simpler <ComClass()> attribute does the work for you.

In either case, you must mark the project itself so VS.NET registers the DLL with COM. Right-click on the NETdll project in the Solution Explorer and select Properties. Click on Configuration Properties and Build in the left-hand pane, and check the option named Register for COM Interop (see Figure 4).

Figure 4. Automate Your Interop

With this option checked, VS.NET creates a COM-callable wrapper (CCW) and a COM type library for your class the next time you build the solution. VS.NET also registers the DLL in the Windows Registry, making it available to VB6. All this goes on under the hood—the only visible indication is a line in the Output window.

The CCW resembles the RCW that VS.NET created for you earlier. This assembly handles the marshaling and all the details that let a COM client app invoke your VB.NET object. Build the project to create the DLL, then open the VB6 IDE. Create a new Standard EXE project. Bring up the References dialog under the Project menu. Your NETdll component should be listed in the dialog. You can add a reference to it just as you would add a reference to any COM component.

Now, add a couple TextBox controls and a Command control to Form1. Double-click on the button to bring up the code window. Add this code in the Click event:

Dim obj As NETclass
Set obj = New NETclass
Text1.Text = obj.GetString
Text2.Text = obj.GetNumber
Set obj = Nothing

As you enter the code, notice that IntelliSense is available. VB6 treats this VB.NET class just like a regular COM class, with nothing in the code to indicate it's anything special at all. Finally, run the app and click on the button. The display should show the values retrieved from the VB.NET assembly.

.NET's Interop services deliver a useful and usually simple way to reuse existing VB6 DLLs from VB.NET, and to use VB.NET DLLs from VB6 as you migrate your components over time. In most cases, Interop services do all the hard work for you, and VS.NET often automates the process. If you're creating multithreaded .NET apps or passing complex data types through Variants, things can get complex and you'll need to do more work by hand.

About the Author
Rockford Lhotka is the coauthor of several books, including Professional Visual Basic Interoperability—COM and VB6 to .NET (Wrox Press), and is a contributing author for VSM and a columnist for MSDN Online. Rockford is the principal technology evangelist for Magenic Technologies, a Microsoft Gold Certified Partner. Reach Rocky at .

Close Window