This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

.NET InterOp

Rob Macdonald

Once you get much beyond the "Hello, world" level of writing .NET code, it won't take long before you encounter the need to interact with existing COM and API code. Fortunately, .NET comes with a bag of tricks to make it possible to call into existing code from .NET programs and to allow COM clients to call into .NET components. In this article, Rob Macdonald explores these features and also shows you how to use "COM InterOp" to develop migration strategies for moving from COM to .NET.

You probably realize by now that life inside the "managed" environment of the Common Language Runtime (CLR) where all .NET code gets executed is very different from the relatively lawless world in which COM objects and traditional Windows binaries execute. Making calls between these two environments in a safe yet efficient way is something of a challenge, but fortunately, the .NET development team has done most of the hard work for us.

Understanding how .NET interoperates with existing COM servers and Windows API calls is something you'll need to master intellectually before considering whether to upgrade existing applications from VB6 to .NET. In the short term, I suspect that COM InterOp will play a major role in many organizations' overall migration strategies to .NET. For example, if you currently have applications that are split between front-end EXE programs and a set of ActiveX DLLs, you might decide that it will be easier to port your ActiveX DLLs to .NET than your front-end programs. COM InterOp will allow you to keep your front-end programs in VB6. Perhaps all you'll have to do is make a few small changes and then recompile them to use a set of .NET DLLs instead of ActiveX DLLs. This article won't tell you everything you need to know to make such important decisions, but it will get you started.

Making Windows API calls

I'm very much aware that I'm trespassing in (Dan) Appleman Country here, so I'll give you just the briefest introduction to how to make API calls from .NET.

