3. 條件預處理指示

我們在第 2.2 節 “標頭檔”中見過Header Guard的用法:

#ifndef HEADER_FILENAME
#define HEADER_FILENAME
/* body of header */
#endif

條件預處理指示也常用於原始碼的配置管理,例如:

#if MACHINE == 68000
    int x;
#elif MACHINE == 8086
    long x;
#else    /* all others */
    #error UNKNOWN TARGET MACHINE
#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的隱藏檔案,其內容類似於:

...
#
# Network device support
#
CONFIG_NETDEVICES=y
# CONFIG_DUMMY is not set
# CONFIG_BONDING is not set
# CONFIG_EQUALIZER is not set
# CONFIG_TUN is not set
...

然後運行make命令編譯內核,這時根據.config檔案生成標頭檔include/linux/autoconf.h,其內容類似於:

...
/*
 * Network device support
 */
#define CONFIG_NETDEVICES 1
#undef CONFIG_DUMMY
#undef CONFIG_BONDING
#undef CONFIG_EQUALIZER
#undef CONFIG_TUN
...

上面的代碼用#undef確保取消一些宏的定義,如果先前沒有定義過CONFIG_DUMMY,用#undef CONFIG_DUMMY取消它的定義沒有任何作用,也不算錯。

include/linux/autoconf.h被另一個標頭檔include/linux/config.h所包含,通常內核代碼包含後一個標頭檔,例如net/core/sock.c

...
#include <linux/config.h>
...
int sock_setsockopt(struct socket *sock, int level, int optname,
                    char __user *optval, int optlen)
{
...
#ifdef CONFIG_NETDEVICES
                case SO_BINDTODEVICE:
                {
			...
                }
#endif
...

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

...
#include <linux/config.h>
...
static int
isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
{
...
#ifdef CONFIG_NETDEVICES
                        case IIOCNETGPN:
                                /* Get peer phone number of a connected
                                 * isdn network interface */
                                if (arg) {
                                        if (copy_from_user(&phone, argp, sizeof(phone)))
                                                return -EFAULT;
                                        return isdn_net_getpeer(&phone, argp);
                                } else
                                        return -EINVAL;
#endif
...
#ifdef CONFIG_NETDEVICES
                        case IIOCNETAIF:
...
#endif                          /* CONFIG_NETDEVICES */
...

這樣,在配置菜單中所做的配置通過條件預處理最終決定了哪些代碼被編譯到內核中。#ifdef#if可以嵌套使用,但預處理指示通常都頂頭寫不縮進,為了區分嵌套的層次,可以像上面的代碼中最後一行那樣,在#endif處用註釋寫清楚它結束的是哪個#if#ifdef

3、要定義一個宏不一定非得在代碼中用#define定義,早在第 6 節 “折半查找”我們就見過用gcc-D選項定義一個宏NDEBUG。對於上面的例子,我們需要給MACHINE定義一個值,可以寫成類似這樣的命令:gcc -c -DMACHINE=8086 main.c。這種辦法需要給每個編譯命令都加上適當的選項,和第2種方法相比似乎也很麻煩,第2種方法在標頭檔中只寫一次宏定義就可以在很多源檔案中生效,第3種方法能不能做到“只寫一次到處生效”呢?等以後學習了Makefile就有辦法了。

最後通過下面的例子說一下#if後面的表達式:

#define VERSION  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,因此條件成立。