Modernizing a Visual Basic 6 application using Google Gemini

In real life... strike that, this is real life... in my work life I've been involved in a few Visual Basic 6 conversion projects. I think Visual Basic 6 is nearly flawless but the rest of the industry moved on from it (often involuntarily). These types of projects were hugely popular in the early 2000s for obvious reasons. I worked on one as late as 2016. I won't go into great detail, but these projects all took different approaches:

Early on, Microsoft provided a tool to assist with conversion. As I recall, and this is not an absolute fact, it only worked with .NET 1.0. Maybe it worked with 2.0, I'm just going off memory. What I recall is most projects I worked on started when that tool supported an obsolete version of .NET. Most sane teams are not rushing to rewrite perfectly functional applications, even when it means they may be cornered into it later.

Today there is, I suspect, a better option - AI (LLMs specifically). I tried using Google Gemini to convert FreeBASIC code to Java and it worked reasonably well. There was no UI involved though. I wonder how it would work for a Visual Basic 6 application with a Windows forms UI? Could Gemini convert something like this to Java Swing or JavaFX?

One could argue that taking an obsolete desktop application and converting it to a different desktop framework isn't really "modernizing". I chose that term because my goal is getting the code into something supported by modern IDEs. Being mostly cross-platform is a nice thing too.

By luck, I have three Visual Basic 6 applications that are small enough to test this idea out. Quite some time ago I wrote three save game editors in Visual Basic 6 because it was the easiest way to build them. These were all editors with a ton of UI elements like checkboxes.

The three Visual Basic 6 editors

I decided to try converting Falcone first. It edits save states for Order of the Griffon on TurboGrafx-16. I built it with the intention of finally finishing the game after so many years. I never followed through on that plan. Maybe that will change?

Order of the Griffon

OK, really, I just started converting these applications in alphabetical order.

I picked the worst of these three projects to start with though. When I wrote Falcone I was lazy and made it edit Magic Engine save states specifically. I should have made it edit the SRAM so it would be portable across emulators. I think at the time Magic Engine was the de-facto standard for TurboGrafx-16 emulation and probably it still is on Windows. This makes Falcone not an especially useful editor. For example, I can't edit the save games created on the Analogue Pocket. I suppose by porting it to Java, it will be easier to update it to support SRAM too (or instead). It also means I get to figure out another checksum algorithm which is fun.

OK, I talked myself out of thinking this is a complete waste of time.

To start the analysis I uploaded the form (which has most of the code) and the project file.

Initial analysis

The initial analysis is an accurate summary of the editor.

Proposed Swing solution

Gemini recommended Swing instead of JavaFX which I am fine with. For a small application like this, Swing works well. It will also be easier for others to download and run. As much as I enjoy working with JavaFX, it leads to so many questions from people who can’t get a program I wrote to run.

Initial analysis done

After completing the analysis it's ready to generate the code. Wish us luck.

UI analysis

Gemini begins the process with a description of the UI conversion approach. It then churns for, well, not long. It was done in about a minute.

Improvements made

What's interesting to me is it didn't ask about these improvements, it just made them. I suspect if I changed the prompt I could have it ask about each recommendation before doing them.

The code it produced was one single Java file. It had no errors and only two annoying warnings:


The serializable class FalconeMain does not declare a static final serialVersionUID field of type long

Type safety: The expression of type JComboBox[] needs unchecked conversion to conform to JComboBox<String>[]	

The first warning is whatever, a one second fix if you care to fix it. The other warning is, I think, unfixable. It comes from this class variable:


private JComboBox<String>[] itemBoxes = new JComboBox[24];

There's a long explanation and the short version is that sometimes warnings don't know you're doing the right thing later on in the code.

Let's look at these three improvements real quick.

The first one is taking code generated by the Visual Basic IDE like this:


   Begin VB.ComboBox ComboBoxItem1 
      Height          =   315
      Left            =   840
      Style           =   2  'Dropdown List
      TabIndex        =   21
      Top             =   2880
      Width           =   2295
   End
