If you’ve worked on a large Delphi project long enough, you’ve probably been bitten by it: a mysterious access violation at startup, a nil reference where an object should already exist, or an intermittent crash that only appears when you reorder your uses clause. Welcome to the world of Delphi unit initialization order — one of the most subtle and persistent sources of bugs in non-trivial applications.

In this post, I’ll talk about why this problem exists, why it gets worse as projects grow, and introduce a dependency resolution framework I’ve built for kbmMW that replaces hope-based initialization ordering with a proper directed-acyclic-graph solver. I’ll walk through the full design, from the problem it solves down to the topological sort and cycle detection at its core.


The Problem: Who Goes First?

Every Delphi unit can have an initialization section that runs automatically when your application starts. The compiler guarantees that if unit A uses unit B, then B’s initialization runs before A’s. Sounds reasonable, right?

The trouble begins when your dependency graph isn’t a simple tree. Consider a framework like kbmMW with hundreds of units, many of which reference each other through complex uses chains. The actual order in which initialization sections fire depends on the full transitive closure of your uses clauses — and crucially, on which order units appear in those clauses. Move a unit name from position 3 to position 7 in a uses list and you might change the global initialization sequence in ways that ripple across your entire application.

Here’s what this looks like in practice. Suppose kbmMWGlobal needs the RTTI system, the exception framework, a hash function unit, and a lock-free primitives unit to all be ready before it initializes. In standard Delphi, you’d rely on the compiler to figure this out from the uses graph. But the compiler only guarantees direct uses ordering. It doesn’t understand that your RunInitialization procedure semantically depends on code from units that might be three uses hops away. If some intermediate unit changes its own uses list, your carefully tested startup sequence can silently break.

This problem has a few nasty properties:

  • It’s invisible. There’s no compiler warning. Things just stop working.
  • It’s fragile. Adding a unit to a uses clause in an unrelated file can change initialization order elsewhere.
  • It’s hard to reproduce. The order can differ between Debug and Release builds, or between Delphi versions.
  • It gets worse over time. The more units in your project, the more edges in the implicit dependency graph, and the harder it becomes to reason about.

The Usual Workarounds (and Why They Fall Short)

Most Delphi developers eventually develop their own coping strategies. You might guard your initialization code with if FInstance = nil then checks. You might introduce artificial uses clauses purely to force ordering. You might centralize everything in a single “bootstrap” unit. Some teams even resort to manual initialization functions called from the .dpr file, abandoning the initialization mechanism entirely.

These all work to varying degrees, but they share a common weakness: the dependency relationships live in the developer’s head (or scattered across comments), not in the code in an enforceable, verifiable way. The day someone new joins the team and innocently refactors a uses clause, the house of cards can collapse.

A Better Way: Explicit Dependency Declarations

The approach I’ve taken in kbmMW is to make dependencies explicit and let a resolver figure out the correct order at runtime. Instead of relying on compiler-inferred uses ordering, each unit declares what it depends on and provides its own initialization and finalization procedures. A dependency graph is built at startup, topologically sorted, and each unit’s initialization is called in guaranteed-correct order.

Here’s what it looks like from the consuming unit’s perspective. This is from kbmMWGlobal.pas:

procedure RunInitialization;
begin
  kbmMWTime32StartTime := TkbmMWTimeNS.NowUTC;
  kbmMWDebugLevel := mwdlNone;
  kbmMWDebugCS := TkbmMWLock.Create;
  TkbmMWTiming.CalculateTiming;
  kbmMWOSVersion := kbmMWGetOSVersion;
  // ... more setup
end;

procedure RunFinalization;
begin
  kbmMWMarshalledVariantType.Free;
  kbmMWDebugCS.Free;
  kbmMWDebugCS := nil;
end;

initialization
  kbmMWDependsOn('kbmMWGlobal', @RunInitialization, @RunFinalization, [
    'kbmMWExceptions',
    'kbmMWHashFNV1A',
    'kbmMWLockFree',
    'kbmMWRTTI'
  ]);
  kbmMWInitialize('kbmMWGlobal');

finalization
  kbmMWFinalize('kbmMWGlobal');

Read that initialization block. It says: “I am kbmMWGlobal. I depend on these four units. Here are my init and finalize procedures. Now please initialize me.” That’s it. The framework handles the rest — it figures out the correct order, initializes dependencies first, and will yell at you if you’ve created a circular dependency.

The finalization section mirrors this: kbmMWFinalize tears things down in the reverse dependency order, ensuring nothing is freed while something else still needs it.

Inside the Framework: How It Works

The dependency framework lives in kbmMWDependency.pas. Let’s look at its key components.

The Dependency Registry

At the heart of everything is a global dictionary that maps unit names to dependency descriptors:

var
  kbmMWAllDependencies: TkbmMWDependencies = nil;

