理解 GNU Libtool

這篇文章與『理解 GLib 的單元測試框架』一文有些淵源,由於後者在幾個示例中使用了 libtool 產生庫文件與應用程序文件。segmentfault

田園時代

我要寫一個叫作 foo 的庫,它提供一個什麼也不作的函數。這個庫的頭文件爲 foo.h:bash

#ifndef FOO_H
#define FOO_H

void foo(void);

#endif

foo.c 是這個庫的實現:框架

#include "foo.h"

void foo(void)
{
}

用 gcc 編譯生成共享庫文件 libfoo.so:編輯器

$ gcc -shared -fPIC foo.c -o libfoo.so

若是用 clang,能夠這樣:ide

$ clang -shared -fPIC foo.c -o libfoo.so

若是是在 Windows 環境中(例如 mingw 或 cygwin 之類的環境),能夠這樣:函數

$ gcc -shared -fPIC foo.c -o libfoo.dll

因而,問題就出現了……若是我想讓 foo 庫可以跨平臺運行,那麼我就不得不爲每個特定的平臺提供相應的編譯命令或腳本。這意味着,你必須知道各個平臺在共享庫支持方面的差別及處理方式。這一般是很煩瑣很無趣的事,況且我還沒說構建靜態庫的事。單元測試

這時候,一個 10000 多行的 Bash Shell 腳本 libtool 站了出來,這些破事,我來作!測試

消除環境差別的方法

要有效的消除各個環境差別性,每每有三種辦法。this

第一種辦法是革命……不要懼怕,不是革程序猿的命,而是革環境的命。譬如 Windows(我更願意是 Linux)掃清寰宇,一統天下,那麼環境的差別性也就不存在了。可是,人類的歷史已經證實了這條路是走不通的。由於,一旦某個環境絕對的統治了一切,那麼它下一步要面對的問題就是自身的分裂……整部中國歷史記錄的都是這種事!code

第二種辦法是改良……有一批人仁志士成立了某個團體,頒佈了一些標準,並號召你們都遵照這個標準,別再自行其是。C 語言標準,C++ 標準,scheme 標準……都挺成功的。如今彷佛尚未共享庫或動態連接庫標準。

第三種辦法是和諧——不要懼怕,這裏沒有 GFW——就是認可現實就是這麼個狗血的現實,而後追求和而不一樣

libtool 選擇了第三種辦法。

libtool 的『和』

gcc 編譯生成共享庫的命令能夠粗略的拆分爲兩步——編譯與鏈接:

$ gcc -fPIC foo.c -o libfoo.o  #編譯
$ gcc -shared libfoo.o -o libfoo.so  #鏈接

與之相應,libtool 說,若是你要用 gcc 編譯生成一個共享庫,無論它是在 Linux 裏,仍是在 Solaris,仍是在 Mac OS X 裏,或者是在 Windows 裏(Cygwin 或 MinGW),可使用一樣的命令:

$ libtool --tag=CC --mode=compile gcc -c foo.c -o libfoo.lo # 編譯
$ libtool --tag=CC --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la  # 鏈接

彷佛 libtool 把問題弄得更復雜了!不過,仔細觀察一下,能夠發現一些規律。好比這兩個命令的前面一半都是:

$ libtool --tag=CC --mode=

--tag 選項用於告訴 libtool 要編譯的庫是用什麼語言寫的,CC 表示 C 語言。libtool 目前支持如下語言:

語言                Tag 名稱
---------------------------
C                   CC
C++                 CXX
Java                GCJ
Fortran 77          F77
Fortran             FC
Go                  GO
Windows Resource    RC
---------------------------

--mode 選項用於設定 libtool 的工做模式。上面的例子中,--mode=compile 就是告訴 libtool 要作的工做是編譯,而 --mode=link 就是鏈接。

libtool 支持 7 種模式,除了上述兩種模式以外,還有執行、安裝、完成、卸載、清理等模式。每一個模式都對應於庫的某個開發階段。這 7 種模式抽象了大部分平臺上的庫的開發過程。這是 libtool 和而不一樣的第一步:庫開發過程的抽象

