Delphi Class Constructors and the Smart Linker: A Silent Trap

AI;DR – for those who can’t be bothered.

Note to self (so I can find it again if I need it):

I just spent an embarrassing amount of time debugging a problem that compiled without errors, produced no warnings, and simply didn’t work at runtime. The culprit? Delphi’s smart linker silently optimizing away code that I very much needed to run.

The Setup

The scenario is a common one: a factory pattern where concrete implementations register themselves with an abstract base class. The base class holds a class variable for the active implementation and exposes a CreateWriter factory method. Concrete implementations call RegisterImplementation to announce their presence. The consuming code just calls CreateWriter and never needs to know which concrete class it gets back. No IFDEFs in the business logic, clean separation of concerns. Textbook stuff.

Two concrete writer implementations exist, selected via a conditional define at project level. Only one is ever compiled into the binary.

The question is: where does the concrete class call RegisterImplementation?

The Obvious Answer (That Doesn’t Work)

Class constructors seem perfect for this. They run automatically during unit initialization, they’re tied to the class they register, and they keep the registration logic right where it belongs. So each concrete implementation got a class constructor:

class constructor TEbene3TdmsWriterNiDdc.Create;
begin
  RegisterImplementation(TEbene3TdmsWriterNiDdc);
end;

The unit is in the DLL’s .dpr uses clause. The project compiles cleanly. The conditional define is set. Everything looks correct.

At runtime: “No TDMS writer implementation registered.”

The class constructor never ran.

The Investigation

The usual suspects were eliminated quickly. The stack trace confirmed the right DLL was loaded. The compiler command line output confirmed the define was set. Both IFDEF paths compiled and ran their self-tests cleanly. The code was correct – it just wasn’t executing.

The breakthrough came from thinking about what makes this pattern special: the concrete class is intentionally never referenced directly. No code instantiates TEbene3TdmsWriterNiDdc by name. No variable is typed to it. No method on it is called explicitly. The only place the class name appears is inside its own class constructor – which is the very code that isn’t running.

And that’s the chicken-and-egg problem.

The Root Cause

Delphi’s smart linker strips out classes that have no direct references in the compiled code. Class constructors are tied to their class, not to their unit. If the linker decides the class is unused, the class constructor goes with it – silently, without any warning or hint.

This is exactly the wrong behavior for self-registration, where the whole point is that nobody references the concrete class directly. The class constructor is supposed to create that reference. But the linker doesn’t see it that way: it sees an unreferenced class and optimizes it away, registration code and all.

What makes this especially nasty:

  • The code compiles with zero errors and zero warnings
  • There is no diagnostic output telling you anything was stripped
  • The pattern looks completely natural and correct
  • It works fine if the class happens to be referenced elsewhere for other reasons

A quick verification confirmed the theory: adding a dummy reference to the class in the unit’s initialization section made the class constructor fire immediately. The linker had been stripping it all along.

The Fix

The fix is simple: use initialization sections instead of class constructors.

// Don't do this - the smart linker may strip it:
class constructor TEbene3TdmsWriterNiDdc.Create;
begin
  TEbene3TdmsWriter.RegisterImplementation(
    TEbene3TdmsWriterNiDdc);
end;

// Do this instead:
initialization
  TEbene3TdmsWriter.RegisterImplementation(
    TEbene3TdmsWriterNiDdc);

The class constructor declaration is removed from the class, and the registration moves to the bottom of the unit. That’s the entire change.

Why This Works

The crucial difference is what the linker considers the “owner” of the code:

  • Initialization sections belong to the unit. If the unit is in the uses clause, its initialization section runs. Period. The linker cannot strip it without removing the entire unit.
  • Class constructors belong to the class. The linker treats the class as a separate entity that can be individually removed if nothing references it, even when the enclosing unit is compiled in.

For self-registration, this distinction is everything. You need code that runs unconditionally when the unit is included – and that’s what initialization sections guarantee.

Takeaway

Never use class constructors for self-registration patterns in Delphi. They look like the right tool for the job, but the smart linker can and will strip them for classes that aren’t directly referenced – which is precisely the situation self-registration creates. Use initialization sections instead. They’ve been in the language since at least Delphi 3, they’re reliable, and the linker can’t touch them.

This was observed with Delphi 12 (Athens) building a Win64 DLL.

Discussion about this in the corresponding post in the international Delphi Praxis forum.