Programming Languages · Objects and Modules

Module Initialization Order and Cyclic Dependencies

How linkers, loaders, and language runtimes order module initialisation, why import cycles produce partially initialised modules, and where the guarantees stop.

Every module system has to answer a question that the type system does not: in what order does module-level code run? The answer determines whether a global table is populated before the code that reads it, whether a cycle of dependencies is an error or a source of half-built objects, and whether two builds of the same program can behave differently because the linker reordered something. The mechanisms differ widely between languages, and so do the guarantees they can offer.

What has to happen before main

A translation unit may contain objects with static storage duration that need run-time computation to initialise: a table built by calling a function, a string whose length is computed, an object with a constructor. The compiler partitions these into constant initialisation, which is written into the data section as bytes and costs nothing at load time, and dynamic initialisation, which must be executed.

For dynamic initialisation the compiler emits the initialiser’s address into a special array section — .init_array on the ELF platforms that follow the Itanium ABI, with the older .ctors arrays still recognised — and the dynamic loader walks those arrays, calling each entry in order, before transferring control to the program’s entry point. The order within one shared object is the order the linker placed the entries, which the compiler generally emits in source order within a translation unit, and which the linker may arrange by object file order across translation units. Across shared objects, the loader initialises dependencies before dependents, because a shared object’s initialisers may call into a library it depends on. That dependency order is itself derived from DT_NEEDED entries, so an object loaded lazily and explicitly after startup is initialised when it is loaded, not when the program started; an initialiser that depends on state the main program has not yet set up will see whatever that state is at that moment.

The consequence is a guarantee that stops at the boundary of a translation unit. The order of dynamic initialisation between two translation units in the same program is unspecified, and the order between two independent shared objects that do not depend on each other is unspecified as well. This is the static initialisation order fiasco: a global object whose constructor reads another translation unit’s global may read it before that global has been constructed, and the value it sees is the zero-initialised state, not the intended one.

The standard mitigations are structural rather than syntactic. Replace a namespace-scope object with a function-local static, which is initialised on first execution and, since C++11, is guaranteed to be initialised exactly once even under concurrent first calls; the guarantee costs a guard variable and an acquire load on each access on the common implementations. Or make the initialisation constant, so it is performed at compile time and needs no ordering at all. Or move the dependency into an explicit initialisation function that the program calls in a defined order.

Dependency order as a graph, not a sequence

Languages that own their module system can do better, because they know the dependency graph. Go’s specification requires that package-level variables be initialised in an order determined by their dependencies: if a variable’s initialiser refers to another variable, that other variable is initialised first, and the implementation computes the order from the reference graph rather than from source order. Within a package, all package-level variables are initialised before any init function runs, and the init functions of every imported package run before any variable or init function of the importer. The compiler rejects an import cycle outright, so the graph is acyclic by construction and a topological order always exists.

Go’s approach is available because the dependency graph is explicit in the source and the language forbids cycles. A language that permits mutual imports cannot order modules topologically, and must instead define what a partially initialised module looks like. Python takes that route: the import system places a module object into sys.modules before executing its body, so a module that imports a module currently being initialised receives that partially initialised object. The object exists, its earlier definitions are present, and its later ones are not. Code at the top of the importing module that reads a name defined later in the imported module fails with an attribute error, and the failure depends on which module the program happened to import first — the same source can work when the entry point imports A and fail when it imports B. The visibility rules that decide what the partially initialised object exposes are those of what private fields actually hide, applied one level up from the class to the module.

The partially initialised object is not a bug in the import machinery; it is the only coherent semantics for cycles. Either the cycle is an error, or one side observes an incomplete module. Python chooses the latter and lets the programmer break the cycle by moving the import into a function body, deferring it until the module is fully initialised.

Class initialisation under concurrency

Java separates initialisation of a class from loading of a class and defines exactly when the former happens: on first active use, which includes creating an instance, reading or writing a static field that is not a compile-time constant, and invoking a static method. The procedure is recursive: before a class is initialised, its superclass is initialised, and the initialisation of an interface does not require its superinterfaces unless the interface declares default methods, in which case the superinterfaces are initialised first.