下面來看編譯過程,當 libtool 的 --mode 選項設爲 compile 時,那麼隨後即是具體的編譯命令,本例中是 gcc -c foo.c -o libfoo.lo。這條 gcc 編譯命令,會被 libtool --tag=CC --mode=compile 變換爲:

$ gcc -c foo.c  -fPIC -DPIC -o .libs/libfoo.o
$ gcc -c foo.c -o libfoo.o >/dev/null 2>&1

注意,注意,注意!事實上,libtool 命令中的 gcc -c foo.c -o libfoo.lo,並不是真正的 gcc 的編譯命令(gcc 輸出的目標文件默認的擴展名是 .o 而非 .lo),它只是 libtool 對編譯器工做方式的一種抽象。在 libtool 看來,它所支持的編譯器,都應該這樣工做:

$ 編譯器 -c 源文件 -o 目標文件

若是 libtool 所支持的編譯器並不支持 -c-o 選項,那麼 libtool 也會想辦法讓它們像這樣工做!這是 libtool 和而不一樣的第二步:庫編譯過程的抽象

下面觀察一下執行 libtool 命令先後文件目錄的變化。假設 foo.h 與 foo.c 位於 foo 目錄,而且 foo 目錄裏只有這兩個文件:

$ cd foo
$ tree -a
.
├── foo.c
└── foo.h

0 directories, 2 files

如今,執行 libtool 編譯命令:

$ libtool --tag=CC --mode=compile gcc -c foo.c -o libfoo.lo

而後再查看一下 foo 目錄:

$ tree -a
.
├── foo.c
├── foo.h
├── libfoo.lo
├── libfoo.o
└── .libs
    └── libfoo.o

1 directory, 5 files

執行 libtool 命令後,多出來一個隱藏目錄 .libs,以及三份文件 libfoo.o, .libs/libfoo.o, libfoo.lo。有點詭異的就是有兩份 libfoo.o 文件,雖然它們位於不一樣的目錄,可是它們的內容相同嗎?libfoo.lo 文件說,它們不相同。由於 libfoo.lo 是一份人類可讀的文本文件,用文本編輯器打開它,能夠看到如下內容:

# libfoo.lo - a libtool object file
# Generated by libtool (GNU libtool) 2.4.6
#
# Please DO NOT delete this file!
# It is necessary for linking the library.

# Name of the PIC object.
pic_object='.libs/libfoo.o'

# Name of the non-PIC object
non_pic_object='libfoo.o'

位於 foo/.libs 目錄中的 libfoo.o,是 PIC 目標文件,而位於 foo 目錄中的 libfoo.o 則是非 PIC 目標文件。在 gcc 看來,PIC 目標文件就是共享庫的目標文件,而非 PIC 目標文件就是靜態庫的目標文件。也就是說,libtool 的目標不只僅要生成共享庫文件,也要生成靜態庫文件。這是 libtool 和而不一樣的第三步:目標文件的抽象

接下來,再執行如下 libtool 的鏈接命令:

$ libtool --tag=CC --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la

從『形狀』上來看,這條命令與

$ gcc -shared libfoo.o -o libfoo.so

類似,libfoo.lo 對應 libfoo.o,而 libfoo.la 對應 libfoo.so。事實上就是這樣對應的。 libfoo.lo 是對 libfoo.o 的抽象,而 libfoo.la 是對 libfoo.so 的抽象。libfoo.lo 抽象的是共享庫與靜態庫的目標文件,而 libfoo.la 抽象的就是共享庫與靜態庫。libfoo.la 也是人類可讀的。用文本編輯器打開 libfoo.la 文件,能夠看到:

# libfoo.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6
#
# Please DO NOT delete this file!
# It is necessary for linking the library.

# The name that we can dlopen(3).
dlname='libfoo.so.0'

