Exploring version 1.10 - Gosubs Refactoring

clock October 13, 2008 09:02

If you read virtually all books and articles related to VB6 migration, you'll learn that VB.NET doesn't support the Gosub keyword, therefore you had to manually refactor your VB6 apps and turn those Gosubs into calls to separate method.

All these articles and books were clearly written before VB Migration Partner made its debut, because we have demonstrated that the inability to convert Gosub keywords to VB.NET was only a limitation of the conversion engine used by the Upgrade Wizard that comes with Visual Studion .NET.

In fact, since its early beta versions VB Migration Partner has always been able to handle Gosub keywords and its On...Gosub variant. Translating a Gosub keyword isn't at all difficult if you think that a Gosub/Return pair can be simulated by a Goto plus another Goto that brings execution back to the statement following the Gosub. It's a simple technique and I show how to implement it during my Tips, Tracks, and Techniques in Migrating VB6 Applications to .NET 75-minute speech. (BTW, if you are attending VSLive! in Las Vegas, don't miss this session... it's tomorrow at 11 am.) Actually, it's such a simple technique that I always wondered why Upgrade Wizard didn't implement it.

Rendering Gosub/Return by means of Goto pairs ensures full functional equivalence between the original VB6 code and the generated VB.NET code, but has one defect: it makes your code less readable than it should. For these reason, a few users asked for a better way of dealing with this VB6 syntax.

The obvious solution was to extract the code portion pointed to by the Gosub keyword into a separate method and replace the Gosub with a call to that external method. The code in the external method must be able to access some (or all) variables in the original code, therefore it is necessary to pass these variables as ByRef parameters to the external method. Finally, if the original method included an On Error Resume Next statement, this statement has to be present in the external method as well.

Implmenting all these features wasn't a breeze, but we managed to do it and we are very proud of the result. As far as we know, VB Migration Partner is the first and only VB conversion tool that has this feature

Enabling Gosub refactoring is as easy as adding a ConvertGosubs pragma. Like most pragmas, it can have project-, file-, and method-level scope, therefore you can decide where exactly it has to be applied. In most cases, the best approach is adding it to the master VBMigrationPartner.pragmas file, so that all your migration projects will use it. 

    '## project:ConvertGosubs True

(You can disable this feature inside a specific file or method by simply inserting another pragma with narrower scope and False in its argument.) Consider the following VB6 code:

Function GetValue(x As Integer, id As String) As Integer
    '## ConvertGosubs True
    On Error Resume Next
   
    Dim s As String, name As String
    s = "ABC"
   name = "Code Architects"
    GoSub BuildResult
    Exit Function

BuildResult:
    GetValue = x + Len(s) + Len(name)
    Return
End Function

VB Migration Partner detects that the code that begins at the BuildResult label (a.k.a. the target label) can be safely refactored to a separate method that receives four arguments. The result of the conversion is therefore:

    Public Function GetValue(ByRef x As Short, ByRef id As String) As Short
        Dim s As String
        Dim name As String
        On Error Resume Next

        s = "ABC"
        name = "Code Architects"
        Gosub_GetValue_BuildResult(x, GetValue, s, name)
        Exit Function
    End Function

    Private Sub Gosub_GetValue_BuildResult( _
            ByRef x As Short, ByRef GetValue As Short, _
            ByRef s As String, ByRef name As String)
        On Error Resume Next
        GetValue = x + Len6(s) + Len6(name)
    End Sub

Notice that the external method contains an On Error Resume Next statement, because the original GetValue method also contains this statement. Also notice that the ID parameter isn't passed as an argument to the external method because it isn't referenced by the code inside it.

All arguments to the new Gosub_GetValue_BuildResult method are passed by reference, so that the caller can receive any value that has been modified inside the method (as is the case with the GetValue parameter in previous example.) You can have VB Migration Partner optimize this passing mechanism and use ByVal if possible by passing True as the second argument to the ConvertGosubs pragma:

    '## ConvertGosubs True, True

If this optimization is used in the above code, the separate method is rendered as follows:

    Private Sub Gosub_GetValue_BuildResult( _
           ByVal x As Short, ByRef GetValue As Short, _
           ByVal s As String, ByVal name As String)
        On Error Resume Next
        GetValue = x + Len6(s) + Len6(name)
    End Sub

If you don’t like the name that VB Migration Partner assigns to the automatically-generated method, you can easily change it by means of a PostProcess pragma:

    '## PostProcess "Gosub_GetValue_BuildResult", "AssignValueResult"

It is important to keep in mind that the conversion of a Gosub block of code into a separate method isn’t always possible. More precisely, VB Migration Partner can perform this conversions only if all the following conditions are met:

