使用 iwyu 幫忙整理 include 檔案

 Oct 31, 2020  |   c , cpp |   c , cpp

故事是這個樣子的,寫 C/C++ 必定會遇到的就是每個檔案開頭的 #include,這可以帶入其他人寫好的程式碼,最後連結的時候再連結函式庫完成整個程式。
不過大家都知道,程式不是寫完就算了,是會長大跟更新,這時候 include 就會慢慢過時,可能本來需要的 include 現在不需要了,但通常我們不會意識到這點。
另外,在一些 project 上,會出現所謂的組合的標頭檔,例如 Qt 會有 QtGui 標頭檔,內含幾乎所有 Qt 元件的標頭, 不管需要什麼 Qt Widget 只要有 QtGui 都能搞定,但問題就是 include 一個標頭檔會帶進標頭檔裡所有程式碼,巨大的標頭檔如 QtGui 會顯著拖慢編譯速度; 如果只是一兩個檔案當然沒什麼,但在大 project 上,例如我之前改的 qucs, 我把裡面的 QtGui 全部換成專屬的 Qt Widget 之後,單核心編譯時間從原本的344秒加速到245秒,提速 29 %。

這也是為什麼,如果參考 Google 的 cpp coding guide,會看到 Include What You Use 這條準則:

If a source or header file refers to a symbol defined elsewhere, the file should directly include a header file which properly intends to provide a declaration or definition of that symbol. It should not include header files for any other reason.

原始碼檔案和標頭檔所需的符號,都應該引入適當的標頭檔來提供宣告或定義;不能因為其他理由而引入標頭檔。

Do not rely on transitive inclusions. This allows people to remove no-longer-needed #include statements from their headers without breaking clients. This also applies to related headers - foo.cc should include bar.h if it uses a symbol from it even if foo.h includes bar.h.

不可依賴過渡引入。開發者可以隨時移除不需要的引入,又不會破壞客戶端的相依性; 這也適用於相關的標頭檔:即使 foo.h 已經引入 bar.h,foo.cc 還是要引入 bar.h 。

iwyu

想當然爾,要開發人員好好管理 include 就跟要他們好好寫文件一樣,都是件樸實無華而且枯燥的事, 而且動輒幾百幾千個原始碼檔和標頭檔,怎麼可能一個一個去分析 include 有沒有寫對?

這就是這篇文章要介紹的工具了,名稱也非常直覺,就叫做 include what you use 簡稱 iwyu

iwyu 是專門開發來對付這個問題的,它執行起來就像一個 gcc,會去分析原始碼中所需的符號,解析 include 檔案, 確定符號都是直接引入而不是過渡引入;還會幫忙產生修正檔,一次完成引入的修正,簡直是 include 的殺手級工具。
想當初我在改 qucs 的時候,還是使用所謂的土砲法:
先把 include QtGui 用 sed 全部移除,然後用編譯器幫我除錯,它報了錯誤就是告訴我缺了什麼, 再把對應的 include 給加回去,雖然有寫一個 python script 幫我分析,但還是很耗工。

iwyu 花了大量工夫在處理各種例外,然後還是有可能有一堆錯誤 - 畢竟它對付 C/C++,詳情可以參考 iwyu 提供的文件: Why Include What You Use Is Difficult
簡單來說 C/C++ 允許 forward-declaration,讓你在原始碼內宣告並使用某個符號,藉此避免引入宣告的標頭檔; C++ 的 template 又允許你在 template 裡面代入型別進行拓展,要適切處理 include 就變成要適切處理 template,然後就沒完沒了;巨集…又比 template 更糟…。

編譯 iwyu

總之我們要懷著感恩的心來使用 iwyu,它在背後毫不意外是使用 llvm/clang 為基礎,這樣才能利用 llvm 提供的標準化工具來做上述的分析,從頭自己手爆 C/C++ 分析是會死人的。
目前 iwyu 最新版是 0.14,搭配 llvm/clang 10,個人遇到的問題是工作站上…沒有最新的工具,套件都是用同一套映像檔不斷複製, 最新的 llvm 停在 3.9.1…,當然也可以搭配對應的 iwyu 版本來跑就是,但我記得舊版的怎麼樣都 build 不過。

