Intel C++ Compiler (ICC) 所編譯出來的執行檔,其速度總令人驚豔,且可以近乎無痛地與 Microsoft Visual C++ (MSVC) 的開發經驗整合,是升級開發工具的好選擇。然而,在 Windows 上,ICC 需要搭配隨 ICC 附上的 libmmd.dll,才能夠執行,再加上 ICC 和 MSVC 是可以隨時切換的,因此,在 build system 的設計上,必須要有些特別的設計,以便處理這種 IDE 不會處理到的事。

我們用的 MSVC 是 VC6,需要偵測目前使用的編譯器,是 MSVC 還是 ICC,如果是 ICC 的話,還要偵測是哪一個版本,然後,依據偵測到的版本,作對應的 post-build 的動作。例如,若是使用 ICC 的話,就將正確版本的 libmmd.dll,複製到目標目錄下。

以下假設 ICC 是安裝在 ICPP_COMPILER 這個目錄下,如 %ProgramFiles%\Intel\Compiler\C++\9.1

裝了 ICC 後,compiler/linker 會被換成在 %ICPP_COMPILER%\..\..\ISELECT\bin\ 目錄下的 xicl6.exexilink6.exe,這只是一個只是一個 front-end,ICC 會依據目前的設定,啟動對應的 compiler/linker 進行編譯或連結。

依據隨附 ICC 安裝的文件裡的「Intel® C++ Compiler Documentation » Building Applications » Building Applications with Microsoft Visual Studio 6.0 » Selecting the Intel® C++ Compiler Using makefile」這一篇文章:

... The Makefile Utility provides users with the ability to switch between the Intel C++ Compiler and the Microsoft Visual C++ Compiler without requiring changes to their makefiles. This utility modifies the registry, so exported makefiles only call cl.exe (CPP = cl).

這個所謂的 Makefile Utility 指的就是 %ICPP_COMPILER%\..\..\ISELECT\bin\pickcmd.exe,會修改 registry,依據給定的參數,設定要使用的編譯器。也就是說,這個 utility 就是 MSVC IDE 裡的 Tools » Intel® C++ Compiler Selection Tool 的 command line 版本。

故 ICC 實際上是依據 registry 來決定要使用哪個 compiler/linker 的。所以觀察 registry 可以發現 HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6 下面有這幾組 key:

HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6
    Path64             REG_SZ
    MSVC_binary_dir    REG_SZ    C:\Program Files\Microsoft Visual Studio\VC98\Bin
    Compiler           REG_SZ    91032
    Use_Intel_Cxx      REG_DWORD 0x1
    Languages          REG_DWORD 0x1
    Compiler_List      REG_SZ    91032

HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6\90032
    bin                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.0\IA32\bin
    CHelpFile          REG_SZ    C:\Program Files\Intel\Compiler\C++\9.0\Docs\Main_cls.chm
    Compiler_Interface REG_DWORD 0x0
    Include            REG_SZ    C:\Program Files\Intel\Compiler\C++\9.0\IA32\include
    Languages          REG_DWORD 0x1
    Lib                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.0\IA32\lib
    Name               REG_SZ    9.0

HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6\91032
    bin                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\IA32\bin
    CHelpFile          REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\Docs\Main_cls.chm
    Compiler_Interface REG_DWORD 0x0
    Include            REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\IA32\include
    Languages          REG_DWORD 0x1
    Lib                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\IA32\lib
    Name               REG_SZ    9.1

HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6\91064
    bin                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\Itanium\bin
    Name               REG_SZ    9.1
    Lib                REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\Itanium\lib
    Languages          REG_DWORD 0x1
    Include            REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\Itanium\include
    Compiler_Interface REG_DWORD 0x0
    CHelpFile          REG_SZ    C:\Program Files\Intel\Compiler\C++\9.1\Docs\Main_cls.chm

從這幾組 registry 的結構以及實驗的結果,我們可以看出,ICC 係依據 Use_Intel_Cxx 這一個 key,來決定要不要用 ICC,以及依據 Compiler 這一個 key,決定要用哪一個版本的 ICC。故,如果我們能夠在 command line 下查詢這些 registry key,我們就可以寫出 BATCH 檔,依據設定作對應的 post-build 動作。