[... times 24 boxes ...]

And changing it to:


JPanel itemPanel = new JPanel(new GridLayout(12, 2, 10, 2));
itemPanel.setBorder(BorderFactory.createTitledBorder("Inventory Items"));
Vector<String> items = getItemList();
for (int i = 0; i < 24; i++) {
  itemBoxes[i] = new JComboBox<>(items);
  itemPanel.add(itemBoxes[i]);
}

The second item is replacing code like this:


Private Sub TextBoxCharacter1HP_Validate(Cancel As Boolean)
    Dim intVal As Integer
        
    On Error GoTo ValidationFailed

    If Not IsNumeric(TextBoxCharacter1HP.Text) Then
        GoTo ValidationFailed
    Else
        intVal = CInt(TextBoxCharacter1HP.Text)
        If (intVal < 1) Or (intVal > 255) Then
            GoTo ValidationFailed
        End If
        Cancel = False
    End If

Exit Sub
[... for each HP box ...]

And using... nothing. Later on it will ask if I want to generate the validation code.

The last one is rewriting this Visual Basic 6 code...


Private Sub SetComboBoxSelection(ByRef cb As ComboBox, b As Byte)
    Dim sval As String
    Dim index As Integer
    Dim foundIndex As Integer
    Dim count As Integer
    Dim currentItem As String
                
    sval = Hex$(b)
    If (Len(sval) < 2) Then
        sval = "0" & sval
    End If
    
    foundIndex = -1
    index = 0
    count = cb.ListCount
    Do While ((foundIndex = -1) And (index < count))
        currentItem = Mid$(cb.List(index), 2, 2)
        If (currentItem = sval) Then
            foundIndex = index
        Else
            index = index + 1
        End If
    Loop
    
    If (foundIndex > -1) Then
        cb.ListIndex = foundIndex
    Else
        cb.ListIndex = 0
    End If

End Sub

...into this Java code:


    private void setComboByHex(JComboBox<String> cb, int val) {
        String hex = String.format("[%02X]", val);
        for (int i = 0; i < cb.getItemCount(); i++) {
            if (cb.getItemAt(i).startsWith(hex)) {
                cb.setSelectedIndex(i);
                return;
            }
        }
        cb.setSelectedIndex(0);
    }

Alright, time to fire it up:

First run

OK, so there are some problems right away. It's missing the maximum HP values and only has a small subset of the items in the dropdown box.

First run problems

I'll start with the second first, it only stubbed out that part:


private Vector<String> getItemList() {
  Vector<String> list = new Vector<>();
  // Ported from LoadComboBox 
  list.add("[00] = Empty");
  list.add("[01] = Short Sword");
  list.add("[02] = Short Sword +1");
  list.add("[0B] = Staff");
  list.add("[51] = Plate Mail Armor");
  // ... (Add all items from  here)
  return list;
}

After looking at a save file in a hex editor I think I figured out the first problem. The Get command in Visual Basic 6 starts at one. The first byte in the file being read is 1, unlike Java (and almost everything else) which starts at 0.

Explaining Visual Basic Get command

It appears to understand the problem... but then suggests an odd code fix. It wants to replace the initially generated code...


private final int HP_OFFSETS[] = {46696, 46697, 46754, 46755, 46812, 46813, 46870, 46871};

...with this new structure:


private final int[][] CHARACTER_HP_OFFSETS = {
    {46695, 46696}, 
    {46753, 46754}, 
    {46811, 46812}, 
    {46869, 46870} 
};

All I wanted it to do is decrement the values, which it didn't do. Instead it decided to make the code more complex without addressing the actual problem. So I fixed it by hand and tried again.

Second run

Let's see if the save button works:

Saving works

Alright, after editing everything, closing, and reloading... things are working as expected:

Third run

The last item, for now, is creating the validation logic that was left out before. Gemini created some standard code for this:


