Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 10 Cycles, Umbrellas, Boundaries, and Enforcement

A dependency cycle can be an architecture defect, a static-linking fact, or a faithful expression of the framework’s semantics. CNA contains all three kinds of pressure, so a flat graph with every edge removed is not the goal. The useful goal is narrower: declare each required edge, keep public closures no wider than necessary, and make accidental edges fail mechanically.

10.1 Three deliberate archive cycles

CNA declares three cycles on both sides so CMake can repeat static archives on the final link line. Each has a concrete runtime reason:

Cycle Forward responsibility Reverse responsibility
Graphics–Input GraphicsDevice updates TouchPanel display metrics and binds Mouse/TextInput to a window. MouseCursor is built from a Texture2D.
Audio–Media FrameworkDispatcher pumps MediaPlayer. Media playback submits songs through the audio mixer.
Graphics–selected renderer Graphics constructs and invokes the one selected renderer factory. Every renderer implements the graphics-owned contract and its header-inlined defaults call back into core logging/validation.

The first two arise from XNA-shaped ownership. Moving MouseCursor or the dispatcher could trade the cycle for a new abstraction, but deleting one edge without relocating the semantic owner merely creates unresolved symbols or a missing update path. The renderer cycle is a compile-time composition seam: CNA does not load a renderer plug-in after startup.

When diagnosing a static link failure, inspect the generated final link line before adding a new PUBLIC dependency. Repeating the two intended archives is different from making every module link the full framework umbrella.

10.2 The three umbrella targets

The root module file exposes three different compositions:

CNA

The historical full-framework INTERFACE target. It carries runtime, framework modules, optional extension modules present in the build, build flags, and the selected renderer. Existing games can continue to link CNA.

cna_cnaext / CNA::CnaExt

The extension composition links CNA::GraphicsExt with CNA::DevicesExt. It is not the declaration marker and it is not a renderer.

cna_build_flags / CNA::BuildFlags

A definitions-only INTERFACE target. It carries the selected CNA_RENDERER_* define plus feature markers such as CNA_CNAEXT, CNA_DEVICES, Draco, and FFmpeg availability. It exports no include directory.

The last point closes a subtle loophole. Older layouts could obtain declarations accidentally through a global include root attached to common flags. At the pinned revision, an include works only when the declaring module is actually reachable.

10.3 Narrow aliases are the architectural surface

A subsystem test or reusable library should link the narrowest CNA alias whose public surface it uses. That makes an undeclared reach visible. A game executable may reasonably use the CNA umbrella; a math-only helper library should prefer CNA::Math, and a content tool should state whether it needs CNA::Content, graphics, or the whole runtime.

PUBLIC and PRIVATE still mean CMake propagation, not conceptual importance. Content’s public headers expose graphics/audio/media types, so those CNA edges are PUBLIC. Its cgltf, stb, SDL, and optional Draco implementation needs are private. A consumer that happens to compile because a private third-party include leaked from its environment has no supported contract.

10.4 Fourteen module probes

On supported native test configurations, CNA registers one small consumer for each framework composition boundary. The probe fleet covers Math, Core, Graphics, Content, Runtime, Input, Audio, Media, Storage, Devices, DevicesExt, GraphicsExt, CnaExt, and Net. Each probe produces two CTest concepts:

  1. 1.

    ModuleProbe_<name> builds/runs a minimal consumer to show that the intended alias is sufficient;

  2. 2.

    ModuleLinkClosure_<name> inspects the emitted link line for required and forbidden libraries.

The probes are excluded on Emscripten and Android. That is a host boundary, not evidence that the module graph changes there. A platform claim must say whether it is source-validated, cross-compiled, or link-inspected on the target.

10.4.1 Two high-value negative contracts

Storage links only its own implementation plus header-level dependencies. Its closure expression forbids any libcna_ archive that is not libcna_storage; even libcna_core must remain absent. PlayerIndex, the CNAEXT marker, and path containment are obtained through CNA::CoreHeaders, not by widening the binary closure.

DevicesExt encodes the opposite separation: project-owned CNA::Devices services must never depend on the XNA-shaped Microsoft::Devices sensor module. Its regex escapes the dot after libcna_devices so that it rejects the base archive without accidentally matching the extension archive itself. These negative assertions are more informative than a full umbrella consumer that would succeed regardless of accidental edges.

Additional closure gates require ENet for Net, require both extension libraries for CnaExt, isolate the Vulkan SDK from other renderer SDKs, and prove that HEADLESS configurations remain free of native graphics SDKs.

10.5 The Ninja caveat: registered does not mean executed

The closure checker reads CMakeFiles/<target>.dir/link.txt. Makefiles generators emit that file, but Ninja normally does not. When the file is missing the script exits with code 77, which CTest records as skipped. CNA’s test preset and hosted workflows use Ninja; therefore the ModuleLinkClosure_* entries are registered there but do not inspect a link command.

The minimal consumer can still compile, and other validators can still pass. Neither result closes the negative-link claim. A report should distinguish:

  • probe source compiled and ran;

  • closure test was registered;

  • closure test inspected a real link line under a compatible generator;

  • forbidden-library mutation was demonstrated to fail.

This is a concrete example of the evidence vocabulary developed in Appendix G: test presence, execution, and oracle engagement are separate coordinates.

10.6 Include reachability and header self-containment

The modularization tools add two source-oriented views. Include reachability resolves public CNA and Microsoft headers through the declared module graph and attributes renderer-gated includes to the selected family. Header self-containment asks whether a public header can be compiled with only its documented prerequisites rather than relying on include order.

These checks answer different questions from the linker probes. A declaration can be reachable while its implementation library is absent; a link can succeed through an overly broad umbrella while a public header still requires an unrelated include to come first. A healthy boundary needs both compile and link evidence.

10.7 Source partition and legacy-root guards

Chapter 9 introduced the physical partition validator. Its complement is a simple root guard that fails configuration if production src/ or include/ directories reappear. Together they prevent two regression shapes:

  • a new translation unit lands outside every owner and is picked up by an ad hoc glob;

  • a global include root quietly restores reachability that module dependencies were meant to control.

The renderer identity validator adds a different invariant: CMake’s accepted strings and the C++ enum must remain the same set. It cannot prove behavior, but it prevents an identity from existing on only one side of the configuration/runtime boundary.

10.8 How to change a boundary safely

A module move or new public dependency is complete only when all affected evidence changes together:

  1. 1.

    move the source/header under its real owner;

  2. 2.

    update the module’s PUBLIC/PRIVATE dependency declarations;

  3. 3.

    update include reachability and header-isolation expectations;

  4. 4.

    add or narrow a consumer probe;

  5. 5.

    inspect the real link artifact with a generator the checker understands;

  6. 6.

    run a negative mutation that demonstrates the gate can fail;

  7. 7.

    update examples, tests, and source citations that encoded the old owner.

An umbrella build going green is the beginning of that argument, not its conclusion. Chapters 68 and 73 show the same distinction at test-suite and hosted-workflow scale.

Type at least three characters. Results are ranked by how often and where the words occur.