我們可以使用 reg.exe 這一支程式[1],在 command line 查詢 registry key,如下:

SHELL> REG QUERY "HKCU\Software\Intel\Intel Tools\Select Compiler\IDE\6" \
                 /v Use_Intel_Cxx

! REG.EXE VERSION 3.0

HKEY_CURRENT_USER\Software\Intel\Intel Tools\Select Compiler\IDE\6
    Use_Intel_Cxx       REG_DWORD       0x1

好,既然 registry 裡的資訊抓得出來了,那剩下的就只剩下 BATCH 檔的程式設計技巧。我們會需要下列技術:

如 UNIX shell 的 backquote 一般,取得程式執行的結果,並存入變數

BATCH 並沒有如 UNIX shell 的 backquote 的功能,也就是另外執行程式,將程式的輸出,轉變成目前的 script 部份內容,進而存入變數。為了取得 registry 裡的值,放入變數以便作後續的處理,我們必須在 BATCH 裡,模擬出 backquote 的功能。

要達到 backquote 的效果,解法很簡單,但非常 tricky。假設我們要達到 set foo `bar` 的效果,我們可以這麼寫:

bar > bar.tmp
SET /p foo=<bar.tmp

SET 使用 /p 選項時,會改向使用者詢問,將得到的字串,設給 foo,而 x 後面的字串,則當作 prompt string 使用。因此,我們先執行 bar,將結果存在暫存檔 bar.tmp 裡,然後用 redirect 將 bar.tmp 的內容,導給 SET /p foo=,如此就可以將 bar 的執行結果,存在 foo 變數裡。

如 UNIX 的 grep 工具一般,在一堆內容裡,找到符合某 pattern 的那些行

因為使用 reg.exe 查詢 registry key 的時候,會額外印出許多不相干的內容,所以我們第一步就是要把顯示值的那一行抓出來。

Windows 的 cmd.exe 有個指令叫 FIND,可以做到如 grep 的效果,只是沒有 grep 那麼強大。假設我們要抓 Use_Intel_Cxx 的值,我們可以這麼下指令:

SHELL> SET ISELECT6=HKCU\Software\Intel\Intel Tools\Select Compiler\IDE\6
SHELL> REG QUERY "%ISELECT6%" /v Use_Intel_Cxx 2>>NUL | FIND "REG_DWORD"
    Use_Intel_Cxx       REG_DWORD       0x1

其中,裡面的 2>>NUL 負責把 stderr 的東西丟掉。

如 UNIX 的 cut 工具一般,依據位置,取出某行文字的部份內容

這個要用到 cmd.exe 變數展開的延伸功能[2]。在 cmd.exe 裡打 HELP SET 我們可以得到下面的說明[3]

您也可以為擴充功能指定子字串。

     %PATH:~10,5%

這將會擴充 PATH 環境變數,然後只使用擴充結果的第 11 個(位移 10)字元
後的 5 個字元如果長度未指定,將會預設為上次使用的變數值。如果數字(位
移或長度)是負數,使用的數字將會是環境變數的長度加上位移或指定長度。

假設含有 Use_Intel_Cxx 的值的那一整行,已經在 ICC_USE_INTEL_CXX 變數裡了,我們就可以用下面的指令,擷取出 0x1 的部份:

SET ICC_USE_INTEL_CXX=%ICC_USE_INTEL_CXX:~28%

很可惜的,Windows 下預設沒有如 sedcutawk 等強大的文字處理工具,而 cmd.exe 的變數延展功能又太過陽春,不能依靠如 regular pattern 的方式處理,因此這裡的數字 28 必須自行計算並寫死。還好,以目前的應用來說,使用固定的數字不會有任何的問題。

有了以上的技術,我們就可以全部組裝起來,達到我們想要的功能。最後完整的程式如下:

@ECHO OFF

SET PREFIX=..\FooProj