a) The method doesn't contain any Resume statement. (On Error Resume Next is OK, though).
b) The target label isn't located inside a If, Select, For, Do, or Loop block.
c) The target label isn't referenced by a Goto, On Goto/Gosub, or On Error statement.
d) The target label must be preceded by a Goto, Return, End, Or Exit Sub/Function/Property statement.
e) The code block must terminate with an unconditional Return, or an End Sub/Function/Property statement.  (Blocks that terminate with Exit Sub/Function/Property statements aren't converted.)
f) The block of code between the target label and the closing Return/End statement doesn't contain another label.

If the conversion of a specific Gosub isn't possible, VB Migration Partner reverts to the default way of converting Gosub/Return (i.e. using Goto pairs).



Exploring version 1.10 - The rename engine

clock October 10, 2008 07:50

Yesterday we finally released VB Migration Partner 1.10 BETA, which registered users can download from our site. Being a beta, we decided to keep the more recent "official" version (1.00.06) online, so that customers can have a choice.

The new version includes so many exciting refactoring features that allow you produce the kind of high-quality VB.NET code that experienced .NET develors can write, only much much faster. For example, you can now reliably merge nested IF blocks or convert On Error Goto and On Error Resume Next statements into Try-Catch blocks by just adding a new project-level pragma.

In this article I show how to leverage the brand-new symbol rename engine. A few customers asked for the ability to rename classes, methods, and controls during the migration process so that the VB.NET code would abide by their naming coding standards. Being the authors of a successful book entirely devoted to coding standard, we couldn't ignore the request.

Earlier versions of VB Migration Partner already included a rename engine - necessary to rename VB6 members that happened to be named after VB.NET keywords, among other things - and more expert users were able to hook into that engine by writing a VB Migration Partner Extender, i.e. a separate DLL that interacts with our tool during the three stages of the migration process (parsing, interpreting, code generation).

DECLARATIVE RULES
Writing an extender requires some familiarity with VB Migration Partner's object model, something most customers don't want toget involved into. (VB Migration Partner already comes with a few extenders that allow you to do complex auxiliary tasks, such as polishing UserControl classes and generating cumulative migration reports when migrating multiple projects in batch mode.)

Thus we decided to expose the inner rename engine to the outside and provide our users with the ability to write declarating rename rules based on regular expressions and stored in an XML file. Thanks to the internal rename engine the entire process was very simple and took a couple of days, most of which were spent to write a series of unit tests and a sample XML file containing a set of standard rename rules for you to use, study, and modify as you see fit.

Enabling this new feature is as simple as adding one ApplyRenameRules pragma anywhere in the VB6 project's source code:

    '## ApplyRenameRules  c:\myrules.xml

The argument is the path of the XML file containing all the rename rules in declarative format. If you omit the argument, the standard RenameRules.xml file that comes with VB Migration Partner 1.10 is used. Instead of adding an ApplyRenameRules pragma in each VB6 project you can store it in a VBMigrationPartner.pragmas file shared by one or more projects. Even better, you can store it in the master pragmas file located in VB Migration Partner's install folder, in which case all yourmigrated projects will use it. Here's a fragment of the XML file that comes with the standard installation:

 <?xml version="1.0" encoding="utf-8" ?>
<NamingRules>
  
   <!-- Components section -->
   <Symbol kind="Component" type="VB.Label" >
      <!-- ensure that we have a "lbl" prefix -->
      <Rule pattern="^(lbl)?" replace="lbl" />
      <!-- drop the "Label" suffix -->
      <Rule pattern="Label$" replace="" />
   </Symbol>

   <!-- more rules for all common controls -->

</NamingRules>

The kind attribute is used first to determine the kind of symbol the rule applies to. Valid names are Form, Class, Module, Type, UserControl, Enum, Component, Method (includes subs and functions), Field, Property, Event, EnumItem (the members of an Enum declaration), Constant, and Variables (includes, local variables and parameters.)

If kind is equal to Component or Variable, you can optionally include a type attribute, that specifies the VB6 type of the member to which the rule applies.In the above example, the type attribue is used to specify that the rule applies to all components of type VB.Label (the standard VB6 label). But you aren't confined to renaming controls: for example, the following rule renames all Integer variables named “i" into “index”

   <Symbol kind="Variable" type="Integer" >
      <Rule pattern="^i$" replace="index" ignoreCase="false" />
   </Symbol>