TkbmMWDependencies is simply a TDictionary<string, IkbmMWDependency>. Each IkbmMWDependency knows its unit name, its init/finalize procedures, what it depends on, and what depends on it. That last part — the reverse edges — is crucial for finalization ordering, which I’ll come back to.

When you call kbmMWDependsOn('kbmMWGlobal', @RunInit, @RunFinalize, ['kbmMWRTTI', ...]), the framework:

  1. Creates (or finds) a dependency node for kbmMWGlobal.
  2. Stores references to RunInitialization and RunFinalization.
  3. For each dependency name, creates (or finds) its node and wires up both directions: kbmMWGlobal depends-on kbmMWRTTI, and kbmMWRTTI is depended-on-by kbmMWGlobal.

This bidirectional linking is what makes finalization work correctly — but we’re getting ahead of ourselves.

Registration vs. Initialization: A Two-Phase Protocol

Notice that the initialization section makes two calls:

initialization
  kbmMWDependsOn('kbmMWGlobal', @RunInitialization, @RunFinalization, [...]);
  kbmMWInitialize('kbmMWGlobal');

The first call registers the dependency. The second call requests initialization. These are deliberately separate because Delphi’s initialization sections can fire in any order. When kbmMWGlobal‘s initialization section runs, the units it depends on might not have registered yet.

The framework handles this gracefully. When kbmMWInitialize is called, the resolver checks whether all dependencies are defined. If they are, it resolves and initializes. If not, it defers — the request is simply noted, and a sweep runs after each kbmMWInitialize call to pick up anything whose dependencies have since become available.

This is the kbmMWTriggerInitializations mechanism: after every explicit kbmMWInitialize call, it iterates over all known units and tries to initialize any that are defined but not yet initialized. This means the system is self-healing with respect to registration order. No matter which unit’s initialization section fires first, eventually all dependencies will be satisfied and everything will be initialized in the correct order.

The Topological Sort

When kbmMWInitialize('kbmMWGlobal') runs and all dependencies are available, it creates a TkbmMWDependencyInitializer and calls ResolveFor. This is where the real graph algorithm lives.

The resolver first builds a subgraph — it walks the dependency tree starting from the requested unit and collects only the nodes that are reachable. There’s no reason to sort the entire universe of units when you only need to initialize one subtree.

Then it performs Kahn’s algorithm for topological sorting:

// Count incoming edges for each node in the subgraph
for u in FSubGraph.Values do
  inDegrees.AddOrSetValue(u.&Unit, u.DependsOnList.Count);

// Seed the queue with nodes that have no dependencies
for u in FSubGraph.Values do
  if inDegrees[u.&Unit] = 0 then
    q.Enqueue(u);

// Process: dequeue, add to result, decrement neighbors
while q.Count > 0 do
begin
  u := q.Dequeue;
  AInitializationOrder.Add(u);
  for p in u.DependedOnList do
  begin
    v := IkbmMWDependency(p);
    if inDegrees.ContainsKey(v.&Unit) then
    begin
      inDegrees.AddOrSetValue(v.&Unit, inDegrees[v.&Unit] - 1);
      if inDegrees[v.&Unit] = 0 then
        q.Enqueue(v);
    end;
  end;
end;

If you haven’t seen Kahn’s algorithm before, the idea is elegant: start with nodes that have zero dependencies (no incoming edges). Process them, then “remove” them from the graph by decrementing the in-degree of everything that depends on them. When a node’s in-degree hits zero, it’s ready. Repeat until everything is processed.

If the output list contains fewer nodes than the subgraph, something couldn’t be resolved — which means there’s a cycle.

Cycle Detection: The Three-Color DFS

When the topological sort detects an unresolvable situation, the framework doesn’t just throw its hands up. It runs a dedicated cycle finder to tell you exactly which units form the cycle.

The cycle finder uses the classic white-gray-black DFS approach:

  • White nodes are unvisited.
  • Gray nodes are currently being visited (in the recursion stack).
  • Black nodes are fully processed.

If during DFS you encounter a gray node, you’ve found a back-edge — a cycle. The finder tracks the current path and extracts exactly the cycle portion:

// Move from white (unvisited) to gray (visiting)
FWhiteSet.Remove(AUnit.&Unit);
FGraySet.Add(AUnit.&Unit, AUnit);
APath.Add(AUnit);

for p in AUnit.DependsOnList do
begin
  d := IkbmMWDependency(p);

  // Gray neighbor = cycle!
  if FGraySet.ContainsKey(d.&Unit) then
  begin
    Result := TList<IkbmMWDependency>.Create;
    j := APath.IndexOf(d);
    for i := j to APath.Count - 1 do
      Result.Add(APath[i]);
    exit;
  end;

  // White neighbor = recurse deeper
  if FWhiteSet.ContainsKey(d.&Unit) then
  begin
    sub := Visit(d, APath);
    if sub <> nil then
    begin
      Result := sub;
      exit;
    end;
  end;
end;

