前言
在 Objective-C 的發展中,Clang 和 GCC 的腳步並不一致,造成兩者在編譯 Objective-C 程式碼時不完全相容。如果很在意程式碼的編譯器相容性的話,最好針對兩個編譯器都各自編譯一次。
由於編譯和執行程式是很機械性的動作,最好能夠把這個過程自動化。因此,筆者自製了一個 shell 命令稿,用來自動化這個檢查的過程。本文一大部分是在講解這個命令稿,如果對此命令稿的細節沒興趣的讀者,可以略過說明的部分,把這個命令稿當成立即可用的腳本即可。
系統需求
本命令稿本身以 POSIX shell 寫成。除此之外,相依於以下三個軟體:
- GCC
- Clang
- GNUstep
基本上就是在非 Apple 平台上寫 Objective-C 的開發軟體。有在寫 Objective-C 的讀者應該都已經裝好了。
本命令稿在 Ubuntu 18.04 LTS 上測試,但對其他的類 Unix 系統應該也可以使用。
使用 objcheck
這個命令稿叫做 objcheck,本身以 POSIX shell 寫成,在類 Unix 系統上可直接使用。
使用 Git 即可下載:
$ git clone https://github.com/opensourcedoc/objcheck.git
使用前要先把命令稿加上可執行的權限:
$ chmod +x objcheck/objcheck
建議把 objcheck
放在合法的 $PATH
路徑,像是 $HOME/bin
或 /usr/local/bin
等。
如果讀者的 Objective-C 程式碼是單一檔案的話,直接把該檔案當成參數即可:
$ objcheck path/to/file.m
如果讀者的 Objective-C 程式碼分散在多個檔案上,則可參考以下指令:
$ objcheck path/to/*.m
如果想測試靜態函式庫,可參考以下指令:
$ objcheck static path/to/*.m
如果想測試動態函式庫,可參考以下指令:
$ objcheck dynamic path/to/*.m
如果目標程式有帶參數的話,使用 --
隔開:
$ objcheck path/to/*.m -- --opt param_a param_b param_c
我們假定標頭檔和原始碼皆位於同一目錄中。如果讀者的標頭檔位在其他目錄,則要設置環境變數 CFLAGS 。設置的方式同 GCC 的 -I
參數。
輸入 help
子命令可看本命令稿的使用說明:
$ objcheck help
本命令稿可用數個環境變數客製化命令稿的行為,請讀者自行閱讀命令稿的說明。接下來,我們會逐一講解此命令稿所執行的動作。
解說此腳本
本節的內容除了可以了解 objcheck
內部實作外,也可用來觀看 shell scripting 的寫法。如果對 shell scripting 沒興趣的讀者,可以略過本節,只要會使用此腳本即可。
此命令稿的第一個參數是解析選擇性的子命令 (subcommand)。以下的 shell 程式碼會解析子命令:
if [ "$action" = "version" ]; then
version;
exit 0;
fi
# Show license info and exit.
# Show help info and exit.
if [ "$action" = "application" ] || [ "$action" = "static" ] || [ "$action" = "dynamic" ]; then
shift;
fi
if [ "$action" != "application" ] && [ "$action" != "static" ] && [ "$action" != "dynamic" ]; then
action="application";
fi
version
是我們自己寫的 shell 函式,只是負責簡單地印出資料。當子命令是 version
時,印出相關資訊後即離開程式。而 license
和 help
的邏輯大抵上雷同,此處不再秀出程式碼。
此範例程式可編譯的項目有 application
、static
、dynamic
三種,分別代表應用程式、靜態函式庫、動態函式庫。當第一個參數是子命令時,將該參數吃入。除此之外,表示該參數代表別的意義,這時候我們會保留該參數。
由於子命令是選擇性的參數,當命令稿使用者未輸入子命令,我們將其設為預設值 application
。
由於 GNUstep 在類 Unix 系統的位置不統一,我們使用環境變數來客製其標頭檔的位置:
GNUSTEP_INCLUDE=$GNUSTEP_INCLUDE
if [ -z "$GNUSTEP_INCLUDE" ]; then
GNUSTEP_INCLUDE=/usr/include/GNUstep
fi
除此之外,我們還有數個接收環境變數的程式碼,讀者可自行追蹤 objcheck
的原始碼即可知。
為求慎重,我們會在編譯程式前檢查 GCC 是否存在:
$GCC --version > /dev/null
if [ "$?" -ne 0 ]; then
echo "Please install gcc and gobjc";
exit 1;
fi
當 GCC 未安裝時,我們會提示使用者安裝 GCC 並中止此程式。我們也用同樣的邏輯來檢查 Clang 是否已安裝。
我們為了檢查 GNUstep 是否存在,我們寫了一個 Hello World 程式,將程式存到暫存檔後編譯及執行:
helloworld=$(cat << EOF
#import <Foundation/Foundation.h>
int main(void)
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
if (!pool)
return 1;
NSLog(@"Hello World");
[pool drain];
return 0;
}
EOF
)
# Check whether GNUstep exists on the host.
temp="$(mktemp --suffix .m)"
echo "$helloworld" > $temp
$GCC -o $dest $temp -lobjc -lgnustep-base -I $GNUSTEP_INCLUDE -L $GNUSTEP_LIB \
-fconstant-string-class=NSConstantString -fobjc-exceptions
./$dest > /dev/null 2>&1
if [ "$?" -ne 0 ]; then
echo "Please install GNUstep";
rm -f $dest
exit 1;
fi
rm -f $dest
我們用 mktemp
建立一個暫存檔,將 Hello World 程式寫入該暫存檔。然後編譯此程式。若未安裝 GNUstep,即無法編譯此程式,這時會提示使用者安裝 GNUstep 並結束此腳本。
為求慎重,我們用同樣的邏輯寫了一個 Objective-C++ 版本的 Hello World 程式。由於兩段程式碼的邏輯雷同,此處不再列出腳本程式碼。
接著,我們檢查使用者是否有輸入參數:
src="$@"
if [ -z "$1" ]; then
echo "No input file";
exit 1;
fi
當使用者未輸入參數時,後續也無法進行編譯的動作,這時候我們會中止腳本。
最後,實際編譯程式。我們會根據程式的類型使用不同的指令來編譯:
if [ "$action" = "application" ]; then
IS_CPP=0
OBJS=
IS_WRONG=0
for file in "$@"; do
if [ "$file" = "--" ]; then
break;
fi
FILE_EXT=$(check_ext $file | cat)
if [ $FILE_EXT = $unsupported_file_extension ]; then
IS_WRONG=1;
continue;
fi
filename=$(basename $file)
objname="${filename%$FILE_EXT}.o"
OBJS="$objname $OBJS"
FILE_TYPE=$(check_file_type $FILE_EXT | cat)
if [ $IS_WRONG -ne 0 ]; then
continue;
fi
if [ "$FILE_TYPE" = "objective-c" ]; then
$GCC -c -o $objname $file -I $GNUSTEP_INCLUDE $CFLAGS \
-fconstant-string-class=NSConstantString -fobjc-exceptions
elif [ "$FILE_TYPE" = "objective-c++" ]; then
IS_CPP=1
$GPP -c -o $objname $file -I $GNUSTEP_INCLUDE $CXXFLAGS \
-fconstant-string-class=NSConstantString -fobjc-exceptions
elif [ "$FILE_TYPE" = "c" ]; then
$GCC -c -o $objname $file $CFLAGS
elif [ "$FILE_TYPE" = "c++" ]; then
IS_CPP=1
$GPP -c -o $objname $file $CXXFLAGS
fi
done
if [ $IS_WRONG -ne 0 ]; then
exit 1;
fi
if [ $IS_CPP -eq 1 ]; then
LIBS_INTERNAL="-lstdc++"
fi
$GCC -o $dest $OBJS $LIBS_INTERNAL -lobjc -lgnustep-base $LIBS \
-I $GNUSTEP_INCLUDE -L $GNUSTEP_LIB $CFLAGS $LDFLAGS -fconstant-string-class=NSConstantString
args=
is_arg=0
for arg in "$@"; do
if [ "$arg" = "--" ]; then
is_arg=1;
continue;
fi
if [ $is_arg -eq 1 ]; then
args="$args $arg"
fi
done
LD_LIBRARY_PATH=$GNUSTEP_LIB:$LD_LIBRARY_PATH ./$dest $args
elif [ "$action" = "dynamic" ]; then
# Compile a dynamic library.
elif [ "$action" = "static" ]; then
# Compile a static library.
fi
if [ "$?" -ne 0 ]; then
echo "Wrong program compiled by GCC";
clean;
exit 1;
fi
clean;
由於這段程式碼比較長,我們省掉類似的部分。只取其中一段來說明。
Objective-C 專案中實際上可能混合四種程式語言的原始碼,這四種語言分別是 C、C++、Objective-C、Objective-C++。所以,我們要逐一偵測每個檔案,根據不同的檔案類型使用不同的指令把原始碼編譯成目的檔,最後才一口氣把所有的目的檔編譯成執行檔或二進位檔。
偵測檔案類型的動作分為兩步,先偵測檔案結尾再偵測檔案類型。要拆成兩步的原因是 C++ 原始碼可使用多種檔案結尾。
偵測檔案結尾的函式如下:
check_ext () {
case $1 in
*.mm)
echo ".mm"
;;
*.m)
echo ".m"
;;
*.cpp)
echo ".cpp"
;;
*.cxx)
echo ".cxx"
;;
*.cc)
echo ".cc"
;;
*.c)
echo ".c"
;;
*)
echo "Unsupported file: $1" >&2;
echo $unsupported_file_extension;
;;
esac
}
由於 shell 沒有什麼字串函式可用,對於檢查字串來說,case
敘述是窮人版的字串解析工具。$unsupported_file_extension
是我們自訂的字串,用來表示不支援的副檔名。
接著,我們就可以偵測檔案類型:
check_file_type () {
case $1 in
.m)
echo "objective-c";
;;
.mm)
echo "objective-c++";
;;
.c)
echo "c";
;;
.cpp|.cxx|.cc)
echo "c++";
;;
*)
echo "Unknown file extension: $FILE_EXT" >&2;
;;
esac
}
除了偵測檔案類型外,我們還有別的任務要做。包括偵測程式碼是純 C 還是有加入 C++ 的部分、檢查是否有不支援的程式、列出目的檔清單等。請讀者回顧稍早的程式碼片段即可理解。
當檔案有誤時,我們不會編譯最後的執行檔或二進位檔,而會列出所有不支援的檔案。
我們一邊走訪檔案清單,一邊建立目的檔清單。其實我們會把所有的目的碼移到工作目錄,之後要清除時比較容易。所以我們用 basename
去除相對目錄的部分,只保留檔名,並將檔尾去除,改為目地檔的檔尾 .o 。
由於 shell script 沒有字串相接的功能,我們用窮人版的方式來處理:
FILE_EXT=$(check_ext $file | cat)
# Chech $FILE_EXT error.
filename=$(basename $file)
objname="${filename%$FILE_EXT}.o"
OBJS="$objname $OBJS"
這段程式實際在做的事情是變數代換,效果等同於字串相接。
我們的程式會偵測程式碼是否有混入 C++ 原始碼:
if [ $IS_CPP -eq 1 ]; then
LIBS_INTERNAL="-lstdc++"
fi
$GCC -o $dest $OBJS $LIBS_INTERNAL -lobjc -lgnustep-base $LIBS \
-I $GNUSTEP_INCLUDE -L $GNUSTEP_LIB $CFLAGS $LDFLAGS -fconstant-string-class=NSConstantString
若有,會在編譯時加上 linking 參數 -lstdc++
,這樣就可以用 GCC 編譯 C++ 目的檔。
由於執行程式時,有可能會帶參數,所以我們需要把 objcheck
的參數和目標程式的參數分開。以下是假想的指令:
$ objcheck application path/to/*.m -- --help
在 --
之前的參數是 objcheck
的參數,而在 --
之後的參數則是目標程式的參數。--
本身用來區隔兩者,不視為參數。
所以我們要再對參數解析一次,然後執行目標程式:
args=
is_arg=0
for arg in "$@"; do
if [ "$arg" = "--" ]; then
is_arg=1;
continue;
fi
if [ $is_arg -eq 1 ]; then
args="$args $arg"
fi
done
LD_LIBRARY_PATH=$GNUSTEP_LIB:$LD_LIBRARY_PATH ./$dest $args
當參數出現 --
時,代表出現分隔線。之後的參數都要收集起來。執行目標程式時,要將這些參數帶入。
如果編譯、執行目標程式的過程出錯,我們會提示使用者並中止腳本:
if [ "$?" -ne 0 ]; then
echo "Wrong program compiled by GCC";
clean;
exit 1;
fi
clean;
clean
也是我們自己寫的函式,基本上就是清除所有的檔案:
clean () {
rm -f $dest "lib${dest}.a" "lib${dest}.so" *.o;
}
編譯函式的過程大同小異,差別在不需執行函式庫。此外,使用 Clang 編譯的程式碼也和使用 GCC 的大同小異,此處不重覆列出,請讀者自行追蹤腳本程式碼。
結語
Clang 和 GCC 之間的不相容,並不是我們所樂見的情形。但這項議題短時間內也不會消失。現階段最好的做法,就是自行檢查 Objective-C 程式碼的編譯器相容性。
檢查編譯器相容器最簡單的做法,就是用兩個編譯器重覆編譯同一原始碼。但編譯和執行程式是相當機械化的動作,所以我們用 shell 寫了 objcheck
腳本程式來自動化這個過程。
由於 Mac 平台上已有 Clang 和 Cocoa 可用,編譯器相容性不是我們關注的議題,所以我們目前不在 Mac 平台上使用 objcheck
。