使用GCC编译器生成头文件依赖

复杂的C/C++工程中的头文件比较多,在编写GNU Makefile时,手动指出其源代码文件的头文件依赖关系是不可行的,需要通过编译工具自动生成头文件的依赖关系。GNU GCC的-MXX命令行选项用于生成某个代码文件的依赖关系,通常使用的命令行选项为:

-MT object.o -MP -MMD -MF object.d

其中,-MT用于指定与源文件对应的目标文件名(此处为object.o);-MP指示GCC编译器为依赖的头文件增加伪目标规则;-MMD不会隐含增加-E编译选项,一次编译操作可以生成头文件依赖文件(上面的object.d)和目标文件,且不引入系统头文件的依赖。最后-MF object.d指定生成的依赖文件名称为object.d。这四个选项不会干扰正常的编译、链接流程,仅仅是告知编译器需要生成object.d;也就是说,依赖规则文件object.d在每次编译时都会随目标文件的生成而重新生成。

GNU Make读取依赖文件

依赖文件(或者依赖规则文件)是由GCC编译器在编译链接过程中,通过上面的-MXXX选项生成的。那么第一次编译C/C++代码时,这些依赖文件是不存在的;那么问题就来了:GNU Make如何包含一个尚未生成的规则文件?官方文档给出了解决方案:

-include FILENAMES
sinclude FILENAMES

GNU Make的-include指令可以忽略规则文件时文件不存在的错误。不过,这一功能特点并不能保证依赖文件会被创建。上面第一节中说明了,依赖文件会在编译过程被生成;这利用了GCC编译器的功能特性。某些情况下,还必须强制生成依赖文件,以便下次编译时能包含到。事实上,只要通过include命令包含的头文件,GNU Make都会尝试进行更新,不论其是否已经存在,这一点后面会提到。

笔者编写的简单C/C++代码共有4个文件,hello.c文件内容为:

#include <stdio.h>
#include "header0.h"
#include "header1.h"
int main(int argc, char *argv[])
{
    printf("%s %s!\n", HELLO, WOLRD);
    return 0;
}

header0.h的内容为:

#ifndef AD_HEADER0_H
#define AD_HEADER0_H 1
#define HELLO "hello"
#endif

header1.h的内容为:

#ifndef AD_HEADER1_H
#define AD_HEADER1_H 1
#include "header2.h"
#define WOLRD "world"
#endif

header2.h的内容为:

#ifndef AD_HEADER2_H
#define AD_HEADER2_H 1
/* nothing to define here */
#endif

最后,编写的Makefile脚本如下:

MAKEFLAGS   += -r -R
CC          := gcc
CFLAGS      := -Wall -O2 -fPIC
SOURCES     := $(wildcard *.c)
OBJS        := $(SOURCES:%.c=%.o)
DEPENDS     := $(SOURCES:%.c=%.d)
TARGETS     := $(OBJS) hello

.PHONY: all clean
all: $(TARGETS)
%: %.o
	$(CC) -o $@ $<
%.o %.d: %.c
	$(CC) -c $(CFLAGS) -MT $*.o -MP -MMD -MF $*.d -o $*.o $<

define include_depend
sinclude $(1)
endef
$(foreach depend,$(DEPENDS),$(eval $(call include_depend,$(depend))))
clean:
	rm -rf $(TARGETS)

自动生成的依赖文件

以上源代码文件保存后,执行make可以自动生成依赖文件hello.d

$ make
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
gcc -o hello hello.o
$ cat hello.d
hello.o: hello.c header0.h header1.h header2.h

header0.h:

header1.h:

header2.h:

查看其内容,可以了解到是标准的Makefile依赖规则;此外,该规则还列出了其间接依赖的header2.h头文件。这正是自动生成大型C/C++工程的头文件依赖关系的基本机制。当仅更新头文件,执行make会重新编译生成hello.o:

$ touch header1.h
$ make
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
gcc -o hello hello.o

这一点正是我们需要达到的效果。

强制生成依赖文件

注意到上面编写的Makefile没有将hello.d文件删除。如果在clean目标中,将该文件删除会怎么样?首先看一下不删除hello.d的效果:

$ make clean
rm -rf hello.o hello
$ make clean
rm -rf hello.o hello

接下来修改Makefile:

diff --git a/Makefile b/Makefile
index 5ef6129..74853be 100644
--- a/Makefile
+++ b/Makefile
@@ -24,4 +24,4 @@ endef
 
 $(foreach depend,$(DEPENDS),$(eval $(call include_depend,$(depend))))
 clean:
-       rm -rf $(TARGETS)
+       rm -rf $(TARGETS) *.d

多次执行make clean结果如下:

$ make clean
rm -rf hello.o hello *.d
$ make clean
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
rm -rf hello.o hello *.d
$ make clean
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
rm -rf hello.o hello *.d

出现这样的结果是因为,依赖文件hello.d是Makefile需要包含的文件;而对于包启的脚本文件,GNU Make会强制更新;如果未找到更新的规则,就会报错(此处不报错是因为使用了sinclude),或者不更新(当文件已存在时);官方文档对此做了详细的说明。因此,上面的编译脚本不删除依赖文件hello.d是有正当理由的。

最后,推荐使用CMake,已集成了自动生成头文件依赖文件的功能,可以省去这些烦恼。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