位元詩人 [Objective-C] 程式設計教學:以 Docker 容器編譯並執行 Objective-C 程式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

由於 Objective-C 在非蘋果平台上不是主流語言,不一定每個 GNU/Linux 發行版都會有預編好的 Objective-C 編譯器和 GNUstep 函式庫。編譯 Objective-C 編譯器 (GCC 或 Clang) 和 GNUstep 都是複雜且耗時的任務,難以自行完成。

因應這項議題,本文介紹以 Docker 編譯 Objective-C 程式的方式。此外,由於 Docker Hub 中所分享的 GNUstep 映像檔都很舊了,本文會從 Ubuntu 基底映像檔重新建立 GNUstep 開發環境。

前置作業

使用系統的套件管理程式來安裝 Docker 即可。此處以 openSUSE 為例:

$ sudo zypper install docker

裝完後要啟動 Docker 服務:

$ sudo systemctl start docker

如果很常用 Docker 的話,可自行將 Docker 服務加入開機啟動程式中。

在命令列上直接執行 Docker 容器的方式

我們在先前的文章已經提過了,此處不再重覆。

下載 Ubuntu 映像檔

由於 Docker Hub 上現存的 GNUsetp 映像檔都有點年代了,這裡只下載 Ubuntu 基礎映像檔來用:

$ sudo docker pull ubuntu:20.04

這個映像檔對應到 Ubuntu 20.04 LTS,這是最新版本的長期支援版本,應該可以用好一陣子。

在 Docker 容器中建立 GNUstep 開發環境

使用 -it 參數即可以互動模式進入 Docker 容器:

$ sudo docker run -it ubuntu:20.04 /bin/bash

在 Docker 環境中會自動提升至 root,所以可以直接安裝軟體。參考以下指令來建置 GNUstep 開發環境:

# apt update
# apt upgrade
# apt install gcc g++ gobjc gobjc++ clang gnustep-devel
# exit

將更動過的 Docker 容器提交 (commit) 到新的映像檔:

$ sudo docker ps -l -q
28a2937bd585
$ sudo docker commit 28a2937bd585 gnustep

每次的行程號碼都不一樣,請不要照抄。建議在提交映像檔時另起一個新的映像檔名稱,像是本例的 gnustep,原本的 Ubuntu 映像檔可以留著建置其他的開發環境。

以 GCC 編譯和執行 Objective-C 容器

由於執行 Docker 容器的指令很長,每次都手動執行不太經濟,這裡將該指令包裝成 shell 命令稿 $HOME/bin/objc

#!/usr/bin/sh

input=$1

if ! [ -f $input ];
then
    sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "gcc $@"
    exit $?
fi

shift;  # Consume the parameter.

gcc_command="gcc -o a.out ${input} -lobjc -lgnustep-base "
gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"

sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
"cp /app/$input /tmp; cd /tmp;"\
"${gcc_command};"\
"export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"

注意 GNUstep 在 Ubuntu 上的路徑並非系統標準路徑,所以要設置額外的參數。

雖然 Objective-C++ 相對少見,我們也為其寫了一個 shell 命令稿 $HOME/bin/objc++

#!/usr/bin/sh

input=$1

if ! [ -f $input ];
then
    sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "g++ $@"
    exit $?
fi

shift;  # Consume the parameter.

gcc_command="g++ -o a.out ${input} -lobjc -lgnustep-base "
gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"

sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
"cp /app/$input /tmp; cd /tmp;"\
"${gcc_command};"\
"export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"

其實只是把 gcc 改為 g++ 而已。

使用方式如下:

$ objc path/to/source.m

以 Clang 編譯和執行 Objective-C 程式

目前 Objective-C 比較新的語法特性只在 Clang 上有實作,所以本節略為修改先前的 shell 命令稿,改用 clang(1) 來編譯程式。

為了要和 GNUstep 相容,這裡刻意使用 GCC 內附的 libobjc。為了取得 libobjc 的標頭檔路徑,先執行單次 Docker 指令以取得該映像檔內的 GCC 函式庫路徑:

$ sudo docker run gnustep /bin/sh -c "gcc -print-prog-name=cc1 /dev/null"
/usr/lib/gcc/x86_64-linux-gnu/9/cc1

由此可知 GCC 函式庫的標頭檔位於 /usr/lib/gcc/x86_64-linux-gnu/9/include 。只要映像檔沒換,這個路徑是固定的,直接寫死在命令稿內也無妨。

接著修改 objc 命令稿:

#!/usr/bin/sh

input=$1

if ! [ -f $input ];
then
    sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "clang $@"
    exit $?
fi

shift;  # Consume the parameter.

gcc_command="clang -o a.out ${input} -lobjc -lgnustep-base "
gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"

sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
"cp /app/$input /tmp; cd /tmp;"\
"${gcc_command} -I /usr/lib/gcc/x86_64-linux-gnu/9/include;"\
"export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"

用同樣的概念修改 objc++ 命令稿:

#!/usr/bin/sh

input=$1

if ! [ -f $input ];
then
    sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "clang++ $@"
    exit $?
fi

shift;  # Consume the parameter.

gcc_command="clang++ -o a.out ${input} -lobjc -lgnustep-base "
gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"

sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
"cp /app/$input /tmp; cd /tmp;"\
"${gcc_command} -I /usr/lib/gcc/x86_64-linux-gnu/9/include;"\
"export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"

這時候就可以用 Clang 來編譯 Objective-C 程式。

注意事項

本文所提供的 shell 命令稿以內部使用為主。由於本文所提供的命令稿皆未考慮使用 Docker 的安全事項,勿將這些命令稿用於對外公開的服務程式。

本方案的限制

本方案所用的 shell 命令稿只能用在單一 Objective-C 程式碼,而且該 Objective-C 程式無法讀入外部檔案。如果需要更複雜的使用情境,就要自行修改命令稿。

對於有多個檔案的 Objective-C 專案來說,另外寫 Dockerfile 是比較好的選擇。限於篇幅,不在本文說明撰寫 Dockerfile 的方式。

由於 Clang 和 GCC 對 Objective-C 程式碼不相容,請依專案需求自行選擇所需的 shell 命令稿。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。