3. 条件预处理指示

我们在第 2.2 节 “头文件”中见过Header Guard的用法:

  1. #ifndef HEADER_FILENAME
  2. #define HEADER_FILENAME
  3. /* body of header */
  4. #endif

条件预处理指示也常用于源代码的配置管理,例如:

  1. #if MACHINE == 68000
  2. int x;
  3. #elif MACHINE == 8086
  4. long x;
  5. #else /* all others */
  6. #error UNKNOWN TARGET MACHINE
  7. #endif

假设这段程序是为多种平台编写的,在68000平台上需要定义xint型,在8086平台上需要定义xlong型,对其它平台暂不提供支持,就可以用条件预处理指示来写。如果在预处理这段代码之前,MACHINE被定义为68000,则包含intx;这段代码;否则如果MACHINE被定义为8086,则包含long x;这段代码;否则(MACHINE没有定义,或者定义为其它值),包含#error UNKNOWN TARGET MACHINE这段代码,编译器遇到这个预处理指示就报错退出,错误信息就是UNKNOWN TARGET MACHINE

如果要为8086平台编译这段代码,有几种可选的办法:

1、手动编辑代码,在前面添一行#define MACHINE 8086。这样做的缺点是难以管理,如果这个项目中有很多源文件都需要定义MACHINE,每次要为8086平台编译就得把这些定义全部改成8086,每次要为68000平台编译就得把这些定义全部改成68000。

2、在所有需要配置的源文件开头包含一个头文件,在头文件中定义#define MACHINE 8086,这样只需要改一个头文件就可以影响所有包含它的源文件。通常这个头文件由配置工具生成,比如在Linux内核源代码的目录下运行make menuconfig命令可以出来一个配置菜单,在其中配置的选项会自动转换成头文件include/linux/autoconf.h中的宏定义。

举一个具体的例子,在内核配置菜单中用回车键和方向键进入Device Drivers ---> Network device support,然后用空格键选中Network device support(菜单项左边的[ ]括号内会出现一个*号),然后保存退出,会生成一个名为.config的隐藏文件,其内容类似于:

  1. ...
  2. #
  3. # Network device support
  4. #
  5. CONFIG_NETDEVICES=y
  6. # CONFIG_DUMMY is not set
  7. # CONFIG_BONDING is not set
  8. # CONFIG_EQUALIZER is not set
  9. # CONFIG_TUN is not set
  10. ...

然后运行make命令编译内核,这时根据.config文件生成头文件include/linux/autoconf.h,其内容类似于:

  1. ...
  2. /*
  3. * Network device support
  4. */
  5. #define CONFIG_NETDEVICES 1
  6. #undef CONFIG_DUMMY
  7. #undef CONFIG_BONDING
  8. #undef CONFIG_EQUALIZER
  9. #undef CONFIG_TUN
  10. ...

上面的代码用#undef确保取消一些宏的定义,如果先前没有定义过CONFIG_DUMMY,用#undef CONFIG_DUMMY取消它的定义没有任何作用,也不算错。

include/linux/autoconf.h被另一个头文件include/linux/config.h所包含,通常内核代码包含后一个头文件,例如net/core/sock.c

  1. ...
  2. #include <linux/config.h>
  3. ...
  4. int sock_setsockopt(struct socket *sock, int level, int optname,
  5. char __user *optval, int optlen)
  6. {
  7. ...
  8. #ifdef CONFIG_NETDEVICES
  9. case SO_BINDTODEVICE:
  10. {
  11. ...
  12. }
  13. #endif
  14. ...

再比如drivers/isdn/i4l/isdn_common.c

  1. ...
  2. #include <linux/config.h>
  3. ...
  4. static int
  5. isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
  6. {
  7. ...
  8. #ifdef CONFIG_NETDEVICES
  9. case IIOCNETGPN:
  10. /* Get peer phone number of a connected
  11. * isdn network interface */
  12. if (arg) {
  13. if (copy_from_user(&phone, argp, sizeof(phone)))
  14. return -EFAULT;
  15. return isdn_net_getpeer(&phone, argp);
  16. } else
  17. return -EINVAL;
  18. #endif
  19. ...
  20. #ifdef CONFIG_NETDEVICES
  21. case IIOCNETAIF:
  22. ...
  23. #endif /* CONFIG_NETDEVICES */
  24. ...

这样,在配置菜单中所做的配置通过条件预处理最终决定了哪些代码被编译到内核中。#ifdef#if可以嵌套使用,但预处理指示通常都顶头写不缩进,为了区分嵌套的层次,可以像上面的代码中最后一行那样,在#endif处用注释写清楚它结束的是哪个#if#ifdef

3、要定义一个宏不一定非得在代码中用#define定义,早在第 6 节 “折半查找”我们就见过用gcc-D选项定义一个宏NDEBUG。对于上面的例子,我们需要给MACHINE定义一个值,可以写成类似这样的命令:gcc -c -DMACHINE=8086 main.c。这种办法需要给每个编译命令都加上适当的选项,和第2种方法相比似乎也很麻烦,第2种方法在头文件中只写一次宏定义就可以在很多源文件中生效,第3种方法能不能做到“只写一次到处生效”呢?等以后学习了Makefile就有办法了。

最后通过下面的例子说一下#if后面的表达式:

  1. #define VERSION 2
  2. #if defined x || y || VERSION < 3
  1. 首先处理defined运算符,defined运算符一般用作表达式中的一部分,如果单独使用,#if defined x相当于#ifdef x,而#if !defined x相当于#ifndef x。在这个例子中,如果x这个宏有定义,则把defined x替换为1,否则替换为0,因此变成#if 0 || y || VERSION < 3

  2. 然后把有定义的宏展开,变成#if 0 || y || 2 < 3

  3. 把没有定义的宏替换成0,变成#if 0 || 0 || 2 < 3,注意,即使前面定义了一个变量名是y,在这一步也还是替换成0,因为#if的表达式必须在编译时求值,其中包含的名字只能是宏定义。

  4. 把得到的表达式0 || 0 || 2 < 3像C表达式一样求值,求值的结果是#if 1,因此条件成立。