# Names of this library.
library_names='libfoo.so.0.0.0 libfoo.so.0 libfoo.so'

# The name of the static archive.
old_library='libfoo.a'

# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''

# Libraries that this one depends upon.
dependency_libs=''

# Names of additional weak libraries provided by this library
weak_library_names=''

# Version information for libfoo.
current=0
age=0
revision=0

# Is this an already installed library?
installed=no

# Should we warn about portability when linking against -modules?
shouldnotlink=no

# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''

# Directory that this library needs to be installed in:
libdir='/usr/local/lib'

文件內容太多,要關注的內容是:

# The name that we can dlopen(3).
dlname='libfoo.so.0'

# Names of this library.
library_names='libfoo.so.0.0.0 libfoo.so.0 libfoo.so'

# The name of the static archive.
old_library='libfoo.a'

Directory that this library needs to be installed in:
libdir='/usr/local/lib'

顯然,libfoo.la 包含了 libtool 生成的(實際上是 gcc 生成的)共享庫與靜態庫信息,而且它還包含了一個 libdir 變量。這個變量的值,顯然是 libtool 的鏈接命令中的 -rpath /usr/local/lib 設定的。libdir 表示 libfoo.la, libfoo.so.*, libfoo.a 等文件最終都應該放到 /usr/local/lib 目錄。

下面看一下 foo 目錄中的文件變化:

$ tree -a
.
├── foo.c
├── foo.h
├── libfoo.la
├── libfoo.lo
├── libfoo.o
└── .libs
    ├── libfoo.a
    ├── libfoo.la -> ../libfoo.la
    ├── libfoo.lai
    ├── libfoo.o
    ├── libfoo.so -> libfoo.so.0.0.0
    ├── libfoo.so.0 -> libfoo.so.0.0.0
    └── libfoo.so.0.0.0

1 directory, 12 files

這就是 libtool 三步抽象的全部成果,libfoo.a 與含有 .so 的那些文件,就是最終生成的靜態庫與共享庫文件,而 libfoo.la 是它們的抽象。

注意,還有一個 libfoo.lai 文件,它是一個臨時文件,當咱們使用 libtool 將 foo 庫安裝到 /usr/local/lib 目錄時,它就變成了 libfoo.la。其實,libfoo.la 與 libfoo.lai 的區別是,前者的內容中有一個 installed 變量,它的值是 no,而在後者的內容中,這個變量的值是 yes。能夠將此刻的 libfoo.la 視爲安裝前的庫的抽象,而將 libfoo.lai 視爲安裝後的庫的抽象。

對未安裝的庫進行抽象,有什麼用?便於在庫的開發過程當中對其進行單元測試。

庫的測試

爲了顯得不那麼業餘,我須要對 foo 目錄中的文件進行一些變更,變更後的目錄結構以下:

$ tree -a
.
├── lib
│   ├── foo.c
│   └── foo.h
└── test

2 directories, 2 files

就是將 foo.h 與 foo.c 放到 lib 目錄中,另外新建了一個 test 目錄。我要在 test 目錄中創建測試程序,即 test.c,其內容以下:

#include <foo.h>

int main(void)
{
        foo();
        return 0;
}

而後使用 libtool 從新編譯生成庫文件:

$ cd lib
$ libtool --tag=CC --mode=compile gcc -c foo.c -o libfoo.lo
$ libtool --tag=CC --mode=link gcc libfoo.lo -rpath /usr/local/lib -o libfoo.la

如今,foo 目錄結構變成:

$ cd .. # 返回 foo 目錄,由於剛纔是在 foo/lib 目錄裏
$ tree -a
.
├── lib
│   ├── foo.c
│   ├── foo.h
│   ├── libfoo.la
│   ├── libfoo.lo
│   ├── libfoo.o
│   └── .libs
│       ├── libfoo.a
│       ├── libfoo.la -> ../libfoo.la
│       ├── libfoo.lai
│       ├── libfoo.o
│       ├── libfoo.so -> libfoo.so.0.0.0
│       ├── libfoo.so.0 -> libfoo.so.0.0.0
│       └── libfoo.so.0.0.0
└── test
    └── test.c