Renaming rules are based on regular expressions. The pattern attribute is used to decide whether the rule applies to a given member, and the replace attribute specifies how the member should be renamed. The ignoreCase optional attribute should be false if you want the pattern to be applied in case-sensitive mode (by default comparisons are case-insensitive). Finally, the changeCase optional attribute allows you to change the case of the result and can be equal to “lower”, “upper”, “pascal” (first char is uppercase), or “camel” (first char is lowercase).

Being Visual Basic a case-insensitive language, in most cases the default behavior is fine. However, consider the following renaming requirement: many VB6 developers preferred to use “C” as a prefix for all class names, but this guideline has been deprecated in VB.NET. Here’s a rule that drops the leading “C”, but only if it is followed by another uppercase character:

   <Symbol kind="Class" >
      <Rule pattern="^C(?=[A-Z])" replace="" ignoreCase="false" />
   </Symbol>

The changeCase attribute is useful to enforce naming rules that involve the case of identifiers. For example, a few developers like all-uppercase constant names; here's a rule that enforce this guideline:

   <Symbol kind="Constant" >
      <Rule pattern="^.+$" replace="${0}" changeCase="upper" />
   </Symbol>

 

There are a few other options that I don't cover here, if you are interested just have a look at the Renaming Program Members section of our manual. 

PROGRAMMATIC RULES
Declarative rules are easy to write and test - if you are familiar with regexes, at least - but there are times when you want to be in full control of how a symbol or a category of symbols are renamed. Achieving this feat with VB Migration Partner is quite easy:

1) Create a Class Library project inside Visual Studio 2005 or 2008 (you can use any .NET language, including VB.NET or C#)
2) Add a reference to the CodeArchitects.VBMigrationEngine.Dll
3) Create a public class, name it as you prefer, and make it inherit from CodeArchitects.VBMigrationLibrary.SpecialRuleBase
4) Override the GetSymbolNetName method
5) write the code that analyzes the properties of the VBSymbol passed as an argument and return its new name.
6) compile the DLL and drop it in VB Migration Partner's install folder.

For example, let's say that you want to replace the "lng" prefix with the "int" for all 32-bit integer variables defined inside a method named MyMethod. Here's the code that does it:

Public Class MyRenameRules
   Inherits SpecialRuleBase

   Public Overrides Function GetSymbolNetName(ByVal symbol As VBSymbol, _
         ByVal netName As String) As String
      If symbol.Kind = SymbolKind.LocalVar _
            AndAlso symbol.TypeName = "Long" _
            AndAlso symbol.ParentSymbol.Name = "MyMethod" Then
         If netName.StartsWith("lng") Then netName = "int" & netName.Substring(3)
      End If
      Return netName
   End Function
End Class


It couldn't be simpler, uh?

A word of caution: when you supercede VB Migration Partner's rename engine you are responsible for generating names that are both valid and unique. For example, if you drop either a prefix or a suffix, you should ensure that the new name is still unique, otherwise the generated VB.NET project won't compile. 



Subtle array cloning issues

clock October 5, 2008 10:46

As you may know, VB6 performs a copy of the array when assigning an array to another array variable. After the assignment, the source and destination arrays are distinct objects, as this code demonstrates:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer
dest() = source()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

Conversely, an array assignment under VB.NET just copies the array address:, the source and destination array variables point to the same set of elements. In other words, changing an element through either variable would affect the value seen by the other array variable.

For these reasons, both the Upgrade Wizard and earlier versions of VB Migration Partner automatically force a copy operation when converting an array assignment. This is how Upgrade Wizard converts the above VB6 code snippet:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer  
dest = source.Clone()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

It seems that adding a call to the Clone method solves the problem once and for all, but unfortunately this isn’t the case. As a matter of fact, there are several cases when the Upgrade Wizard (and probably other VB6-to-VB.NET conversion tools) doesn’t preserve functional equivalence with the original VB6 code.

For startes, a plain Clone method doesn't work correctly if the array you are assigning is an array of arrays (Under VB6, an array of arrays is a Variant array whose individual elements are arrays.) The problem here is that the Array.Clone method performs a shallow copy and doesn’t correctly copy nested arrays and the converted VB.NET application doesn’t work as it should. For this reason, starting with version 1.10, VB Migration Partner supports the CloneArray6 helper method, which can perform both a shallow copy and a deep copy (if the second argument is True, also nested arrays are copied):

   Dim arrayOfArrays() As Variant
    ' …
    Dim dest() As Variant
    dest() = CloneArray6(arrayOfArrays(), True)

The CloneArray6 method is available both in VB6 and VB.NET, but the VB6 version is a do-nothing version and doesn't change the application behavior. The CloneArray6 method can correctly handle arrays of arrays of any nesting level, arrays of structures, and more in general arrays of any ICloneable object.

