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. .gif)
.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.