最近在看一本书《Windows游戏编程大师技巧》 (Tricks of Windows Game Programming Gurus). 第一章给出了一个打砖块小游戏的示例程序. 包括三个文件: blackbox.h, blackbox.cpp和freakout.cpp (600行代码, 对于Windows C++程序来说还好, 没有让我freak out…). blackbox.cpp封装了部分DirectDraw, 提供了一些更傻瓜化的初始化DirectDraw, 画点, 画方框的工具函数. blackbox.h包括了这些函数的声明. freakout.cpp引用了blackbox.h文件, 包括WinMain主函数和主要的游戏逻辑.
How to build?
和我一样还在使用N年前的垃圾电脑的看官们, 多半也不愿意用Visual Studio来挑战自己的耐心. 但是所以让我们选择小米加步枪: GCC (MinGW) + Makefile. 写个简单的makefile例如:
all: freakout.exe
freakout.exe: freakout.o blackbox.o
g++ freakout.o blackbox.o -o stupid.exe -L D:gamedevdx81sdkDXFDXSDKlib -lddraw -mwindows
freakout.o: freakout.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
g++ -c freakout.cpp -o freakout.o -I D:gamedevdx81sdkDXFDXSDKinclude
blackbox.o: blackbox.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
g++ -c blackbox.cpp -o blackbox.o -I D:gamedevdx81sdkDXFDXSDKinclude
Problem?
上面的代码当然有很多问题, 比如”-L <DX lib path>”, “-I <DX inc path>”可以被定义在类似$(LIB), $(INC)的变量里面, 比如我没有按照makefile的习惯写一个clean目标清理生成的东东…不过我的重点是在红色高亮的blackbox.h: 两个.o文件都需要依赖于blackbox.h.
假设我们修改freakout.cpp引用更多的头文件, 每加一条#include “somefile.h”指令, 就需要相应地在makefile里为freakout.o增加一个依赖项somefile.h.
假设我们修改stupid.h文件, 让stupid.h也引用一个头文件someotherfile.h, 那么我们也需要相应地在makefile里为所有依赖stupid.h的.o文件 (在这个例子中是freakout.o和blackbox.o) 增加依赖项someotherfile.h.
所以问题是: 在.cpp和.h被修改的情况下, 怎么维护.o目标文件和.h头文件的依赖关系.
Solution – 自动生成依赖关系
Google一下”makefile 头文件 依赖”会发现大多数编译器都提供了一个选项生成.o目标文件所依赖的文件列表. 比如GCC的”-MM”选项. 运行GCC –MM freakout.cpp blackbox.cpp <库文件和头文件选项>得到输出:
freakout.o: freakout.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
blackbox.o: blackbox.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
所以一个简单的处理依赖关系的办法是把这些编译器生成的依赖关系写入一个文件里, 然后在makefile中用include指令包含这个文件:
CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
all: freakout.exe
freakout.exe: ${OBJ}
${CPP} ${OBJ} -o freakout.exe ${LIB}
<span style="color: #ff0000;"><strong>include depend</strong></span>
# note the symbols: $< and '$@
%.o: %.cpp
${CPP} -c $< -o $@ ${INC}
# generate depend file
<span style="color: #ff0000;"><strong>depend:</strong></span>
<span style="color: #ff0000;"><strong>${CPP} -MM ${OBJ:.o=.cpp} ${INC} > depend</strong></span>
注意红色的部分, depend是GCC生成的包含了依赖关系的文件 (也注意上面的makefile中有”$<”, “$@”这种perl风格的bt匹配字符…) . 有了这样的makefile, 我们就能用两个步骤来build项目:
1) 在需要更新依赖关系的时候 (比如在某个文件里多加了一条#include指令) 运行make depend.
2) 运行make.
Makefile Auto Dependency – 只运行一次make
懒惰是程序员的一大美德, 所以GNU make的手册里提供了一个运行一次make就能更新所有依赖关系并且按照依赖关系build的办法 (http://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites): 与整体引入一个depend目标不同, 我们为每一个.o/.cpp文件引入一个.d依赖关系文件. 依赖关系文件由GCC的”-MM”选项生成, 并且在GCC输出的基础上把自己本身加入到目标列表中. 比如:
# freakout.d
freakout.o <strong><span style="color: #ff0000;">freakout.d</span></strong>: freakout.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
# blackbox.d
blackbox.o <strong><span style="color: #ff0000;">blackbox.d</span></strong>: blackbox.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
注意红色的部份: .d文件被加入到了目标列表, 依赖相关的.h和.cpp文件. 那么怎样自动生成这些.d文件呢? 类似的, makefile:
CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
all: freakout.exe
freakout.exe: ${OBJ}
${CPP} ${OBJ} -o freakout.exe ${LIB}
<span style="color: #ff0000;"><strong>include ${OBJ:.o=.d}</strong></span>
%.o: %.cpp
${CPP} -c $< -o $@ ${INC}
<strong><span style="color: #ff0000;">%.d: %.cpp</span></strong>
rm -f $@ &
${CPP} -MM $< ${INC} > $@.$$ &
insertdfile.exe $< $@.$$ > $@ &
rm -f $@.$$
这个makefile和上一节给出的makefile大致差不多, 不同的是两个红色的部分: 第一部分我们用include指令include相关的所有.d文件; 第二部分定义了生成.d文件的规则 (又是$@, $<符号…): 第一行首先删除原有的.d文件; 第二行运行g++ -MM生成依赖关系写入一个临时文件($@.$$)里; 第三行把.d文件加入到第二行生成的依赖关系中并把最终结果写到.d文件中; 第四行删除临时文件.
P.S. insertdfile.exe是我自己写的一个小程序, 用来把.d文件加入到第二行生成的依赖关系中, 例如把”blackbox.o: blackbox.cpp blackbox.h”转换为”blackbox.o blackbox.d: blackbox.cpp blackbox.h”. 我为什么要自己写一个字符串替换的程序? 因为我没有装sed工具…如果有, 当然用官方推荐的办法, 把第三行换成神奇的符咒: sed ‘s,($*).o[ :]*,1.o $@ : ,g’ < $@.$$ > $@ .
好了, 运行make:
1) make尝试去包含blackbox.d和freakout.d文件, 没有发现, 所以检查有没有规则可以生成这些.d文件, 发现我们%.d: %.cpp的这条规则, 于是运行这条规则生成所有的.d文件并且包含进来.
2) 按照正常规则生成.o文件, 然后生成.exe.
然后我们做一些修改, 比如增加一个stupid.h文件, 然后修改blackbox.h文件以包含stupid.h. 运行make:
1) make把第一个目标all加入到需要生成的目标列表中.
2) make包含blackbox.d和freakout.d文件. 注意在这个过程中这两个.d文件也会被加入到make的需要生成的目标列表中 – 因为可能有规则能更新这两个.d文件.
3) 根据blackbox.d和freakout.d包含的规则: .d文件 (已经被入到了make的需要生成目标列表中) 需要被更新, 于是相应地运行%.d: %.cpp规则更新.d文件: 新加入的 stupid.h文件被加入到两个.d文件的依赖项中.
4) make发现包含的两个.d文件在第2)步中都被更新, 于是重新包含这两个.d文件.
5) make根据在第2)步生成在第3)步被包含进来的规则, 发现stupid.h的日期比两个.o文件的日期都要新, 于是重新生成.o文件.
6) 根据规则生成.exe.