Another case when UpgradeWizard generates bugged VB.NET code is when assigning a structure that contain one or more arrays. Consider this VB.NET code, generated by Upgrade Wizard when converting an assignment between two structures:

Structure MyUDT
    Public arr() As Integer
End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt
    ' udt and udt2 point to different structures, but
    ' they share the same set of array elements
    ' …
End Sub

It is obvious that the assignment between structures doesn’t work as in the original VB6 code, because the udt and udt2 structures are pointing to the same array: changing one element in the array in one structure affects the array pointed to by the other structure.Unlike Upgrade Wizard and other conversion tools, VB Migration Partner works around this issue by rendering the assignment as follows:

Structure MyUDT
    Public arr() As Integer

    Function Clone() As MyUDT
        Dim copy As MyUDT = Me
        copy.arr = CloneArray6(Me.arr)
       Return copy
    End Function

End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt.Clone()
    ' udt and udt2 are now completely disjoint objects, and share no arrays
    ' …
End Sub

You might bump into another elusive bug when invoking methods that store a reference to the array passed as an argument. The simplest example of this problem is when you store an array into multiple items of a Collection, as this VB6 code demonstrates:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' notice that here we reuse the same array…
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)
Debug.Print col(1)(0) & "," & col(2)(0)   ' => "Francesco,Giuseppe"

The Upgrade Wizard converts this code verbatim, but the resulting VB.NET code just doesn’t work as expected: both col(1) and col(2) elements actually point to the same array, therefore the text displayed in the Output window is not the one you hoped for:

Debug.WriteLine(col(1)(0) & "," & col(2)(0))  ' => "Giuseppe,Giuseppe"

A solution to this problem is to explicitly ReDim the array after each Add method, as follows:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' allocate a new memory block for array elements
ReDim values(1)
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)

However, this workaround adds unnecessary overhead to the VB6 code. Starting with VB Migration Partner version 1.10 you can avoid this overhead and have the most efficient behavior in both VB6 and VB.NET by using the CloneArray6 method:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(CloneArray6(values))
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(CloneArray6(values))

The bottom line: pay close attention to how arrays are assigned and copied in your VB.NET project and carefully scrutinize the code that your VB6-to-VB.NET migration tool generates in these cases. Else you might be spending hours trying to track down these elusive bugs.



A better cure for a common VB6 misconception

clock October 3, 2008 02:48

In an article I wrote about two months ago I showed how you can fix a programming mistake that is quite common among VB6 developers, who tend to believe that the following declaration

     Dim x1, y1, x2, y2 As Integer

declares four integer variables, whereas it actually declares only one integer (y2) and three Variant variables. The solution I proposed required one PreProcess pragma for statements declaring two variables, another PreProcess pragma for statements declaring three variables, and so forth.

Yesterday Marco Giampetruzzi - from the VB Migration Partner Team - surprised me with a better solution that uses just one pragma to account for all Dim, Public, Private, and Static statements, regardless of the number of variables they declare:

    '## PreProcess "(?<=\b(Dim|Public|Private|Static)\b.*)
                    (?<!\b(Sub|Function|Property)\b.*)
                    (?<!As\s+)(?<var>\b\w+\b)(?=,.*\bAs\s+(?<type>\w+))",
                    "${var} As ${type}", True

Let me briefly explain how it works. The first regex tells VB Migration Partner to search an identifier and name it as "var" (?<var>\w+). This identifier must preceded on the same line by one of the declaration keywords (?<=\b(Dim|Public|Private|Static)\b.*). To prevent that parameters on a method declaration be mistakenly matched, we also check that the identifier not be preceded by a keyword used in method and property declarations (?<!\b(Sub|Function|Property)\b.*). Next, the regex ensures that the indentifier be immediately followed by a comma and (after some characters) by an As clause (=?,.*\bAs\b\s+(?<type>\w+)). Notice that above pragma doesn't support array declarations.

Very smart indeed... thanks Marco!

You can build similar PreProcess pragmas to account for similar cases. For example, the following pragma adds an As clause to parameters inside method declarations:

    '## PreProcess "(?<=\b(Sub|Function|Property)\b.*)
                    (?<!As\s+)(?<var>\b\w+\b)(?=,.*\bAs\s+(?<type>\w+))",
                    "${var} As ${type}", True

If you aren't familiar with regexes, drop a line to our tech support and we'll cook one for you!


Fun in Las Vegas (... and lots of migration tricks)

clock October 3, 2008 02:37

What: a 75-minute session about VB6-to-VB.NET migration tips, tricks, and techniques... by yours truly

When:Tuesday, October 14, at 11 am

Where: Mirage Hotel, Las Vegas