During my recent discussions about upstreaming a new Linux kernel driver, a tool was mentioned that I had not used before: Include What You Use (IWYU).
The idea behind the tool is simple: analyze C/C++ source files and check which headers are actually needed. It can suggest missing includes and point out includes that are not being used.
It is not a mature solution that can blindly modify your code and be finished. The results require review, especially in large projects with their own conventions. However, it can be a useful tool to find hidden dependencies and clean up include lists.
Here I will show you how to use it for Linux kernel source code –and in particular for the IIO subsystem– as an example, but the same approach can be adapted to any C/C++ project.
What is IWYU and why is it useful?
IWYU was originally created at Google to be used on their own projects, and it later became an open source project. It is built on top of Clang, using Clang’s parsing capabilities to understand C and C++ code instead of trying to guess dependencies from text. As you will see, IWYU is tightly coupled to Clang, so that you will have to build it for a specific Clang version.
The main idea is the rule behind the name: Include what you use. If a source file uses a symbol (e.g. a function, a macro or a definition), the header that provides that symbol should be included directly instead of relying on another header including it indirectly. That approach has the advantage of minimizing indirect dependencies that might break things if a header you need is removed from the one you actually included.
The project is hosted on GitHub, and you will find it here.
IWYU mapping files
One important concept before running IWYU is mapping files. IWYU uses .imp files to provide additional information about
relationships between headers. The analysis can detect where a symbol comes from, but large projects
often have abstraction layers. A project may want users to include a
public API header while an internal header provides the actual
implementation. An .imp file allows IWYU to understand these relationships and avoid
suggesting internal headers.
The documentation for creating mappings is available here. It explains how to create mappings for your own project, including mapping symbols to headers and defining relationships between private implementation headers and public headers.
The iio.imp file I am going to use is not currently part of the official Linux kernel
tree. It came from an upstream discussion related to my VEML6031X00
light sensor driver within the IIO subsystem, and you can find it here.
In a nutshell, the relationships between the elements (includes, symbols and references) IWYU finds, and what should be included are described one by one like this:
{ "include": ["\"asm-generic/div64.h\"", "private", "<linux/math64.h>", "public"] }
Here, we are telling IWYU that if it finds a symbol that is actually
defined in a low-level header like asm-generic/div64.h (a private
one), <linux/math64.h> (a public header) should be added instead.
You will have to tailor your own mapping according to your project needs and architecture, but as you can see, it is no rocket science.
Building/Installing IWYU
Some distros alreday ship IWYU, and you can simply install them on your computer. But you have to bear in mind that IWYU is always built for a specific Clang version, and you will need to have to have the right one installed on your computer for it to work properly. A more flexible option is building from sources, which is what I did.
My package manager would have installed IWYU for Clang 17, but my system already had Clang 18 installed:
clang --version
Ubuntu clang version 18.1.3
So let’s build IWYU for that version. First we will clone the repository:
mkdir ~/repos/iwyu
cd ~/repos/iwyu
git clone https://github.com/include-what-you-use/include-what-you-use.git
Then we have to checkout the matching Clang branch:
cd include-what-you-use
git checkout clang_18
IWYU gets build with CMake, so now we have to follow the common steps. First, let’s create the build directory:
cd ..
mkdir build
cd build
Configure:
cmake -G "Unix Makefiles" \
-DCMAKE_PREFIX_PATH=/usr/lib/llvm-18 \
../include-what-you-use
...
CMake Error at /usr/lib/llvm-18/lib/cmake/clang/ClangTargets.cmake:833 (message):
The imported target "clangBasic" references the file
"/usr/lib/llvm-18/lib/libclangBasic.a"
but this file does not exist. Possible reasons include:
* The file was deleted, renamed, or moved to another location.
* An install or uninstall procedure did not complete successfully.
* The installation package was faulty and contained
"/usr/lib/llvm-18/lib/cmake/clang/ClangTargets.cmake"
but not all the files it references.
Call Stack (most recent call first):
/usr/lib/llvm-18/lib/cmake/clang/ClangConfig.cmake:20 (include)
CMakeLists.txt:20 (find_package)
-- Configuring incomplete, errors occurred!
Oops! My first attempt failed because the Clang development libraries were
missing. If you get a similar error, just install the libclang-18-dev
package:
sudo apt install libclang-18-dev
Once you have met the dependencies, it should build without any issues:
-- Configuring done (4.4s)
-- Generating done (0.0s)
-- Build files have been written to: /home/hb/repos/iwyu/build
Now that the build system has been generated, you can invoke it and build the binaries:
make
The binary is now available in:
~/repos/iwyu/build/bin/include-what-you-use
We are not going to call it directly, though. Instead, we are going to use the iwyu_tool.py wrapper to add a compilation database (the typical compile_commands.json file). IWYU can also be invoked directly, but then we would need to provide all the compiler options manually. The compilation database already contains this information for each source file, such as include paths, defines, and compiler flags, so the wrapper can run IWYU using the same build context used by the project. We will need to have access to include-what-you-use anyway, so we’ll add its location to PATH:
export PATH=/home/hb/repos/iwyu/build/bin:$PATH
Running IWYU on a Linux kernel driver
First, we will generate the compilation database. There is a script for that under scripts/clang-tools:
./scripts/clang-tools/gen_compile_commands.py
That script will generate a compile_commands.json file in the root
directory, which will be passed to iwyu_tool.py via -p . (if you are
running the command from the root directory, of course). We will also
indicate include-what-you-use that we have our own mapping by passing
-- -Xiwyu --no_default_mappings -Xiwyu --mapping_file=tools/iio/iio.imp.
Note: the -- separator indicates that the next arguments are for
include-what-you-use and not for the wrapper, and of course you have to
provide the right path to the imp file.
The complete command will look like this:
python3 ~/repos/iwyu/include-what-you-use/iwyu_tool.py \
-p . \
drivers/iio/light/veml6031x00.c \
-- \
-Xiwyu --no_default_mappings \
-Xiwyu --mapping_file=tools/iio/iio.imp
Results
IWYU gave me the following output:
drivers/iio/light/veml6031x00.c should add these lines:
#include <asm/byteorder.h> // for le16_to_cpu, cpu_to_le16
#include <linux/bitops.h> // for BIT, const_test_bit
#include <linux/errno.h> // for EINVAL, EBUSY, ENOMEM
#include <linux/stddef.h> // for NULL, true
#include <linux/sysfs.h> // for attribute_group
#include <linux/types.h> // for __le16, bool, aligned_s64
#include "asm-generic/bitops/builtin-ffs.h" // for ffs
#include "asm-generic/bitops/const_hweight.h" // for hweight8
#include "linux/array_size.h" // for ARRAY_SIZE
#include "linux/compiler-context-analysis.h" // for __must_hold
#include "linux/dev_printk.h" // for dev_err_probe, dev_dbg
#include "linux/device/devres.h" // for devm_add_action_or_reset
#include "linux/iio/buffer.h" // for iio_push_to_buffers_wi...
#include "linux/irqreturn.h" // for irqreturn, irqreturn_t
#include "linux/kconfig.h" // for __ARG_PLACEHOLDER_1
drivers/iio/light/veml6031x00.c should remove these lines:
The full include-list for drivers/iio/light/veml6031x00.c:
#include <asm/byteorder.h> // for le16_to_cpu, cpu_to_le16
#include <linux/bitfield.h> // for FIELD_PREP
#include <linux/bitops.h> // for BIT, const_test_bit
#include <linux/cleanup.h> // for guard, scoped_guard
#include <linux/delay.h> // for fsleep
#include <linux/device.h> // for dev_get_drvdata, devic...
#include <linux/err.h> // for IS_ERR, PTR_ERR
#include <linux/errno.h> // for EINVAL, EBUSY, ENOMEM
#include <linux/i2c.h> // for i2c_client, i2c_get_ma...
#include <linux/iio/events.h> // for IIO_UNMOD_EVENT_CODE
#include <linux/iio/iio-gts-helper.h> // for GAIN_SCALE_ITIME_US
#include <linux/iio/iio.h> // for iio_chan_info_enum
#include <linux/iio/sysfs.h> // for iio_const_attr, IIO_CO...
#include <linux/iio/trigger.h> // for devm_iio_trigger_register
#include <linux/iio/trigger_consumer.h> // for iio_poll_func, iio_tri...
#include <linux/iio/triggered_buffer.h> // for devm_iio_triggered_buf...
#include <linux/interrupt.h> // for devm_request_threaded_irq
#include <linux/limits.h> // for U16_MAX
#include <linux/mod_devicetable.h> // for kernel_ulong_t, i2c_de...
#include <linux/module.h> // for MODULE_DEVICE_TABLE
#include <linux/mutex.h> // for class_mutex_constructor
#include <linux/pm.h> // for pm_ptr
#include <linux/pm_runtime.h> // for pm_runtime_put_autosus...
#include <linux/regmap.h> // for regmap_get_device, reg...
#include <linux/regulator/consumer.h> // for devm_regulator_get_enable
#include <linux/stddef.h> // for NULL, true
#include <linux/sysfs.h> // for attribute_group
#include <linux/types.h> // for __le16, bool, aligned_s64
#include "asm-generic/bitops/builtin-ffs.h" // for ffs
#include "asm-generic/bitops/const_hweight.h" // for hweight8
#include "linux/array_size.h" // for ARRAY_SIZE
#include "linux/compiler-context-analysis.h" // for __must_hold
#include "linux/dev_printk.h" // for dev_err_probe, dev_dbg
#include "linux/device/devres.h" // for devm_add_action_or_reset
#include "linux/iio/buffer.h" // for iio_push_to_buffers_wi...
#include "linux/irqreturn.h" // for irqreturn, irqreturn_t
#include "linux/kconfig.h" // for __ARG_PLACEHOLDER_1
As you can see, it provides one list with the missing headers, another one with the ones that should be removed (empty in this example), and the full list that includes the headers that were already added and should stay. It also justifies why the headers should be included according to your codebase.
The list looks massive indeed, and there is a lot of low-level stuff that you would not usually include as IWYU suggests. That’s where judgement, guidelines and project architecture step in. Nevertheless, you can use these lists as a starting point to build upon.
The workflow is therefore:
- Run IWYU.
- Check why each include is suggested.
- Keep the changes that improve the dependency model.
- Ignore suggestions that introduce unnecessary low level dependencies.
- Adapt your .imp file to get better results next time.
IWYU is a useful additional tool for C and C++ projects, but the final include list should still follow the architecture and conventions of the project.
Now you can start using IWYU in your projects, ideally adding it to your CI/CD pipeline. Happy days!