.NET provides a mechanism called "Platform Invoke" (often shortened to P-Invoke) that allows managed code (VB.NET, C#, and so on) to make calls into the Windows API and other raw functions exported by DLLs. While this sounds a little intimidating, in nearly all cases, VB.NET programmers can continue to work with the familiar Declare statement, which conceals most of the internals of P-Invoke.

So far, I haven't located a tool in .NET Beta 2 that's as convenient as the VB6 API Viewer, so let's see if we can have any success using the VB6 tool to generate Declare statements for use in a VB.NET program. Here's the Declare statement that the VB6 API Viewer generates for the GetUserName Windows API function:

Public Declare Function GetUserName Lib 
"advapi32.dll" Alias "GetUserNameA" 
(ByVal lpBuffer As String, nSize As Long) As Long

Take a look at what happens when I paste this code into a VB.NET project in Visual Studio.NET:

Public Declare Function GetUserName Lib _
"advapi32.dll" Alias "GetUserNameA" _
(ByVal lpBuffer As String, ByVal nSize As Long) As Long

Now, I've added the line continuation characters, but take a look to see what Visual Studio.NET has added. In VB6, method arguments are passed by reference unless you specify otherwise. However, the default for .NET is pass-by-value, and therefore, to avoid ambiguity, Visual Studio.NET has inserted the ByVal keyword for the nSize argument. Kind, but cruel, because nSize should be passed by reference. Therefore, I need to remember to explicitly specify ByRef for all arguments that should be passed by reference. There's one other change I should make. VB.NET Integers are the same size as VB6 Longs, so to be correct, I need to change Long datatypes to Integer. Here's the declare statement I need:

Public Declare Function GetUserName Lib _
"advapi32.dll" Alias "GetUserNameA" _
(ByVal lpBuffer As String, _
ByRef nSize As Integer) As Integer

The code for calling this Windows API function is as follows:

Dim RetVal As Integer
Dim Name As String
Dim Buffer As String
Dim NullChar As New Char()
Buffer = New String(NullChar, 25)
RetVal = GetUserName(Buffer, 25)
Name = Buffer.Substring(0, Buffer.IndexOf(NullChar))
MessageBox.Show(Name)

This code creates a string buffer of 25 null characters, which is then passed to GetUserName. I've then trimmed the value of Buffer to exclude trailing nulls, using the SubString method available on strings in .NET.

As you can see, for such a simple case, calling API functions remains similar to the VB6 approach. If you're a heavy user of API calls and plan to remain so when using .NET, it will behoove you to study P-Invoke and the DllImport attribute, along with some of the other new options available for the Declare statement, in more depth.

Calling COM

COM objects and .NET objects might seem to have a lot in common, but they're really very different beasts. .NET provides COM InterOp functionality to bridge the gap between them. To see how easy this can be, I've compiled a VB6 ActiveX DLL project called ThisIsCOM containing a single class called COMClass, coded as follows:

'THIS IS VB6
Public Function Hello() As String
Hello = "Hello from COM"
End Function

I can call this from a VB.NET project as easily as this:

'VB.NET
Dim obj As Object
obj = CreateObject("ThisIsCOM.COMClass")
MsgBox(obj.Hello)

If you're not surprised that it's this easy, you should be! The CreateObject function in .NET not only creates a COM object, it also builds a .NET wrapper object through which the COM object can be accessed. Notice that the obj variable is declared As Object. In VB6, this means late binding, and it means exactly the same in VB.NET—and, as always, late binding means slow runtime operation.

Late binding is only supported in VB.NET, however, when two conditions are met. The first is that you must use a variable declared "As Object," and the second is that Option Strict must be Off. In VS.NET Beta 1, Option Strict was on by default. In Beta 2, Option Strict is off by default. You can change this either by using the Option Strict On statement at the top of each code file, or by setting Option Strict to On using the Project Properties window. Option Strict not only enables late binding, it also relaxes the type checking performed at compile time. As this increases the chance of runtime errors, my preference is to set Option Strict to On in all cases except when I specifically want to use late binding. While late binding is best avoided unless you really need it, it's worth pointing out that the ability to do late binding (with .NET objects as well as COM objects) is one key benefit that VB.NET has over C#. It is possible to do late binding in C#, but not without writing a lot of additional code.

To get early binding, it's not enough simply to turn Option Strict On. If you do this, the preceding code won't compile, because the compiler will check to see whether the Object class supports a method called "Hello" (it doesn't, and therefore the compiler will complain).

Just as in VB6, if you want to use early binding with a COM Server from VB.NET, you need to set a reference to that server, and then declare a variable of the specific type you're interested in. You set a reference to a COM Server by using the Add Reference dialog and selecting the COM tab. This lists all of the available Type Libraries registered on your computer (just like the References dialog in VB6), from which you can select the library of your choice (see Figure 1).

When you press OK, VS.NET will attempt to locate a "wrapper assembly" for this COM Server. If it can't find one, it displays the message box shown in Figure 2, which allows you to have a new wrapper built for you.

If you choose to have the wrapper built, a new .NET assembly will be generated on the fly and will be saved to your project's bin directory—and then referenced from your project. You can then use this assembly in your code just like any other .NET assembly that you might reference. When you use a .NET object from this assembly, it will make early-bound calls to the COM Server that you selected in the References dialog.

I can now declare and use variables of type ThisIsCOM.COMClass within my code with the full early-bound, IntelliSense support I'd expect. I just need to remember that I'm actually using a .NET object that's providing a wrapper around a COM object of the same name. Here's the code:

Dim obj As New ThisIsCOM.COMClass()
MsgBox(obj.Hello)

If I still want to use CreateObject, I can. I'll still benefit from early binding if I declare a variable "As ThisIsCOM.COMClass" as follows:

Dim obj As ThisIsCOM.COMClass
obj = CType(CreateObject("ThisIsCOM.COMClass"), _
   ThisIsCOM.COMClass)
MsgBox(obj.Hello))

With Option Strict On, it's necessary to use the CType function to coerce the object reference returned by CreateObject to the type required by the obj variable.

While you should be cautious about any timing figures given for beta versions of .NET, it's been my experience that using COM InterOp with early binding was about six times faster than using it with late binding, even for the very trivial example shown here. Given that calling COM InterOp will always be slower than calling directly to a .NET object, this kind of performance difference is always going to be significant. The only real disadvantage of using early binding is that you need to remember to include the wrapper assembly as part of your deployment.

COM calling

We've seen how .NET clients can call COM Servers, but it's also likely that you'll want to call .NET components from VB6 apps. As mentioned earlier, this can be an important part of a COM-to-.NET migration strategy. To show you what I mean, I'm going to migrate a VB6 project using a Standard EXE and an ActiveX DLL to a state where my VB6 Standard EXE project is using COM InterOp to call a .NET version of my ActiveX DLL. My VB6 ActiveX DLL is called DoesStuff. It has a class called Useful, which has a method that returns the name of the currently logged on user. Here's all of its code:

'THIS IS VB6
Private Declare Function GetUserName Lib _
"advapi32.dll" Alias "GetUserNameA" _
 (ByVal lpBuffer As String, nSize As Long) As Long
Public Function UserName() As String
Dim RetVal As Long
Dim Name As String
Dim Buffer As String
Buffer = String(25, 0)
RetVal = GetUserName(Buffer, 25)
UserName = Left(Buffer, InStr(Buffer, Chr(0)))
End Function

I have an even more trivial VB6 client project called UsesStuff, which has a reference to DoesStuff and contains the following code:

'THIS IS VB6
Private Sub Command1_Click()
Dim obj As New DoesStuff.Useful
MsgBox obj.UserName
End Sub

To begin the migration process, I've tested out my belief that the VB.NET upgrade wizard for VB6 projects will do a better job with ActiveX DLLs than with Standard EXEs in many cases. You can run the wizard simply by opening a .vbp or .vbg file using Visual Studio.NET. I used it to open DoesStuff.vbp and followed the wizard, selecting defaults at each stage. It successfully generated a .NET Class Library project called DoesStuff with the following VB.NET code:

Option Strict Off
Option Explicit On
Public Class Useful
Private Declare Function GetUserName _
   Lib "advapi32.dll"  Alias "GetUserNameA" _
   (ByVal lpBuffer As String, ByRef nSize _
   As Integer) As Integer
   Public Function UserName() As String
     Dim RetVal As Integer
      Dim Name As String
      Dim Buffer As String
      Buffer = New String(Chr(0), 25)
      RetVal = GetUserName(Buffer, 25)
      UserName = Left(Buffer, InStr(Buffer, Chr(0)))
   End Function
End Class

Notice that the upgrade wizard has correctly converted the VB6 Declare statement. Notice also that the code that's been generated looks quite like VB6 code and is quite different from the .NET code I wrote earlier to perform the same task. The VB upgrade wizard makes heavy use of the Microsoft Visual Basic compatibility library that comes with .NET (in fact, this library was written primarily for use by the upgrade wizard). You can use the functionality provided by the compatibility library in your own code, and arguably it makes adopting .NET easier than learning your way around the many .NET base class libraries. Personally, however, I prefer to come to grips with the base class libraries, because this is going to make it much easier for me to read and write code in other .NET languages when the time arises—as well as making it easier for C# programmers to read my VB.NET code.

Now I have a .NET Class Library that performs the same task as my ActiveX DLL, and I want to be able to call this library from my VB6 Standard EXE client with the minimum of changes. Before doing anything, I've unregistered my ActiveX DLL to leave the Registry clean.

Obviously, there's no functionality built into VB6 right now to allow it to access a .NET assembly. Instead, .NET ships with a command line tool that will generate a COM Type Library for any .NET assembly. You can then set a reference to this Type Library from your VB6 client. The Type Library will connect VB6 to a COM InterOp wrapper generated by .NET that makes the .NET assembly look like a COM object. The .NET tool I'm going to use is called REGASM, and the easiest way to use it is to open a specially configured command prompt available from the Visual Studio.NET Tools menu that's created when you install Beta 2. Once I've opened this command prompt, I can navigate to the directory containing the .NET assembly called DoesStuff.DLL. At the command prompt, I can then build a Type Library using the following command:

regasm DoesStuff.dll /tlb:DoesStuff.tlb

This tells REGASM to build a Type Library called DoesStuff.tlb, based on the .NET assembly called DoesStuff.dll. Now that I have a COM Type Library, I can go back to my VB6 client. Once I've removed the reference to the old VB6-based DoesStuff (which is no longer registered), I can use the Browse button in the VB6 References dialog to locate the newly generated Type Library (DoesStuff.tlb) and click OK to set the reference.

It looks like I now have a green light to run the VB6 client. However, if I do, I get a message saying that the server can't be loaded. To understand why, you might have to think back to what I covered a couple of months ago concerning assemblies. My DoesStuff assembly is a private assembly, and therefore it must be locatable in the base folder of the calling EXE, or one of its subfolders. The easiest way to achieve this is to copy the .NET DoesStuff.dll into the folder where my client EXE runs. This presents an issue, because when I'm working in the VB6 IDE, the EXE is running from c:\Program Files\Microsoft Visual Studio\VB98, whereas my compiled EXE will probably run from a different location. As a one-off solution, I can simply copy the DLL into both locations for the time being, but you'll probably want to devise a better solution that this, either based on how you organize your development files, or by making the DoesStuff.DLL into a shared assembly with a strong name.

Having addressed these file location issues, I'm now able to recompile my client, which will then be able to call into my .NET assembly without any code changes.

The upshot of this is that I can switch my client to start using a .NET assembly via InterOp without any code changes—all that's required is to change some references and recompile. In fact, it's even possible to avoid the recompilation process in some cases, because you can tell REGASM exactly what GUIDs and DispIDs should be used when building the type library, so that they match precisely with those used in the ActiveX DLL that the .NET assembly replaces.

Conclusion

The main focus of this article has been .NET's InterOp features, which support calls to Windows API functions and bi-directional exchange with COM-based programs. I've used this technology to explore one possible migration path for moving from COM to .NET, but don't get me wrong. The VB.NET upgrade wizard isn't going to be a smooth ride for all of your projects—whether component-based or front ends—and adopting COM InterOp in real programs won't be as easy as I've made it look here.

However, now that Beta 2 is with us, it's time to start thinking about .NET migration strategies, and COM InterOp is clearly an essential part of the equation. Another key technology for hooking up managed and unmanaged code is Web Services, and next month I'll be looking at how you can call .NET Web Services from unmanaged clients (such as VB6).

To find out more about Visual Basic Developer and Pinnacle Publishing, visit their website at http://www.pinpub.com/

Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.

This article is reproduced from the September 2001 issue of Visual Basic Developer. Copyright 2001, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. Visual Basic Developer is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-788-1900.

© 2009 Microsoft Corporation. All rights reserved.   Terms of Use | Trademarks | Privacy Statement
Page view tracker
Rate the Lightweight library
x
Lightweight builds on ScriptFree (loband) by adding features you've requested: a SearchBox and default code language selection.
Do you like the SearchBox?
Do you like the tabbed code blocks?
How useful is this topic?
Tell us more.
Thanks
x
You're helping to improve MSDN Online.
Feedback
Switch View
Classic
Lightweight Beta
ScriptFree
Switch View