位元詩人 [C 語言] 程式設計教學:處理 C 專案相依性 (Dependency)

C 語言函式庫
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

一開始用來練習 C 語法的小型練習程式,通常都只會用到 C 標準函式庫內的函式或巨集,所以不用處理相依性的議題。

然而,當我們要寫應用程式時,很少會只用標準函式庫的功能。這時候,我們需要第三方函式庫來補足標準函式庫不足之處。對於小型任務,還勉勉強強可以自製輪子,但對於 GUI 函式庫或其他大型函式庫,就超出單一開發者的能力了。所以還是要會處理 C 專案的相依性。

直接複製 C 原始碼

對於簡短的 C 原始碼,其實可以跳過函式庫這個分享 C 程式的形式,直接把 C 原始碼拷貝到自己的 C 專案中。C 語言本來就是設計成從原始碼編譯成應用程式的語言。當我們複製外部 C 原始碼時,只是將專案的一部分 C 程式碼委由他人幫我們寫好,省下了重覆實作的時間和心力。

clibs 充分發揮這個想法。該專案提供 clibs(1) 這隻小型程式,用來自動拷貝簡短的 C 程式碼到自己的專案中。clibs 是知名的程式設計師 TJ Holowaychuk 在玩 C 語言時所發想的專案。TJ 也是知名的 Node.js 網頁框架 Express 和 Koa 的作者。雖然 clibs 沒有變成管理 C 相依性的主流,但該專案充分體現 C 專案相依性的本質。

使用函式庫

對於較具規模的函式庫,直接拷貝 C 原始碼就顯得效率不彰。此外,有些函式庫沒有原始碼,只有標頭檔和二進位檔,這在 Windows 上是很常見的情形。所以,我們還是得學習 C 函式庫的使用方式。

C 函式庫分為靜態函式庫 (static library) 和動態函式庫 (dynamic library 或 shared library) 兩種,對於外部程式來說,兩種函式庫的效果相異,所以要根據不同情境選擇不同的格式。

當主程式使用靜態函式庫時,靜態函式庫內的程式會「貼」到主程式中。之後在發佈主程式時,不需要攜帶靜態函式庫。但主程式的體積會變大。此外,有多個應用程式共同一份靜態函式庫時,等同於複製多份函式庫到應用程式中,無形中浪費了一些空間。

由於靜態函式庫是直接「黏」在主程式中,當我們更新靜態函式庫的內容後,必需要將主程式重新編譯一次,再連同主程式一起發佈。由此可知,更新靜態函式庫等同於重新複製一份主程式,比較沒有效率。

相對來說,當主程式使用動態函式庫時,動態函式庫內的程式不會直接複製到主程式中,而是在主程式運行時才和函式庫動態連結。所以,主程式體積會比較少。若有多個應用程式共用同一份動態函式庫時,不需要重覆拷貝程式,不會浪費系統的磁碟空間。

當動態函式庫要更新時,不需要重新拷貝主程式,只要更換動態函式庫的部分即可。所以,更新動態函式庫會比較有效率。

附帶一提,當我們要在其他高階語言呼叫 C 函式庫時,得將 C 程式編譯成動態函式庫,無法使用靜態函式庫。

現在由於磁碟和網路頻寬相對便宜,所以新的執行檔會傾向用靜態連結。像是 Golang 在預設情形下所有的應用程式都使用靜態連結,就反映了這個現象。

函式庫的命名原則

C 函式庫有一定的命名規則,但在不同系統上會有一些差異。本節以假想的函式庫 mylib 為例來說明此規則。其命名規則如下:

函式庫 Unix macOS MinGW MSVC
靜態 libmylib.a libmylib.a libmylib.a mylib.lib
動態 libmylib.so libmylib.dylib libmylib.dll mylib.dll

Windows 有 MSVC (Visual C++) 和 GCC (MinGW) 兩種 C 編譯器系統。MSVC 和 GCC 的函式庫往往不能混用,這是因為 ABI 不相容。

標頭檔不論在那個系統,一律使用 .h 為副檔名。以本例來說,可能會是 mylib.h

我們會在後續的文章中,以範例來展示在各個平台上編譯函式庫的方式,也會整理函式庫在不同平台的命名慣例。

使用函式庫時,原始碼不是必要的,只要有標頭檔和二進位檔即可。自由軟體是一種軟體的思維及授權模式,而非使用 C 函式庫的必需條件。

函式庫和套件的差異