下面,編譯測試程序,即編譯 test.c:

$ cd test
$ libtool --tag=CC --mode=compile gcc -I../lib -c test.c
$ libtool --tag=CC --mode=link gcc ../lib/libfoo.la test.lo -o test

執行 test 程序:

$ ./test

結果什麼也不顯示,這是正確的。由於 foo() 函數原本就是什麼也不作的函數。

再看一下 foo 的目錄結構的變化:

$ cd ..  # 由於剛纔在 foo/test 目錄中
$ tree -a
.
├── lib
│   ├── foo.c
│   ├── foo.h
│   ├── libfoo.la
│   ├── libfoo.lo
│   ├── libfoo.o
│   └── .libs
│       ├── libfoo.a
│       ├── libfoo.la -> ../libfoo.la
│       ├── libfoo.lai
│       ├── libfoo.o
│       ├── libfoo.so -> libfoo.so.0.0.0
│       ├── libfoo.so.0 -> libfoo.so.0.0.0
│       └── libfoo.so.0.0.0
└── test
    ├── .libs
    │   ├── test
    │   └── test.o
    ├── test
    ├── test.c
    ├── test.lo
    └── test.o

4 directories, 18 files

結果,在 test 目錄中生成了可執行文件 test,可是 test 目錄也有個 .libs 目錄,而這個 .libs 目錄裏也包含了一份可執行文件 test……這是 libtool 的第四部抽象:可執行文件的抽象。結果,在 test 目錄中生成了可執行文件 test,可是 test 目錄也有個 .libs 目錄,而這個 .libs 目錄裏也包含了一份可執行文件 test……這是 libtool 的第四步抽象:可執行文件的抽象。不知你有沒有注意到,生成 test 的過程與生成 libfoo.la 的過程幾乎是同樣的!其實也沒什麼好奇怪的,由於共享庫或靜態庫自己就是可執行文件。

foo/test/test 文件,實際上是一份 Bash 腳本,而 foo/test/.libs/test 纔是真正的 test 程序。爲了便於描述,我將前者稱爲 test 腳本,將後者稱爲 test 程序。test 腳本就是對 test 程序的抽象!

test 腳本所作的工做就是爲 test 程序的運行提供正確的環境。由於運行 test 程序,須要加載 foo 庫。按照 Linux 的共享庫加載邏輯,系統會自動去 /usr/lib 目錄爲 test 程序搜索共享庫 libfoo.so,或者去環境變量 LD_LIBRARY_PATH 所定義的路徑去搜索 libfoo.so。可是,可是,可是,此刻咱們的 foo 庫尚未被安裝,咱們也沒有設置 LD_LIBRARY_PATH 變量,test 程序是運行不起來的,因此,須要一個 test 腳原本抽象它!

用文本編輯器打開 test 腳本,在 200 多行 Bash 代碼中能夠看到如下內容:

# Add our own library path to LD_LIBRARY_PATH
    LD_LIBRARY_PATH="/tmp/foo/lib/.libs:$LD_LIBRARY_PATH"

    # Some systems cannot cope with colon-terminated LD_LIBRARY_PATH
    # The second colon is a workaround for a bug in BeOS R4 sed
    LD_LIBRARY_PATH=`$ECHO "$LD_LIBRARY_PATH" | /bin/sed 's/::*$//'`

    export LD_LIBRARY_PATH

我看着這份死活也看不懂的 test 腳本,深深感到 libtool 爲了讓這個什麼也不作的 test 程序可以正確的運行,嘔心瀝血,很拼的!

庫的安裝與卸載

foo 庫,通過個人精心測試,沒發現它有什麼 bug,如今我要將它安裝到系統中:

$ cd lib  # 由於剛纔跑到 foo 目錄下查看了目錄結構
$ sudo libtool --mode=install install -c libfoo.la /usr/local/lib

