How to Upgrade Legacy Delphi Code to Modern Versions

Moving a Delphi 7 or Delphi 2007 codebase to a modern version (11/12 Alexandria) breaks in predictable places — string types, deprecated RTL calls, and component behavior changes. This post walks through the pitfalls I hit most often when migrating client codebases, and the fixes that actually hold up.

The problem

Every few years a client comes to me with the same request: “we have a Delphi application from 2006, it still works, but we need it on a modern compiler.” The reasons vary — Windows 11 compatibility, 64-bit builds, a new hire who can’t get Delphi 7 to install, or just wanting to stop paying for antivirus exceptions on an ancient IDE. The code itself is usually fine. It’s the assumptions baked into it that break.

I’ve done this migration enough times (Delphi 7 to XE, Delphi 2007 to 10.x, Delphi 2010 to 11) to know the failures repeat. Below are the ones that cost the most time, in the order I usually hit them.

String types: the big one

Delphi 2009 switched the default string type from AnsiString to UnicodeString. If your codebase predates 2009, this is where most of your compile errors — and worse, your silent runtime bugs — will come from.

The obvious breakage is anything doing pointer arithmetic on strings, assuming one byte per character:

// Delphi 7 code — assumes 1 byte per char, breaks silently under Unicode
procedure TrimBuffer(var Buf: array of Char; MaxLen: Integer);
var
  P: PChar;
begin
  P := @Buf[0];
  Inc(P, MaxLen);  // wrong stride once Char is 2 bytes
  P^ := #0;
end;

The fix isn’t just recompiling — it’s auditing every PChar, every SizeOf(Char) assumption, and every place you write raw bytes to a file or socket expecting ANSI. If you’re serializing strings to disk or over the wire, do it explicitly:

// Explicit encoding — works the same regardless of default string type
procedure WriteStringUtf8(Stream: TStream; const S: string);
var
  Bytes: TBytes;
  Len: Integer;
begin
  Bytes := TEncoding.UTF8.GetBytes(S);
  Len := Length(Bytes);
  Stream.WriteBuffer(Len, SizeOf(Len));
  if Len > 0 then
    Stream.WriteBuffer(Bytes[0], Len);
end;

I wrote about byte-level string handling in an older format-conversion context in my rtf2xml notes — the same care about explicit encoding applies here.

Deprecated and removed RTL calls

The compiler will flag deprecated calls with a warning, which is helpful, but a few common ones generate warnings that are easy to ignore in a codebase with thousands of them already. Watch for these specifically because their replacements aren’t always a drop-in swap.

StrPas and StrPCopy still compile but are legacy PChar-era helpers. Replace with direct string assignment now that string handles the conversion:

// Old
S := StrPas(SomePCharValue);

// New — direct assignment does the right thing
S := string(SomePCharValue);

TThread.Suspend and TThread.Resume were removed outright in newer versions, not just deprecated. If you have old worker-thread code pausing threads this way, you need to redesign around an event or a TEvent/TMonitor wait instead:

// Old — Suspend/Resume no longer exist
// MyThread.Suspend;

// New — use a signal the thread checks itself
type
  TWorker = class(TThread)
  private
    FPauseEvent: TEvent;
  protected
    procedure Execute; override;
  end;

procedure TWorker.Execute;
begin
  while not Terminated do
  begin
    FPauseEvent.WaitFor(INFINITE);
    // do work
  end;
end;

ShowMessage and message-box calls with old positional parameters sometimes shift behavior across VCL versions — always check the parameter order against the current help file rather than assuming it matches what you remember from Delphi 7.

Component and package issues

Third-party components are the second-biggest time sink after strings. Anything installed as a .dpk package from 2007 is unlikely to have a compatible version for modern Delphi without a paid upgrade or a source patch. Before starting the migration, I make a full inventory of every third-party component in use, and for each one I check three things: does a current version exist, is it still maintained, and does it support the target Delphi version specifically (not just “modern Delphi” generically — vendors often lag one or two releases behind).

Where a component is abandoned, I’ve had better luck forking the source and patching it myself than swapping the whole thing for an unfamiliar replacement — the DFM streaming format is stable enough that old component source usually compiles with minor changes once the string and deprecated-API issues above are fixed.

Speaking of DFM files: if you’re doing any programmatic manipulation of forms outside the IDE, my dfm2xml tool is useful here for diffing form definitions across a migration, since DFM text format itself hasn’t changed much even though the components referenced in it have.

Compiler directives and conditional code

If the codebase already survived one migration (say, from Delphi 5 to Delphi 7), it likely has {$IFDEF} blocks keyed to old version constants. Don’t delete these blindly — audit each one. I keep a small reference table taped to my monitor, honestly, because the version constants are not intuitive:

{$IFDEF VER150} // Delphi 7
{$IFDEF VER185} // Delphi 2007
{$IFDEF VER210} // Delphi 2010
{$IFDEF VER230} // Delphi XE2

For anything targeting current Delphi, prefer feature-testing constants over version constants where they exist, since they survive future upgrades better:

{$IF CompilerVersion >= 23.0} // XE2 and later — Win64 support exists
  {$DEFINE HAS_WIN64}
{$IFEND}

64-bit compilation

If 64-bit is part of the goal, budget separate time for it — it’s not free once your Unicode migration is done. The usual suspects are Integer-sized variables holding pointers, inline assembly (which simply won’t compile for Win64 and needs rewriting or isolating behind a platform-specific unit), and any direct calls into 32-bit-only DLLs or ActiveX controls you don’t control the source of.

// Breaks under Win64 — pointer truncated to 32 bits
var
  Handle: Integer;
begin
  Handle := Integer(SomeObject);  // wrong, use NativeInt or IntPtr

// Fixed
var
  Handle: NativeInt;
begin
  Handle := NativeInt(SomeObject);

A practical migration order

I don’t try to do everything in one pass. The order that’s worked best for me across several projects: first get the project compiling on the target IDE version with ANSI/legacy string handling preserved where possible (some projects let you keep {$STRINGCHECKS OFF} temporarily), fix every compiler warning about deprecated calls one unit at a time, then tackle Unicode string correctness as a dedicated pass with actual testing against non-ASCII input, and only then attempt 64-bit if it’s required. Trying to do all four at once produces a pile of unrelated errors that’s much harder to debug than four smaller, sequential piles.

Conclusion

Upgrading legacy Delphi isn’t really about the compiler — it’s about surfacing every implicit assumption the original code made about string encoding, threading APIs, and component availability, and making each one explicit again. Budget more time for the string and component audit than for the actual recompile; that’s consistently where the real work is.

Leave a Reply

Your email address will not be published. Required fields are marked *