This repo uses real mixed C++23 module units, not toy examples. These are the rules that keep them building across MSVC, Clang, Windows SDK headers, and the engine's platform glue.
If a module unit needs classic #include headers before the named module
declaration, it must start with:
module;That opens the global module fragment. Put legacy headers, Windows/X11/OpenGL SDK headers, and preprocessor-driven setup there.
Then declare the actual module:
export module my.module;After that, you are in the named module.
module;
#define EPOCH_SOME_PLATFORM_SWITCH 1
#include <windows.h>
#include <string>
export module epoch.example;
import aengine.platform;
export namespace epoch::example
{
export constexpr bool some_platform_switch = EPOCH_SOME_PLATFORM_SWITCH != 0;
}If you need a global module fragment at all, put module; at the top. Do not
start with random includes and try to retrofit a named module later.
Macros in the global module fragment are useful for controlling textual headers, but they are not the right public API for importers.
If a compile-time switch matters to code outside the module, mirror it after
export module ... with exported constexpr values, enums, or functions.
That is the "double compile-time macro" lesson:
- one side controls old-school headers during preprocessing
- the other side exposes the same decision through the actual module interface
In clean module units, import std; may be fine.
In mixed units that already needed global-fragment includes, it has been safer
in this repo to keep standard library usage textual with classic headers
instead of trying to import std afterward. Toolchain/header interactions have
been fragile there.
Practical rule:
- clean unit:
export module ...; import std; - mixed unit with
module;and classic includes: prefer classic standard headers too
Anything that is macro-heavy, order-sensitive, or hostile to modules belongs before the named module declaration:
windows.h- X11 / GLX headers
- OpenGL loader headers
- old C libraries that expect textual inclusion
After export module ..., prefer:
importfor real engine modules- exported
constexpr/ types / functions for feature visibility - minimal dependency leakage
Converting old header-only code into a module is not just "wrap it in
export module ...". A lot of header-era behavior depends on textual inclusion
and quietly breaks when the code becomes a compiled module boundary.
Header-only code often "worked" because some other include happened to pull in:
<string><vector><algorithm>- platform headers
In a module, be explicit. Import or include what the unit actually needs.
Header-only code often expects the includer to set macros first.
That pattern does not translate cleanly to modules:
- importer macros do not behave like textual header configuration
- global-fragment macros are implementation detail, not exported API
If consumers need configuration, expose it as:
- exported
constexpr - enums
- traits
- explicit build flags wired into the module implementation
Old header-only utilities often relied on:
- "include platform header first"
- "include config before this file"
- "define this switch before include"
If that ordering still matters, keep it in the global module fragment with
module; and make the named module boundary explicit afterward.
Header-only code may rely on static, anonymous namespaces, or inline globals
behaving one way per translation unit.
Once moved into a module, you are compiling an actual unit, not text-pasting into every consumer. Re-check:
- global state lifetime
statichelper visibility- inline variable ownership
- one-time initialization assumptions
If a header-only system used:
#if EPOCH_FEATURE_Xdo not stop halfway when converting it.
The implementation may still need the macro, but the imported interface should publish the result in a stable C++ form:
export constexpr bool feature_x_enabled = EPOCH_FEATURE_X != 0;Some files in this repo still act as compatibility wrappers for older builds. That is fine, but keep the roles clear:
- compatibility header: shim or fallback
- module interface: real ownership surface
Do not let a shim header become the accidental source of truth again.
If the old header-only code depended on textual standard headers, macros, or
hostile SDK interactions, import std; will not automatically rescue the
conversion. In mixed units, it can make the situation worse.
Prefer:
- classic standard headers in the global fragment for mixed/platform-heavy units
import std;only in genuinely clean module units
Use:
- global module fragment for platform setup and toxic headers
- named module for the real interface
- exported constants for feature state
- imported Epoch modules for engine-owned dependencies
Avoid:
- treating macros as exported API
- mixing heavy SDK includes into the named module body
- assuming
import std;will save a unit that already depends on textual setup - assuming header-only behavior survives unchanged after crossing a module boundary
When in doubt:
- Put
module;first. - Put classic includes and macro setup in the global fragment.
- Declare
export module .... - Expose compile-time decisions again through exported module-facing symbols.
- Prefer classic standard headers over
import std;in those mixed units. - When converting header-only code, remove transitive/include-order assumptions instead of preserving them by accident.