函式庫 (library) 和套件 (package) 在寫程式時都會用到。兩者乍看是相同的東西,實際上有所差異。我們從 C 語言的角度來看兩者的差異。

我們在先前的文章提過,C 函式庫由標頭檔和二進位檔組成。函式庫中會預寫好型別 (type)、函式 (function)、巨集 (macro) 等可呼叫的程式,包裝成機械碼的格式,供外部程式使用。

相對來說,軟體套件透過套件管理軟體來管理,可自動化進行安裝、升級、移除等任務。軟體套件除了包裝函式庫外,也可包裝應用程式。由此可知,函式庫和套件是兩種不同的概念。

C 語言是一個有函式庫但沒有套件的語言。我們在 GNU/Linux 上可見的 DEB 或 RPM,是系統套件管理所用的檔案格式,而非 C 函式庫的格式。所謂的 C 函式庫套件,是由 C 函式庫檔案 (標頭檔、二進位檔) 加上套件的元資料 (metadata) 所組成。套件的元資料對使用函式庫沒有幫助,而是給套件管理軟體用的。

套件管理軟體的種類

承上一節,從套件管理軟體的來源來看,可將套件管理軟體分為以下三類:

  • 系統內設的套件管理軟體,如 Debian 系列的 apt 和 RHEL 系列的 dnf
  • 第三方的套件管理軟體,如 macOS 上的 Homebrew 和 MacPorts
  • 語言特定的套件管理軟體,如 Python 的 pip 和 Node.js 的 npm

C 語言本身沒有內設的套件管理軟體。由於 C 語言和 Unix 或類 Unix 系統緊密結合,這些系統內設的套件管理軟體也可以用來安裝 C 函式庫。

macOS 雖然也是 Unix,但缺乏內設的套件管理軟體。目前程式人熟知的 Homebrew 或 MacPorts 屬於社群方案,而非系統內建的功能。

Windows 上則長期缺乏套件管理軟體。MSYS2 (MinGW) 即包含套件管理軟體 Pacman,但 MSYS2 適用於 GCC 而非 MSVC。近年來微軟主導的 vcpkg 則是 C 和 C++ 的套件管理軟體,適用於 MSVC。但 vcpkg 目前暫時無法調整編譯 C 或 C++ 程式時的參數,靈活性偏低。

系統上的函式庫

Unix 或類 Unix 系統在設計時,即考慮將 C 語言整合進來。所以,系統會有預設的 C 函式庫路徑,像是存放標頭檔的 /usr/include 和存放二進位檔的 /usr/lib 等。所以,不同軟體要共用函式庫相當地容易。

相對來說,Windows 是設計給一般使用者 (end user) 使用的,不著重系統函式庫路徑,也不鼓勵函式庫重用。函式庫通常和執行檔存放在同一個目錄中。Windows 上相當於 /usr/lib 的系統函式庫路徑位於 C:\Windows\System32 ,但甚少開發者會將函式庫放在該路徑。

微軟在這篇文章介紹 Windows 搜尋動態函式庫的方式。不過,將執行檔和函式庫放在同一個目錄中仍然是最方便也最常用的方式。

使用套件管理系統管理相依性

在 Unix 或類 Unix 系統上,使用套件管理系統來安裝 C 函式庫是最簡單的方式。

在 Debian 系列上,函式庫套件會拆分成二進位檔和標頭檔。接續先前的 mylib 的例子。二進位檔的套件名稱可能為 libmylib ,而標頭檔的套件名稱可能為 libmylib-dev

在 RHEL 系列上,採用類似的概念。同樣以 mylib 為例,二進位檔的套件名稱可能為 mylib ,而標頭檔的套件名稱可能為 mylib-devel

為什麼要將函式庫套件拆分呢?這主要是為了節約系統空間。對於已編譯的程式來說,標頭檔不是必要的,只要有二進位檔即可運行。其實標頭檔只是文字檔案,所占空間通常不會太多,但這是一種系統管理的哲學。

有些套件管理系統則不拆分函式庫套件的二進位檔和標頭檔,像 FreeBSD 或 macOS 的套件管理系統就是如此。

手動管理相依性

在 Windows 上,不著重系統函式庫路徑的概念,當我們需要外部函式庫時,得自行指定函式庫路徑或將函式庫手動拷貝到 C 專案中。使用 Visual Studio 或其他 IDE 時,可在 IDE 中設置函式庫路徑。反之,則需將函式庫路徑寫在自動編譯設定檔中。