The recursion is protected by a lock per class. A thread that begins initialising a class marks it as being initialised by that thread; another thread that needs the class blocks until the first finishes. A reentrant request from the same thread returns immediately, which is what allows a class’s static initialiser to refer to the class itself without deadlocking. The design admits a deadlock between two threads that initialise two classes in opposite orders, and the specification allows it: a cyclic dependency between class initialisers can hang, and the runtime is not required to detect it.

The visible consequence is that a class’s static initialiser runs at most once per class loader, not once per program. Two class loaders that each load the same class name produce two distinct classes, each with its own initialisation and its own static state. That is deliberate, since loaders exist to isolate namespaces, but it means that “initialised once” is a claim about a class loader and not about a program.

The initialisation order also explains a common failure in libraries that register classes by having each class’s static initialiser add itself to a registry. The registry must already exist when the first class initialises, and nothing in the language orders the two. The design therefore cannot rely on static initialisation and instead scans or registers explicitly at a defined point.

Linker and loader mechanisms

On the ELF platforms, a shared object can request initialisation through the dynamic section entries that name an initialisation function and a termination function, and through the array sections that name many. The loader processes these after relocation, in dependency order, and processes termination functions in reverse order at unload. The ordering across a dependency graph is the loader’s, not the programmer’s, and it is the reason a plugin that registers itself in a constructor works when loaded after its dependencies and fails when loaded before them.

Static archives make the problem worse in a specific way. A linker traditionally extracts only the object files that satisfy an undefined symbol, so an object file whose only purpose is to contribute an initialiser may never be extracted at all. The traditional workaround is to force extraction by referencing a symbol from the file, or to wrap the archive so that all members are included. Linkers that support section garbage collection add a related hazard: an initialiser in a section that nothing references can be discarded, and with it the registration it performed. The mechanism is designed for the common case where initialisation is a side effect of using the code, and it behaves badly when initialisation is the point.

Dynamic loading at run time repeats the whole procedure for a library that was not present at startup. The loader maps the object, applies relocations, runs its initialisers, and only then returns the handle to the caller. A library that depends on symbols from the main program must be linked so those symbols are exported, and the resolution happens before initialisation, so a constructor can call into the program’s functions.

Lazy binding applies to function references made through the procedure linkage table, not to data references: a function call resolved lazily means an undefined symbol may be reported on first call rather than at load time, while a missing data symbol is resolved at load time regardless. dlopen also accepts a flag that requests immediate binding of the loaded object’s references, converting a late failure into an immediate, locatable one.

Designing around the order

Three rules follow from the mechanisms rather than from any particular language.

Initialisation should be a function of data, not a function of order. A module-level table built from literal data, or computed lazily on first access, has no dependency on when it runs. A table built by calling into another module does.

Cycles should be broken by deferral, not by ordering. Where the language permits mutual imports, the reliable pattern is to import inside the function that needs the name, so the module is fully initialised by the time the reference is resolved. Where the language forbids cycles, the graph itself documents the order and no deferral is needed.

Registration should be explicit. A module that must add itself to a registry is better served by a build-time list or an explicit call than by a constructor whose execution depends on whether the linker kept the object file. The explicit form is also visible to a reader, which the constructor is not.

The last point connects back to the boundary discussed in symbol visibility and the one definition rule: an initialiser is a symbol like any other, and whether it runs depends on whether it was linked, whether its section survived, and whether the loader considered its shared object a dependency. The language guarantees the semantics of initialisation; it does not guarantee that the initialisation happens, and the gap between those two is where most of the failures live.

References

  1. The Java Language Specification: Execution, Chapter 12 — Oracle
  2. The Go Programming Language Specification: Program Initialization — The Go project
  3. System V Application Binary Interface, Generic ABI — System V ABI working group
  4. Python Language Reference: The import system — Python Software Foundation