Layering check with Clang
2022-9-25 15:0:0 Author: maskray.me(查看原文) 阅读量:21 收藏

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=A is needed to indicate that the source file is logically part of module A. -fmodule-map-file= is needed to load the source module map to check #include from the main file.
  • An included file from the source module includes a file from another module B. The module map defining B must be loaded by specifying -fimplicit-module-maps or a -fmodule-map-file=.
  • A does not have a use-declaration of B

Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
cat > a.cc <<'eof'


int main() {}
eof
echo 'module A { header "a.h" use B }' > module.map
mkdir -p dir
echo '#include "c.h"' > dir/b.h
echo 'void fc();' > dir/c.h
cat > dir/module.map <<'eof'
module B { header "b.h" use C }
module C { header "c.h" }
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
2
3
4
5
6
7
8
9
10
% clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.map -fmodule-name=A -fimplicit-module-maps a.cc 
a.cc:1:10: error: module A does not depend on a module exporting 'dir/c.h'
#include "dir/c.h"
^
1 error generated.
% clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.map -fmodule-name=A -fmodule-map-file=dir/module.map a.cc
a.cc:1:10: error: module A does not depend on a module exporting 'dir/c.h'
#include "dir/c.h"
^
1 error generated.

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
2
3
4
5
6
% clang -fsyntax-only -fmodules-decluse -fmodule-map-file=module.map -fmodule-name=A a.cc
% clang -fsyntax-only -fmodules-strict-decluse -fmodule-map-file=module.map -fmodule-name=A a.cc
a.cc:1:10: error: module A does not depend on a module exporting 'dir/c.h'
#include "dir/c.h"
^
1 error generated.

Many systems do not ship Clang module map files for C/C++ standard libraries, so -fmodules-strict-decluse is not suitable.

1
2
3
4
5
% clang -fsyntax-only -fmodules-strict-decluse -fmodule-map-file=module.map -fmodule-name=A -fimplicit-module-maps a.cc
a.cc:1:10: error: module A does not depend on a module exporting 'stdio.h'
#include <stdio.h>
^
...

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
2
module B { private header "b.h" use C }
module C { header "c.h" }

Then clang -fsyntax-only -fmodule-map-file=module.map -fmodule-name=A a.cc -fimplicit-module-maps will report an error:

1
2
3
4
a.cc:2:10: error: use of private header from outside its module: 'dir/c.h' [-Wprivate-header]
#include "dir/c.h"
^
1 error generated.

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
2
3
4
5
6
7
8
9
10
11
12
cat > a.cc <<'eof'

int main() { fc(); }
eof
echo 'module A { header "a.h" use B }' > module.map
mkdir -p dir
echo '#include "c.h"' > dir/b.h
echo 'void fc();' > dir/c.h
cat > dir/module.map <<'eof'
module B { header "b.h" use C }
module C { header "c.h" }
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cat > ./a.cc <<'eof'

int main() { fc(); }
eof
echo '#include "c.h"' > ./a.h
echo '#include "c.h"' > ./b.h
cat > ./BUILD <<'eof'
cc_binary(
name = "a",
srcs = ["a.cc", "a.h"],
deps = [":b"],
)

cc_library(
name = "b",
hdrs = ["b.h"],
deps = [":c"],
)

cc_library(
name = "c",
srcs = ["c.cc"],
hdrs = ["c.h"],
)
eof
echo 'void fc() {}' > ./c.cc
echo 'void fc();' > ./c.h
cat > ./WORKSPACE <<'eof'
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
2
3
4
5
6
7
8
9
10
11
% CC=/tmp/RelA/bin/clang bazel build --features=layering_check :a
INFO: Analyzed target //:a (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
ERROR: /tmp/f/BUILD:1:10: Compiling a.cc failed: (Exit 1): clang-16 failed: error executing command /tmp/RelA/bin/clang-16 -U_FORTIFY_SOURCE -fstack-protector -Wall -Wthread-safety -Wself-assign -Wunused-but-set-parameter -Wno-free-nonheap-object -fcolor-diagnostics -fno-omit-frame-pointer '-std=c++0x' ... (remaining 29 arguments skipped)

Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging
In file included from a.cc:1:
./a.h:1:10: error: module //:a does not depend on a module exporting 'c.h'

^
1 error generated.

Here are the generated .cppmap module maps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
% cat bazel-out/k8-fastbuild/bin/a.cppmap
module "//:a" {
export *
private textual header "../../../a.h"
use "//:b"
use "@bazel_tools//tools/cpp:malloc"
use "crosstool"
}
extern module "//:b" "../../../bazel-out/k8-fastbuild/bin/b.cppmap"
extern module "@bazel_tools//tools/cpp:malloc" "../../../bazel-out/k8-fastbuild/bin/external/bazel_tools/tools/cpp/malloc.cppmap"
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"
% cat bazel-out/k8-fastbuild/bin/b.cppmap
module "//:b" {
export *
textual header "../../../b.h"
use "//:c"
use "crosstool"
}
extern module "//:c" "../../../bazel-out/k8-fastbuild/bin/c.cppmap"
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"
% cat bazel-out/k8-fastbuild/bin/c.cppmap
module "//:c" {
export *
textual header "../../../c.h"
use "crosstool"
}
extern module "crosstool" "../../../external/local_config_cc/module.modulemap"

external/local_config_cc/module.modulemap contains files in Clang's default include paths to make -fmodules-strict-decluse happy.


文章来源: https://maskray.me/blog/2022-09-25-layering-check-with-clang
如有侵权请联系:admin#unsafe.sh