怎麼辦呢?沒有 llvm 10 就自己編啊,這有什麼問題?

git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout llvmorg-10.0.0
mkdir build && cd build
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS=clang ../llvm

再來就是 make 讓它慢慢編譯,我在編譯的後半段,link libclang-cpp.so 的時候會遇到一個神奇的問題:

[ 88%] Linking CXX shared library ../../../../lib/libclang-cpp.so
/bin/ld: cannot find /lib/../lib64/crtn.o: Too many open files
collect2: error: ld returned 1 exit status

這個暫時無解,網路上的解法都是用 ulimit 把開檔案的數量上限調高就可以解掉,但我已經調到 hard limit 的 4096 了還是編不過。

幸好…編到這裡 iwyu 就可以編過了(不要問我,我那時候也是一臉???):

git clone https://github.com/include-what-you-use/include-what-you-use.git
git checkout clang_10
mkdir build && cd build
cmake -G "Unix Makefiles" -DCMAKE_PREFIX_PATH=path_to_llvm/llvm-project/build ../

幸好這個 make 只要一瞬間就結束,終於得到 include-what-you-use 執行檔了。

使用 iwyu

iwyu 如果照文件的建議,是儘量搭配專案本來就有的 Makefile 或 CMake 使用,直接把 CC 或 CXX 代換成 include-what-you-use 加上 -k 編譯, iwyu 就會輸出編譯檔案 include 的修正檔了(加上 -k 是因為 iwyu 回報 include 錯誤會導致編譯停止); 使用 iwyu 時一定要讓參數和真正編譯時儘量相同,才能分析編譯時使用的標頭檔和巨集使用 define 的變數。

這時我就遇到第二個問題:小弟的公司不用 Makefile、CMake 或是任何主流的編譯工具……。

沒關係,再怎麼樣這個工具還是會把編譯工作分解到最小的編譯單位,呼叫 gcc 編譯單個 .c 檔,我就把這個設定給複製出來, 再自己編輯一個 Makefile,設定放在 CFLAGS 裡面,確保 iwyu 能看到我用的標頭檔跟 define 的變數。

CC = include-what-you-use
CXX = include-what-you-use
# 在這裡填入你編譯時標頭檔的位置和巨集名稱
CFLAGS = -I../../include -I … -D …

TARGET = target
SRC = *.c
OBJ = $(patsubst %.c,%.o,$(SRC))

target: $(OBJ)
  echo $@

%.o:%.c
  $(CC) $< $(CFLAGS)

make -k 就會看到大量的 include 修正訊息。

或者好像也有人會這樣跑:

find . -name "*.h" | xargs include-what-you-use <flags>
find . -name "*.c" | xargs include-what-you-use <flags>

但要注意,直接對著 .h 使用 iwyu 可能會有問題,如果 .h 是公開有人使用的話,套用修正可能會把這個標頭拆散,要使用者引入其他的標頭檔,這會破壞使用者的相依性,特別是在大公司裡面其他你管不到的專案可能會用你管的函式庫,此時請小心使用。 註:如果你整個專案都已經沒有過渡引入的話就沒差,但就是還沒做到這樣才需要 iwyu,所以結論就是不要對著公開介面標頭標用 iwyu,要用也要小心。

套用修正與缺陷

依照 iwyu 文件的建議,使用 iwyu 所附的 fix_includes.py 套用修正:

make -k 2> iwyu.out
python fix_includes.py < iwyu.out

如此一來就完成 include 的修正。 雖然說我用完之後還是會遇到一些問題啦,像是出現這樣的 include

#include<data.h>
#include"data.h"

或是還是有些原始碼檔案缺了一些符號必須自己手動補上 include,但整體來說已經比自己手動修正快上不少了; 如果你的程式 - 特別是用到複雜 template 的 C++ - iwyu 可能會在判斷引入上出錯,iwyu 有提供一些對應的 pragma 來處理這個問題, 就請參考相關的使用文件

本文感謝強者我同學在 Google 大殺四方的小新大大指導

comments powered by Disqus