我以爲我不必再說廢話,簡明扼要的說,這是 libtool 的第五步抽象:庫文件安裝抽象。

事實上,不多有人去用 libtool 來安裝庫。大部分狀況下,libtool 是與 GNU Autotools 配合使用的。更正確的說法是,libtool 屬於 GNU Autotools。我不知道在這裏我將話題引到 GNU Autotools 是否是太唐突,由於有關 GNU Autotools 的故事,要差很少半個月才能講完……

簡單的說,GNU Autotools 就是產生兩份文件,一份文件是 configure,用於檢測項目構建(預處理、編譯、鏈接、安裝)環境是否完備;另外一份文件是 Makefile,用於項目的構建。若是咱們的項目是開發一個庫,那麼一旦有了 GNU Autotools 生成的 Makefile,編譯與安裝這個庫的命令一般是:

$ ./configure         # 檢測構建環境
$ make                # 編譯、鏈接
$ sudo make install   # 安裝

也就是說,Makefile 中包含了 libtool 的編譯、鏈接以及安裝等命令。這篇文章的目的是幫助你理解 libtool,並不是但願你使用 libtool 這個小木船來取代航母級別的 GNU Autotools。

既然之後極可能是用 GNU Autotools 來構建項目,所以能夠用 libtool 命令卸載剛纔所安裝的庫文件:

$ sudo libtool --mode=uninstall rm /usr/local/lib/libfoo.la

這是 libtool 的第六步抽象:庫文件卸載抽象。它對應於 GNU Autotools 產生的 Makefile 中的 make uninstall

歸根覆命

foo 庫,通過個人精心測試,沒發現它有什麼 bug 了,我想將源代碼分享給個人朋友……雖然我幾乎沒有這種朋友。

既然是分享,那麼就得將一些對別人無用的東西都刪掉。如今個人 foo 目錄中有這些文件:

$ cd ..  # 由於剛纔在 foo/lib 目錄中
$ tree -a
.
├── lib
│   ├── foo.c
│   ├── foo.h
│   ├── libfoo.la
│   ├── libfoo.lo
│   ├── libfoo.o
│   └── .libs
│       ├── libfoo.a
│       ├── libfoo.la -> ../libfoo.la
│       ├── libfoo.lai
│       ├── libfoo.o
│       ├── libfoo.so -> libfoo.so.0.0.0
│       ├── libfoo.so.0 -> libfoo.so.0.0.0
│       └── libfoo.so.0.0.0
└── test
    ├── .libs
    │   ├── test
    │   └── test.o
    ├── test
    ├── test.c
    ├── test.lo
    └── test.o

4 directories, 18 files

我要刪除 *.o*.lo*.la, *.lai, *.a, *.so.* 等文件,只保留源代碼文件。手動刪除有點繁瑣,爲此,libtool 提供了第七步抽象:歸根覆命的抽象。歸根覆命,這麼高大上的概念,是老子創造的。他說,『夫物芸芸,各歸其根。歸根曰靜,是謂覆命』。

哲學的事,先放一邊,libtool 讓 foo 庫歸根覆命的命令是:

$ cd lib
$ libtool --mode=clean rm libfoo.lo libfoo.la
$ cd ../test
$ libtool --mode=clean rm test.lo test

而後再看一下 foo 的目錄結構:

$ cd ..  # 由於剛纔在 test 目錄中
$ tree -a
.
├── lib
│   ├── foo.c
│   └── foo.h
└── test
    └── test.c

2 directories, 3 files

對於 GNU Autotools 產生的 Makefile 而言,libtool 的 clean 模式對應於 make clean

總結

將上面所講的 libtool 命令的用法都忘了吧,只須要大體上理解它對哪些事物進行了抽象便可。由於,GNU Autotools 提供的 autoconf 與 automake 已將全部的 libtool 命令隱藏在它們的黑暗魔法中了。

相關文章
相關標籤/搜索