Modernising legacy software: moving Delphi, VB6 and MFC to .NET step by step

Laboratory workstation with pipette, samples and measurement software on a laptop

Software architecture and UML

The software has been running for twenty years, customers rely on it – and the only person who really knows the Delphi code retires next year. Rewrite everything? That takes years and only pays off at the very end. There is a better way: piece by piece, without stopping operations.

Why now – and why not all at once

With Visual Basic 6 the situation is clear. Microsoft has not supported the development environment since 8 April 2008. According to Microsoft the runtime still works on Windows 11, but support is limited to serious regressions and critical security issues. Microsoft explicitly recommends replacing VB6 applications. Delphi and MFC, by contrast, are still maintained. There the pressure comes from elsewhere: fewer developers who understand the code, an outdated user interface, missing tests, libraries without updates.

Nobody tears down a bridge that carries traffic every day in order to build a new one. You build alongside it and move the traffic lane by lane. Martin Fowler named this pattern after the strangler fig, which slowly grows around a tree: new code grows around the old until the old can be switched off. Every step goes into production and delivers value immediately.

Five steps

  1. Inventory: record modules, database, interfaces and forms. Which parts change often, which cause the most defects?
  2. Capture behaviour: run the old software with real inputs and record the results. These “golden masters” are the yardstick for every new line.
  3. Build a bridge: old and new have to talk to each other as long as both are alive.
  4. Move module by module: start with the part that changes most often or hurts most. Ship every step.
  5. Switch off: only when nothing calls the old part any more does it go.

Bridges between old and new

  • Delphi: the old calculation core is made accessible as a DLL with stdcall exports, and .NET calls it via P/Invoke. Alternatively the two talk via COM.
  • VB6: new parts are written in .NET and provided as a COM server. With EnableComHosting the SDK creates a .comhost.dll that is registered with regsvr32. Two traps: VB6 is 32-bit, but the COM host is 64-bit by default. And .NET no longer generates a type library. For early binding in VB6 you write it yourself in IDL or bind late via CreateObject.
  • MFC: C++/CLI connects native and .NET code in the same process. New dialogs are built in WPF or WinForms and embedded in the MFC windows.

This is what the Delphi bridge looks like on a small scale. The old core:

library MeasureCore;

// Old calculation core, exposed as a DLL with stdcall
function Mean(Values: PDouble; Count: Integer): Double; stdcall;
var
  I: Integer;
  Sum: Double;
begin
  Sum := 0;
  for I := 0 to Count - 1 do
  begin
    Sum := Sum + Values^;
    Inc(Values);
  end;
  if Count > 0 then
    Result := Sum / Count
  else
    Result := 0;
end;

exports
  Mean;

begin
end.

And the call from C#, together with the comparison against the new implementation:

using System;
using System.Linq;
using System.Runtime.InteropServices;

double[] series = { 10.2, 10.4, 10.1, 10.3 };
double oldResult = MeasureCore.Mean(series, series.Length);
double newResult = series.Average();
// Golden master: the new code must deliver what the old code delivered
Console.WriteLine(Math.Abs(oldResult - newResult) < 1e-9
    ? "identical" : $"Deviation: {oldResult} / {newResult}");

internal static class MeasureCore
{
    // If the DLL is 32-bit, the .NET process must also run as x86
    [DllImport("MeasureCore.dll", CallingConvention = CallingConvention.StdCall)]
    internal static extern double Mean(double[] values, int count);
}

Data type traps

  • Floating point: Delphi’s Extended is ten bytes on Win32, a .NET double eight. Results therefore differ in the last digits. Compare with a tolerance.
  • Dates: Delphi’s TDateTime and VB6’s Date count days since 30 December 1899 as a floating-point number. DateTime.FromOADate converts them.
  • Currency: in both languages Currency is a 64-bit integer with four fixed decimal places. In .NET it becomes decimal, not double.
  • Strings: a Delphi string never belongs across a DLL boundary. Use PWideChar or WideString. Older Delphi versions work with ANSI text, usually in code page 1252.

From practice

A measuring instrument manufacturer had been using Delphi software developed in-house for decades. It started with consulting and planning: we compared goals and the possibilities of new technologies, and prototypes showed what solutions could look like. Then we trained the customer’s entire team on site. A project team of ten people is porting the software to C# and WPF in an agile way, with MariaDB and Entity Framework. The new software is already in internal use and convinces both measurement technicians and sales. For a civil engineering add-on written in C++, MFC and ObjectARX, a cost-benefit calculation showed in advance when the port would pay for itself.

Typical mistakes

  • Everything new, everything at once: years without benefit, and in the end half of it is missing.
  • Rewriting from memory instead of from measured behaviour.
  • Silently fixing old bugs that users or downstream systems rely on.
  • Mixing 32 and 64 bit. A 32-bit DLL in a 64-bit process ends in a BadImageFormatException.
  • Passing strings across the boundary without a clear rule. The result is crashes or garbled characters.
  • Rebuilding the user interface pixel by pixel instead of improving the workflows.
  • Leaving your own team behind. Anyone who does not understand the new code will soon be maintaining legacy software again.

How we help

Sources: Microsoft, “Support Statement for Visual Basic 6.0 on Windows”; Microsoft Learn, “Exposing .NET Core components to COM”; Martin Fowler, “Strangler Fig Application”; Embarcadero DocWiki, “System.Extended”; case studies “Modernising measuring instrument software: from Delphi to C#” and “Modernising an AutoCAD add-on for civil engineering”, Cadida case studies.

First meetingarrange nowFirst meetingarrange now