public class HPInputVerifier extends InputVerifier {
  @Override
  public boolean verify(JComponent input) {
    String text = ((JTextField) input).getText();
    try {
      int value = Integer.parseInt(text);
      // Original VB6 logic: "Please enter a number between 1 and 255" [cite: 123, 133]
      if (value >= 1 && value <= 255) {
        input.setBackground(Color.WHITE);
        return true;
      }
    } catch (NumberFormatException e) {
      // Not a number
    }
    // Validation failed
    input.setBackground(new Color(255, 200, 200)); // Light red background for error
    return false;
  }
  @Override
  public boolean shouldYieldFocus(JComponent source, JComponent target) {
    boolean valid = verify(source);
    if (!valid) {
      // Original VB6 logic: show a MsgBox on failure [cite: 123, 133]
      JOptionPane.showMessageDialog(null, 
      "Please enter a number between 1 and 255", 
      "Invalid Input", 
      JOptionPane.ERROR_MESSAGE);
    }
    return valid;
  }
}

Which is then attached like this:


HPInputVerifier hpVerifier = new HPInputVerifier();
for (int i = 0; i < 8; i++) {
  hpFields[i] = new JTextField(5);
  // Attach the verifier to each field
  hpFields[i].setInputVerifier(hpVerifier);
[...]

I might tweak the validation code a little but it's good enough for now.

The base conversion and debugging only took about an hour. Cleaning up the code into a style I prefer took about 20 minutes. I think changing the addresses to use a different save state format would be easy. Supporting multiple formats, and/or SRAM, would be a medium amount of work.

OK, this is not bad. I would say overall it went well. The cleaned-up code is here and the latest build can be downloaded here.

I'd like to close by considering some common critiques of using AI for problems like this.

In this example I took a free, open source program written by a single person and upgraded it using an LLM. Had I not used an LLM, this upgrade would simply never happen. I didn't lose out on the intellectual challenge of converting this code because I wasn't going to ever do that. One day I'd click the "archive repository" button in GitHub and that would be it. Now I have something that can be updated on modern IDEs.

I'm sensitive to concerns about the power utilization of LLMs. Being able to edit Order of the Griffon save states is a pretty trivial thing to waste power on. Firing up the PlayStation 5 every night is a pretty trivial thing to waste power on... running xmas lights for a month is a pretty trivial thing to waste power on... I don't know where one person should draw the line.

Application modernization is not a great use of anyone's time. This makes it a suitable use case for LLMs. Time spent simply upgrading code is time not spent building something new. Go ahead and burn some GPU cycles so other code can take care of this.

I was thinking about the last Visual Basic conversion project I worked on, the one from 2016. It actually started before then but effectively ended that year. I wasn't hands-on with the project, I was the lead architect for the company that owned the product (this is all visible on my LinkedIn so it's not a secret). This product had paying customers and competitors. The Visual Basic technical debt meant we couldn't build features as quickly as competitors. Ultimately the product fell too far behind and was abandoned.

LLMs will definitely eliminate jobs, I'm not naive. It will also save some. This abandoned application had ~15 years of business logic buried in Visual Basic code. With something like Gemini, I think we could have converted & tested all that code in 1-3 months. I very rarely underestimate how long a project will take so I mean 3 months max. If we had a suddenly modern codebase, I'm positive we could have kept up with competitors. This is the first time I've pondered what could have been and it's not a happy feeling.

Based on my almost 30 years of professional experience I assure you there are tons of legacy systems on life support. In the past, businesses had to make very tough decisions about them. Often entire products are scrapped along with everyone supporting them. There is now a modernization approach that didn't exist even 5 years ago. There's a lot of talk about "vibe coding" to create new applications. I think the potential to rejuvenate existing, functional systems is being overlooked in it.

I chose Java for this because I thought it would be easy. Based on this trial I suspect a conversion to Electron or a native mobile application would be easy for someone experienced in those. Someone unfamiliar with the nuances of Visual Basic might find themselves stumped by an issue like the one I encountered. Perhaps this would be a good way to learn a new language or framework. I have at least two more small applications I want to modernize like this. After that, I'm not sure exactly what I want to tackle - only that I am going to try more projects like this.



Related