This article describes some Clang modules features which enforce a more explicit dependency graph. Strict dependency information provides documentation purposes and makes refactoring convenient.
Layering check
-fmodules-decluse
For a #include directive, this option emits an error if the following conditions are satisfied (see clang/lib/Lex/ModuleMap.cpp diagnoseHeaderInclusion):
- The main file is within a module (called "source module", say,
A).-fmodule-name=Ais needed to indicate that the source file is logically part of moduleA.-fmodule-map-file=is needed to load the source module map to check#includefrom the main file. - An included file from the source module includes a file from another module
B. The module map definingBmust be loaded by specifying-fimplicit-module-mapsor a-fmodule-map-file=. Adoes not have a use-declaration ofB
Here is an example:
1 | cat > a.cc <<'eof' |
The following commands lead to an error about dir/c.h. #include "dir/b.h" is allowed because module A has a use-declaration on module B.
1 | % clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.map -fmodule-name=A -fimplicit-module-maps a.cc |
textual header "c.h" triggers the error as well.
If we remove -fmodule-name=A, we won't see an error: Clang does not know a.cc logically belongs to module A.
-fmodules-strict-decluse
This is a strict variant of -fmodules-decluse. If the included file is not within a module, -fmodules-decluse allows the inclusion while -fmodules-strict-decluse reports an error.
Use the previous example, but drop -fimplicit-module-maps and -fmodule-map-file=dir/module.map so that Clang thinks dir/c.h is not within a module.
1 | % clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.map -fmodule-name=A a.cc |
Many systems do not ship Clang module map files for C/C++ standard libraries, so -fmodules-strict-decluse is not suitable.
1 | % clang -fsyntax-only -fmodules-strict-decluse -fmodule-map-file=module.map -fmodule-name=A -fimplicit-module-maps a.cc |
This is an enabled-by-default warning checking use of private headers. The warning is orthogonal to -fmodules-decluse/-fmodules-strict-decluse.
Change dir/module.map by making b.h private:
1 | module B { private header "b.h" use C } |
Then clang -fsyntax-only -fmodule-map-file=module.map -fmodule-name=A a.cc -fimplicit-module-maps will report an error:
1 | a.cc:2:10: error: use of private header from outside its module: 'dir/c.h' [-Wprivate-header] |
To make full power of the layering check features, the source files must have clean header inclusions.
In the following example, a.cc gets dir/c.h declarations transitively via dir/b.h but does not include dir/b.h directly. -fmodules-strict-decluse cannot flag this case.
1 | cat > a.cc <<'eof' |
If #include "dir/b.h" is added due to clean header inclusions, -fmodules-decluse will report an error. Include What You Use describes the benefits of clean header inclusions well, so I will not repeat it here.
In the absence of clean header inclusions, dependency-related linker options (-z defs, --no-allow-shlib-undefined, and --warn-backrefs) can mitigate some brittle build problems.
Bazel
Bazel has implemented the built-in feature layering_check (https://github.com/bazelbuild/bazel/pull/11440) using both -fmodules-strict-decluse and -Wprivate-header.
Bazel generates .cppmap module files from deps attributes. hdrs and textual_hdrs files are converted to textual header declarations while srcs headers are converted to private textual header declarations. deps attributes are converted to use declarations.
When building a target with Clang and layering_check enabled, Bazel passes a list of -fmodule-map-file= (according to the build target and its direct dependencies) and -fmodule-name= to Clang.
1 | cat > ./a.cc <<'eof' |
The following build command gives an error with Clang 16: a.h's inclusion of c.h does not have a corresponding use-declaration. (Older Clang did not check -fmodules-decluse in textual headers: https://reviews.llvm.org/D132779)
1 | % CC=/tmp/RelA/bin/clang bazel build --features=layering_check :a |
Here are the generated .cppmap module maps:
1 | % cat bazel-out/k8-fastbuild/bin/a.cppmap |
external/local_config_cc/module.modulemap contains files in Clang's default include paths to make -fmodules-strict-decluse happy.