IF "%1"==""       GOTO USAGE
IF "%1"=="/?"     GOTO USAGE
IF "%1"=="/h"     GOTO USAGE
IF "%1"=="/help"  GOTO USAGE
IF "%1"=="-h"     GOTO USAGE
IF "%1"=="--help" GOTO USAGE

SET CONFIGURATION=%1
IF "%CONFIGURATION%"=="debug" GOTO CONFIGURATION_CHECK_OK
IF "%CONFIGURATION%"=="release" GOTO CONFIGURATION_CHECK_OK
:CONFIGURATION_CHECK_FAILED
ECHO ERROR: Bad [configuration].
GOTO USAGE
:CONFIGURATION_CHECK_OK

SET ISELECT6=HKCU\Software\Intel\Intel Tools\Select Compiler\IDE\6
SET TMP_FILE=tmp-backquote.txt

REM Determine whether ICC is used
REG QUERY "%ISELECT6%" /v Use_Intel_Cxx 2>>NUL | FIND "REG_DWORD" > %TMP_FILE%
SET /P ICC_USE_INTEL_CXX=<%TMP_FILE%
SET ICC_USE_INTEL_CXX=%ICC_USE_INTEL_CXX:~28%
IF %ICC_USE_INTEL_CXX%==0x1 GOTO POST_BUILD_ICC_BEGIN
IF %ICC_USE_INTEL_CXX%==0x0 GOTO POST_BUILD_MSVC_BEGIN

:POST_BUILD_ICC_BEGIN
ECHO Perform ICC post-build steps:

REM Get ICC version code, which is part of next reg key to query
REG QUERY "%ISELECT6%" /v Compiler 2>>NUL | FIND "REG_SZ" > %TMP_FILE%
SET /P ICC_VER_CODE=<%TMP_FILE%
SET ICC_VER_CODE=%ICC_VER_CODE:~20%

REG QUERY "%ISELECT6%\%ICC_VER_CODE%" /v bin 2>>NUL | FIND "REG_SZ" > %TMP_FILE%
SET /P ICC_BIN_PATH=<%TMP_FILE%
SET ICC_BIN_PATH=%ICC_BIN_PATH:~15%

IF "%CONFIGURATION%"=="debug"   XCOPY "%ICC_BIN_PATH%\libmmdd.dll" %PREFIX%\bin /Y /F /D
IF "%CONFIGURATION%"=="release" XCOPY "%ICC_BIN_PATH%\libmmd.dll"  %PREFIX%\bin /Y /F /D

:POST_BUILD_ICC_END
GOTO POST_BUILD_COMMON_BEGIN

:POST_BUILD_MSVC_BEGIN
ECHO Perform MSVC post-build steps:

:POST_BUILD_MSVC_END
GOTO POST_BUILD_COMMON_BEGIN

:POST_BUILD_COMMON_BEGIN
ECHO Perform common post-build steps:
XCOPY lib\libFoo\include\foo_core.h %PREFIX%\include /Y /F /D
:POST_BUILD_COMMON_END
GOTO QUIT

:USAGE
ECHO Usage: %0 [configuration]
ECHO ----
ECHO [configuration] could be Debug or Release.
GOTO QUIT

:QUIT
IF EXIST %TMP_FILE% DEL /Q %TMP_FILE%
EXIT /B

由於在 Release mode 我們需要的是 libmmd.dll,而在 Debug mode 裡需要的是 libmmdd.dll,所以這個 script 吃一個參數,可以是 debugrelease (大小寫不拘),以決定要 XCOPY 哪一個 DLL。

參考資料:


  1. 內含於 Windows Support Tools 裡。
  2. Windows 的 cmd.exe 有兩種模式:基本模式與擴充模式。擴充模式的內建指令功能較強,可以在 cmd 啟動時加上 /E 選項開啟,或是直接在 registry 裡設定是否開啟的預設值。這裡用的「延伸功能」的字眼,指的就是擴充模式裡才有的變數展開功能。
  3. 說明裡的「擴充」兩字,應是翻譯自英文的「expand」,我覺得這個翻譯不太好,故本文裡都是用「變數展開」的方法描述,和「巨集展開」的用法一樣。Microsoft 的中文翻譯,很多地方都不太傳神。