// Done visiting, move to black
FGraySet.Remove(AUnit.&Unit);
FBlackSet.Add(AUnit.&Unit, AUnit);
APath.Delete(APath.Count - 1);

The output is a human-readable path like kbmMWFoo -> kbmMWBar -> kbmMWBaz -> kbmMWFoo, making it immediately clear where the problem is. In debug mode, this even pops up a message box on Windows so you can’t miss it. In production, it calls Halt — a circular dependency is a fatal configuration error, not something to limp along with.

Finalization: The Mirror Image

Finalization is the mirror of initialization. Where initialization follows the DependsOn edges (initialize what I need first), finalization follows the DependedOn edges (finalize what needs me first).

The TkbmMWDependencyFinalizer builds its subgraph by walking DependedOnList instead of DependsOnList, and its Kahn’s algorithm seeds with nodes that have zero DependedOnList count (nothing depends on them, so they’re safe to finalize first). The result is that teardown happens in precisely the reverse order of setup — no use-after-free, no dangling references.

This is why the bidirectional linking during registration matters. Without the reverse edges, you’d need to invert the entire initialization order to get the finalization order. With them, finalization is solved independently, which is more robust and handles cases where the finalization graph might differ from the initialization graph.

Deferred Initialization and the Trigger Sweep

The deferred initialization mechanism deserves special mention because it solves a real chicken-and-egg problem. When Delphi fires initialization sections, the order is compiler-determined. Unit A might register and request initialization before Unit B has even registered. The framework returns gracefully with a “not ready yet” status.

But who retries? After every kbmMWInitialize call, the framework runs kbmMWTriggerInitializations:

procedure kbmMWTriggerInitializations;
var
  allUnitNames: TArray<string>;
  unitName: string;
  d: IkbmMWDependency;
begin
  if kbmMWAllDependencies = nil then
    exit;

  allUnitNames := kbmMWAllDependencies.Keys.ToArray;
  for unitName in allUnitNames do
  begin
    if kbmMWAllDependencies.TryGetValue(unitName, d) then
      if d.IsDefined and not d.IsInitialized then
        kbmMWInitialize(d.&Unit, true);
  end;
end;

Each time a new unit registers and initializes, the sweep picks up any previously deferred units whose dependencies are now satisfied. The AIsTriggeredCall parameter prevents infinite recursion — triggered calls don’t trigger further sweeps.

This means the system converges naturally. As more and more units register through their initialization sections, each sweep resolves a few more, until everything is initialized. No explicit ordering. No manual bootstrapping. The graph takes care of it.

Validation and Debugging

The framework also provides kbmMWValidateDependencies, which you can call after startup to verify that every registered dependency was both fully defined and successfully initialized. If anything slipped through — perhaps a unit was referenced as a dependency but never actually included in the project — you’ll get a clear report.

There’s also a debug mode ({$DEFINE KBMMW_DEBUG_DEPENDENCY}) that logs every registration, initialization, and finalization call through OutputDebugString on Windows or Log.d on mobile. When you’re chasing a startup issue, this trace is invaluable — it shows you the exact order everything happened and where things went wrong.

Why This Matters Beyond Frameworks

While I built this for kbmMW, the pattern applies to any large Delphi project. If you have more than a handful of units with initialization sections that depend on each other, you’re sitting on a potential ordering bug. The risk grows superlinearly with project size — each new unit adds edges to the implicit dependency graph, and the chance of an accidental reordering increases.

The explicit dependency declaration approach has several advantages over the implicit compiler-determined approach:

  • Dependencies are documented in code. When you read a unit’s initialization section, you see exactly what it needs. No guessing, no tracing uses chains.
  • Cycles are detected immediately. Instead of a mysterious access violation three stack frames deep, you get a clear error message naming the exact units involved.
  • Order is deterministic. It doesn’t matter how the compiler traverses the uses graph. The topological sort always produces a valid order.
  • Finalization is automatic. No more wondering if you’re freeing something too early. The reverse-dependency sort handles it.
  • It’s testable. You can call kbmMWValidateDependencies in your test suite to catch configuration errors before they reach production.

The trade-off is a small amount of ceremony — each unit needs to register its dependencies explicitly rather than relying on the implicit uses ordering. In practice, this is a few lines per unit, and the clarity it provides is well worth the effort.

Wrapping Up

Delphi’s unit initialization mechanism is powerful and convenient for small projects. But as your codebase grows, the implicit ordering it provides becomes a source of subtle, hard-to-diagnose bugs. By replacing hope with a proper dependency graph, topological sort, and cycle detection, you get deterministic startup behavior, clear error messages, and correct finalization ordering — all without giving up the convenience of per-unit initialization code.

The full implementation is in kbmMWDependency.pas. It’s under a thousand lines, has no external dependencies beyond Generics.Collections, and solves a problem that has quietly caused headaches in the Delphi community for decades.

If you’ve ever added a comment saying // IMPORTANT: This unit must appear before X in the uses clause, it might be time to try a different approach.

Loading

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.