後來,因應 C++ 開發者的需求,微軟試著在 Windows 中加入套件管理軟體。原先試著使用 NuGet,但是不太成功。現在則是使用 vcpkg 來管理 C 和 C++ 函式庫套件。在筆者寫這篇文章時 (西元 2019 年 12 月),vcpkg 還算是新興專案,能不能獲得成功,得再觀察一陣子。

自行從上游原始碼編譯函式庫

一般情形下,我們會使用預編好的函式庫,因為預編的函式庫立即可用,比較方便。但是,在某些情境下,我們會想自已從上游原始碼編譯函式庫。例如,套件管理系統未提供該函式庫、預編函式庫的版本不夠新、預編函式庫的編譯參數不符需求等。

自行編譯函式庫是一個費時的過程。首先,要自行閱讀該專案的 INSTALL 文件,依照該說明文件的指示建置可用的開發環境。

接著,會根據該函式庫專案提供的自動編譯設定檔來編譯專案。這會依系統而異。

在 Unix 或類 Unix 系統上,通常會使用 Autotools。在專案中,Autotools 會以 configure 命令稿的形式存在。該命令稿的用途在於生成可用的 Makefile 。之後我們就可以使用 Make 來編譯該函式庫專案。

典型的步驟如下:

$ tar xf software-x.y.x.tar.gz
$ cd software-x.y.z
$ ./configure --prefix=/usr/local
$ make
$ sudo make install

將上游專案 tarball 下載後解壓縮。接著將工作目錄移至專案所在的根目錄中。

呼叫 configure 命令稿以生成可用的 Makefileconfigure 會偵測宿主系統是否可用。此外,這時候可額外加入所需的參數。

接著,呼叫 make(1) 來編譯此函式庫專案。根據專案的大小,有時候需花費一些時間。

最後,將編譯好的函式庫安裝到系統路徑。

在 Windows 上,並沒有像 Autotools 這麼一致的工具可用。不同專案會提供不同的設定檔來編譯該專案。有些專案提供 CMake 設定檔,有些專案提供 Make 設定檔,有些專案開發者自行寫 Batch 命令稿給專案使用者。最糟的情形是完全不提供 Windows 下可用的自動編譯設定檔,這時候就要自行編譯 C 原始碼。

(選擇性) 手動管理自行編譯的 C 函式庫

本節承接上一節的內容,談談如何管理自行編譯的 C 函式庫。這個方式只適用於 Unix 或類 Unix 系統,而且不是普遍的主流手法,只是提供一個可行的做法。

傳統上,自行編譯的 C 函式庫會集中在 /usr/local 中,這是為了和系統目錄 /usr 區分。

但 C 函式庫丟入 /usr/local 後,就只是散落在數個目錄的檔案。日後要移除時只能手動逐一移除,而且有可能會遺漏掉一些檔案。因此,我們希望將 C 函式庫集中在同一個目錄中,以利管理。

關鍵在於安裝 C 函式庫時,不要直接將 C 函式庫裝在 /usr/local 中,而要裝在獨立的目錄中。所以,在呼叫 configure 命令稿時,修改參數如下:

$ ./configure --prefix=/usr/local/package/software

當我們使用這樣的參數時, software 會安裝到獨立的目錄 /usr/local/package/software 中。在該目錄內會有獨立的 binincludelibshare 等子目錄。然後,我們再手動建立連結,將函式庫連結回 /usr/local 目錄中。

日後我們要移除該函式庫時,直接移除 /usr/local/package/software 目錄即可。

更進一步來說,我們可以將自行編譯的 C 函式庫放在家目錄中。在呼叫 configure 時,將參數修改如下:

$ ./configure --prefix=$HOME/package/package/software

在這個概念下,自行編譯的函式庫會存放在 $HOME/package 目錄中。實際存放的路徑會位於 $HOME/package/package/software 中,然後再用連結連回 $HOME/package 的子目錄中,像是 $HOME/package/include$HOME/package/lib 。當要編譯 C 程式時,就額外設置函式庫路徑到 $HOME/package/include$HOME/package/lib 等。

已經有套件管理軟體使用這個想法了,像是 macOS 的 Homebrew 和 GNU 的 stow。實際上,要考慮的情境很多,所以這種套件管理軟體不是很好寫,而且在偶然情境下仍可能會出錯。

結語

在本文中,我們介紹了處理 C 相依性的方式。由於這個議題的處理方式會因系統而異,除了閱讀本文外,也要閱讀各個系統的說明文件,才能找出適合自已專案的方式。

關於作者

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

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